responsive design and bug fixes

This commit is contained in:
Carl-Gerhard Lindesvärd
2023-11-04 10:01:22 +01:00
parent 13618d1fd4
commit f5670253bc
51 changed files with 992 additions and 336 deletions

View File

@@ -47,6 +47,7 @@ export function EventsTable({ data, pagination }: EventsTableProps) {
const profile = info.getValue();
return (
<Link
shallow
href={`/${params.organization}/${params.project}/profiles/${profile?.id}`}
className="flex items-center gap-2"
>
@@ -91,7 +92,7 @@ export function EventsTable({ data, pagination }: EventsTableProps) {
footer: () => 'Created At',
}),
];
}, []);
}, [params]);
return (
<>

View File

@@ -1,8 +1,11 @@
import { useState } from 'react';
import { cn } from '@/utils/cn';
import { MenuIcon } from 'lucide-react';
import Link from 'next/link';
import { Container } from '../Container';
import { Breadcrumbs } from '../navbar/Breadcrumbs';
import { NavbarMenu } from '../navbar/NavbarMenu';
import { NavbarUserDropdown } from '../navbar/NavbarUserDropdown';
interface MainLayoutProps {
children: React.ReactNode;
@@ -10,23 +13,40 @@ interface MainLayoutProps {
}
export function MainLayout({ children, className }: MainLayoutProps) {
const [visible, setVisible] = useState(false);
return (
<>
<div className="h-2 w-full bg-gradient-to-r from-blue-900 to-purple-600"></div>
<nav className="border-b border-border">
<Container className="flex h-20 items-center justify-between ">
<Link href="/" className="text-3xl">
<Link shallow href="/" className="text-3xl">
mixan
</Link>
<div className="flex items-center gap-8">
<div
className={cn(
'flex items-center gap-8 z-50',
visible === false && 'max-sm:hidden',
visible === true &&
'max-sm:flex max-sm:flex-col max-sm:absolute max-sm:inset-0 max-sm:bg-white max-sm:justify-center max-sm:top-4 max-sm:shadow-lg'
)}
>
<NavbarMenu />
<div>
<NavbarUserDropdown />
</div>
</div>
<button
className={cn(
'px-4 sm:hidden absolute z-50 top-9 right-4 transition-all',
visible === true && 'rotate-90'
)}
onClick={() => {
setVisible((p) => !p);
}}
>
<MenuIcon />
</button>
</Container>
</nav>
<main className={className}>{children}</main>
<Breadcrumbs />
<main className={cn(className, 'mb-8')}>{children}</main>
</>
);
}

View File

@@ -33,6 +33,7 @@ export function SettingsLayout({ children, className }: SettingsLayoutProps) {
<Sidebar>
{links.map(({ href, label }) => (
<Link
shallow
key={href}
href={href}
className={cn(

View File

@@ -0,0 +1,58 @@
import { useOrganizationParams } from '@/hooks/useOrganizationParams';
import { api } from '@/utils/api';
import { ChevronRight, HomeIcon } from 'lucide-react';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { Container } from '../Container';
export function Breadcrumbs() {
const params = useOrganizationParams();
const org = api.organization.get.useQuery(
{
slug: params.organization,
},
{
enabled: !!params.organization,
staleTime: Infinity,
}
);
const pro = api.project.get.useQuery(
{
slug: params.project,
},
{
enabled: !!params.project,
staleTime: Infinity,
}
);
return (
<div className="border-b border-border text-xs">
<Container className="flex items-center gap-2 h-8">
{org.isLoading && pro.isLoading && (
<div className="animate-pulse bg-slate-200 h-4 w-24 rounded"></div>
)}
{org.data && (
<>
<HomeIcon size={14} />
<Link shallow href={`/${org.data.slug}`}>
{org.data.name}
</Link>
</>
)}
{org.data && pro.data && (
<>
<ChevronRight size={10} />
<Link shallow href={`/${org.data.slug}/${pro.data.slug}`}>
{pro.data.name}
</Link>
</>
)}
</Container>
</div>
);
}

View File

@@ -1,4 +1,3 @@
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
DropdownMenuContent,
@@ -17,14 +16,17 @@ export function NavbarCreate() {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button size="sm">Create</Button>
<button>Create</button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56" align="end">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem asChild>
<Link href={`/${params.organization}/reports`}>
<Link
shallow
href={`/${params.organization}/${params.project}/reports`}
>
<LineChart className="mr-2 h-4 w-4" />
<span>Create a report</span>
</Link>

View File

@@ -1,24 +1,40 @@
import { useOrganizationParams } from '@/hooks/useOrganizationParams';
import { cn } from '@/utils/cn';
import Link from 'next/link';
import { NavbarCreate } from './NavbarCreate';
import { NavbarUserDropdown } from './NavbarUserDropdown';
export function NavbarMenu() {
const params = useOrganizationParams();
return (
<div className="flex gap-6 items-center">
<Link href={`/${params.organization}`}>Home</Link>
<div className={cn('flex gap-6 items-center text-sm', 'max-sm:flex-col')}>
{params.project && (
<Link href={`/${params.organization}/${params.project}/events`}>
<Link shallow href={`/${params.organization}/${params.project}`}>
Home
</Link>
)}
{params.project && (
<Link shallow href={`/${params.organization}/${params.project}/events`}>
Events
</Link>
)}
{params.project && (
<Link href={`/${params.organization}/${params.project}/profiles`}>
<Link
shallow
href={`/${params.organization}/${params.project}/profiles`}
>
Profiles
</Link>
)}
<NavbarCreate />
{params.project && (
<Link
shallow
href={`/${params.organization}/${params.project}/reports`}
>
Create report
</Link>
)}
<NavbarUserDropdown />
</div>
);
}

View File

@@ -27,25 +27,28 @@ export function NavbarUserDropdown() {
<DropdownMenuContent align="end" className="w-[200px]">
<DropdownMenuGroup>
<DropdownMenuItem asChild className="cursor-pointer">
<Link href={`/${params.organization}/settings/organization`}>
<Link
href={`/${params.organization}/settings/organization`}
shallow
>
<User className="mr-2 h-4 w-4" />
Organization
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild className="cursor-pointer">
<Link href={`/${params.organization}/settings/projects`}>
<Link href={`/${params.organization}/settings/projects`} shallow>
<User className="mr-2 h-4 w-4" />
Projects
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild className="cursor-pointer">
<Link href={`/${params.organization}/settings/clients`}>
<Link href={`/${params.organization}/settings/clients`} shallow>
<User className="mr-2 h-4 w-4" />
Clients
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild className="cursor-pointer">
<Link href={`/${params.organization}/settings/profile`}>
<Link href={`/${params.organization}/settings/profile`} shallow>
<User className="mr-2 h-4 w-4" />
Profile
</Link>

View File

@@ -3,32 +3,23 @@ import type { IChartType } from '@/types';
import { chartTypes } from '@/utils/constants';
import { Combobox } from '../ui/combobox';
import { RadioGroup, RadioGroupItem } from '../ui/radio-group';
import {
changeChartType,
changeDateRanges,
changeInterval,
} from './reportSlice';
import { changeChartType } from './reportSlice';
export function ReportChartType() {
const dispatch = useDispatch();
const type = useSelector((state) => state.report.chartType);
return (
<>
<div className="w-full max-w-[200px]">
<Combobox
placeholder="Chart type"
onChange={(value) => {
dispatch(changeChartType(value as IChartType));
}}
value={type}
items={Object.entries(chartTypes).map(([key, value]) => ({
label: value,
value: key,
}))}
/>
</div>
</>
<Combobox
placeholder="Chart type"
onChange={(value) => {
dispatch(changeChartType(value as IChartType));
}}
value={type}
items={Object.entries(chartTypes).map(([key, value]) => ({
label: value,
value: key,
}))}
/>
);
}

View File

@@ -1,49 +1,28 @@
import { useDispatch, useSelector } from '@/redux';
import type { IInterval } from '@/types';
import { intervals, timeRanges } from '@/utils/constants';
import { timeRanges } from '@/utils/constants';
import { Combobox } from '../ui/combobox';
import { RadioGroup, RadioGroupItem } from '../ui/radio-group';
import { changeDateRanges, changeInterval } from './reportSlice';
import { changeDateRanges } from './reportSlice';
export function ReportDateRange() {
const dispatch = useDispatch();
const range = useSelector((state) => state.report.range);
const interval = useSelector((state) => state.report.interval);
const chartType = useSelector((state) => state.report.chartType);
return (
<>
<RadioGroup>
{timeRanges.map((item) => {
return (
<RadioGroupItem
key={item.range}
active={item.range === range}
onClick={() => {
dispatch(changeDateRanges(item.range));
}}
>
{item.title}
</RadioGroupItem>
);
})}
</RadioGroup>
{chartType === 'linear' && (
<div className="w-full max-w-[200px]">
<Combobox
placeholder="Interval"
onChange={(value) => {
dispatch(changeInterval(value as IInterval));
<RadioGroup className="overflow-auto">
{timeRanges.map((item) => {
return (
<RadioGroupItem
key={item.range}
active={item.range === range}
onClick={() => {
dispatch(changeDateRanges(item.range));
}}
value={interval}
items={Object.entries(intervals).map(([key, value]) => ({
label: value,
value: key,
}))}
/>
</div>
)}
</>
>
{item.title}
</RadioGroupItem>
);
})}
</RadioGroup>
);
}

View File

@@ -0,0 +1,46 @@
import { useDispatch, useSelector } from '@/redux';
import type { IInterval } from '@/types';
import { isMinuteIntervalEnabledByRange } from '@/utils/constants';
import { Combobox } from '../ui/combobox';
import { changeInterval } from './reportSlice';
export function ReportInterval() {
const dispatch = useDispatch();
const interval = useSelector((state) => state.report.interval);
const range = useSelector((state) => state.report.range);
const chartType = useSelector((state) => state.report.chartType);
if (chartType !== 'linear') {
return null;
}
return (
<Combobox
placeholder="Interval"
onChange={(value) => {
dispatch(changeInterval(value as IInterval));
}}
value={interval}
items={[
{
value: 'minute',
label: 'Minute',
disabled: !isMinuteIntervalEnabledByRange(range),
},
{
value: 'hour',
label: 'Hour',
},
{
value: 'day',
label: 'Day',
},
{
value: 'month',
label: 'Month',
disabled: range < 1,
},
]}
/>
);
}

View File

@@ -1,12 +1,15 @@
import { Button } from '@/components/ui/button';
import { toast } from '@/components/ui/use-toast';
import { useOrganizationParams } from '@/hooks/useOrganizationParams';
import { pushModal } from '@/modals';
import { useSelector } from '@/redux';
import { api, handleError } from '@/utils/api';
import { SaveIcon } from 'lucide-react';
import { useReportId } from '../hooks/useReportId';
import { useReportId } from './hooks/useReportId';
export function ReportSaveButton() {
const params = useOrganizationParams();
const { reportId } = useReportId();
const update = api.report.update.useMutation({
onSuccess() {
@@ -27,10 +30,9 @@ export function ReportSaveButton() {
update.mutate({
reportId,
report,
dashboardId: '9227feb4-ad59-40f3-b887-3501685733dd',
projectId: 'f7eabf0c-e0b0-4ac0-940f-1589715b0c3d',
});
}}
icon={SaveIcon}
>
Update
</Button>
@@ -43,8 +45,9 @@ export function ReportSaveButton() {
report,
});
}}
icon={SaveIcon}
>
Create
Save
</Button>
);
}

View File

@@ -1,7 +1,6 @@
import { createContext, memo, useContext, useMemo } from 'react';
import { pick } from 'ramda';
interface ChartContextType {
export interface ChartContextType {
editMode: boolean;
}

View File

@@ -0,0 +1,30 @@
import React, { useEffect, useRef } from 'react';
import { useInViewport } from 'react-in-viewport';
import type { ReportChartProps } from '.';
import { Chart } from '.';
import type { ChartContextType } from './ChartProvider';
export function LazyChart(props: ReportChartProps & ChartContextType) {
const ref = useRef<HTMLDivElement>(null);
const once = useRef(false);
const { inViewport } = useInViewport(ref, undefined, {
disconnectOnLeave: true,
});
useEffect(() => {
if (inViewport) {
once.current = true;
}
}, [inViewport]);
return (
<div ref={ref}>
{once.current || inViewport ? (
<Chart {...props} editMode={false} />
) : (
<div className="h-64 w-full bg-gray-200 animate-pulse rounded" />
)}
</div>
);
}

View File

@@ -93,9 +93,9 @@ export function ReportBarChart({ data }: ReportBarChartProps) {
getCoreRowModel: getCoreRowModel(),
onSortingChange: setSorting,
getSortedRowModel: getSortedRowModel(),
debugTable: true,
debugHeaders: true,
debugColumns: true,
// debugTable: true,
// debugHeaders: true,
// debugColumns: true,
});
return (
<div ref={ref}>

View File

@@ -40,47 +40,52 @@ export function ReportLineChart({ interval, data }: ReportLineChartProps) {
return (
<>
<AutoSizer disableHeight>
{({ width }) => (
<LineChart width={width} height={Math.min(width * 0.5, 400)}>
<YAxis dataKey={'count'} width={30} fontSize={12}></YAxis>
<Tooltip content={<ReportLineChartTooltip />} />
<CartesianGrid strokeDasharray="3 3" />
<XAxis
fontSize={12}
dataKey="date"
tickFormatter={(m: string) => {
return formatDate(m);
}}
tickLine={false}
allowDuplicatedCategory={false}
/>
{data?.series
.filter((serie) => {
return visibleSeries.includes(serie.name);
})
.map((serie) => {
const realIndex = data?.series.findIndex(
(item) => item.name === serie.name
);
const key = serie.name;
const strokeColor = getChartColor(realIndex);
return (
<Line
type="monotone"
key={key}
isAnimationActive={false}
strokeWidth={2}
dataKey="count"
stroke={strokeColor}
data={serie.data}
name={serie.name}
/>
);
})}
</LineChart>
)}
</AutoSizer>
<div className="max-sm:-mx-3">
<AutoSizer disableHeight>
{({ width }) => (
<LineChart
width={width}
height={Math.min(Math.max(width * 0.5, 250), 400)}
>
<YAxis dataKey={'count'} width={30} fontSize={12}></YAxis>
<Tooltip content={<ReportLineChartTooltip />} />
<CartesianGrid strokeDasharray="3 3" />
<XAxis
fontSize={12}
dataKey="date"
tickFormatter={(m: string) => {
return formatDate(m);
}}
tickLine={false}
allowDuplicatedCategory={false}
/>
{data?.series
.filter((serie) => {
return visibleSeries.includes(serie.name);
})
.map((serie) => {
const realIndex = data?.series.findIndex(
(item) => item.name === serie.name
);
const key = serie.name;
const strokeColor = getChartColor(realIndex);
return (
<Line
type="monotone"
key={key}
isAnimationActive={false}
strokeWidth={2}
dataKey="count"
stroke={strokeColor}
data={serie.data}
name={serie.name}
/>
);
})}
</LineChart>
)}
</AutoSizer>
</div>
{editMode && (
<ReportTable
data={data}

View File

@@ -1,3 +1,5 @@
import { memo } from 'react';
import { useOrganizationParams } from '@/hooks/useOrganizationParams';
import type { IChartInput } from '@/types';
import { api } from '@/utils/api';
@@ -5,17 +7,18 @@ import { withChartProivder } from './ChartProvider';
import { ReportBarChart } from './ReportBarChart';
import { ReportLineChart } from './ReportLineChart';
type ReportLineChartProps = IChartInput;
export type ReportChartProps = IChartInput;
export const Chart = withChartProivder(
({
export const Chart = memo(
withChartProivder(function Chart({
interval,
events,
breakdowns,
chartType,
name,
range,
}: ReportLineChartProps) => {
}: ReportChartProps) {
const params = useOrganizationParams();
const hasEmptyFilters = events.some((event) =>
event.filters.some((filter) => filter.value.length === 0)
);
@@ -29,6 +32,7 @@ export const Chart = withChartProivder(
range,
startDate: null,
endDate: null,
projectSlug: params.project,
},
{
keepPreviousData: true,
@@ -63,5 +67,5 @@ export const Chart = withChartProivder(
}
return <p>Chart type &quot;{chartType}&quot; is not supported yet.</p>;
}
})
);

View File

@@ -6,7 +6,7 @@ import type {
IChartType,
IInterval,
} from '@/types';
import { alphabetIds } from '@/utils/constants';
import { alphabetIds, isMinuteIntervalEnabledByRange } from '@/utils/constants';
import { createSlice } from '@reduxjs/toolkit';
import type { PayloadAction } from '@reduxjs/toolkit';
@@ -104,6 +104,13 @@ export const reportSlice = createSlice({
// Chart type
changeChartType: (state, action: PayloadAction<IChartType>) => {
state.chartType = action.payload;
if (
!isMinuteIntervalEnabledByRange(state.range) &&
state.interval === 'minute'
) {
state.interval = 'hour';
}
},
// Date range

View File

@@ -1,6 +1,6 @@
import { ColorSquare } from '@/components/ColorSquare';
import { Combobox } from '@/components/ui/combobox';
import { RenderDots } from '@/components/ui/RenderDots';
import { useOrganizationParams } from '@/hooks/useOrganizationParams';
import { useDispatch, useSelector } from '@/redux';
import type { IChartBreakdown } from '@/types';
import { api } from '@/utils/api';
@@ -10,9 +10,12 @@ import { ReportBreakdownMore } from './ReportBreakdownMore';
import type { ReportEventMoreProps } from './ReportEventMore';
export function ReportBreakdowns() {
const params = useOrganizationParams();
const selectedBreakdowns = useSelector((state) => state.report.breakdowns);
const dispatch = useDispatch();
const propertiesQuery = api.chart.properties.useQuery();
const propertiesQuery = api.chart.properties.useQuery({
projectSlug: params.project,
});
const propertiesCombobox = (propertiesQuery.data ?? []).map((item) => ({
value: item,
label: item, // <RenderDots truncate>{item}</RenderDots>,

View File

@@ -14,6 +14,7 @@ import {
} from '@/components/ui/command';
import { RenderDots } from '@/components/ui/RenderDots';
import { useMappings } from '@/hooks/useMappings';
import { useOrganizationParams } from '@/hooks/useOrganizationParams';
import { useDispatch } from '@/redux';
import type {
IChartEvent,
@@ -37,10 +38,12 @@ export function ReportEventFilters({
isCreating,
setIsCreating,
}: ReportEventFiltersProps) {
const params = useOrganizationParams();
const dispatch = useDispatch();
const propertiesQuery = api.chart.properties.useQuery(
{
event: event.name,
projectSlug: params.project,
},
{
enabled: !!event.name,
@@ -99,11 +102,13 @@ interface FilterProps {
}
function Filter({ filter, event }: FilterProps) {
const params = useOrganizationParams();
const getLabel = useMappings();
const dispatch = useDispatch();
const potentialValues = api.chart.values.useQuery({
event: event.name,
property: filter.name,
projectSlug: params.project,
});
const valuesCombobox =

View File

@@ -2,6 +2,7 @@ import { useState } from 'react';
import { ColorSquare } from '@/components/ColorSquare';
import { Dropdown } from '@/components/Dropdown';
import { Combobox } from '@/components/ui/combobox';
import { useOrganizationParams } from '@/hooks/useOrganizationParams';
import { useDispatch, useSelector } from '@/redux';
import type { IChartEvent } from '@/types';
import { api } from '@/utils/api';
@@ -16,7 +17,10 @@ export function ReportEvents() {
const [isCreating, setIsCreating] = useState(false);
const selectedEvents = useSelector((state) => state.report.events);
const dispatch = useDispatch();
const eventsQuery = api.chart.events.useQuery();
const params = useOrganizationParams();
const eventsQuery = api.chart.events.useQuery({
projectSlug: params.project,
});
const eventsCombobox = (eventsQuery.data ?? []).map((item) => ({
value: item.name,
label: item.name,

View File

@@ -1,13 +1,17 @@
import { Button } from '@/components/ui/button';
import { SheetClose } from '@/components/ui/sheet';
import { ReportBreakdowns } from './ReportBreakdowns';
import { ReportEvents } from './ReportEvents';
import { ReportSaveButton } from './ReportSaveButton';
export function ReportSidebar() {
return (
<div className="flex flex-col gap-4 p-4">
<div className="flex flex-col gap-8">
<ReportEvents />
<ReportBreakdowns />
<ReportSaveButton />
<SheetClose asChild>
<Button>Done</Button>
</SheetClose>
</div>
);
}

View File

@@ -9,7 +9,7 @@ const Avatar = React.forwardRef<
<AvatarPrimitive.Root
ref={ref}
className={cn(
'relative flex h-9 w-9 shrink-0 overflow-hidden rounded-full',
'relative flex h-8 w-8 shrink-0 overflow-hidden rounded-full text-sm',
className
)}
{...props}

View File

@@ -3,6 +3,7 @@ import { cn } from '@/utils/cn';
import { Slot } from '@radix-ui/react-slot';
import { cva } from 'class-variance-authority';
import type { VariantProps } from 'class-variance-authority';
import type { LucideIcon, LucideProps } from 'lucide-react';
import { Loader2 } from 'lucide-react';
const buttonVariants = cva(
@@ -39,6 +40,7 @@ interface ButtonProps
VariantProps<typeof buttonVariants> {
asChild?: boolean;
loading?: boolean;
icon?: LucideIcon;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
@@ -51,11 +53,13 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
children,
loading,
disabled,
icon,
...props
},
ref
) => {
const Comp = asChild ? Slot : 'button';
const Icon = loading ? Loader2 : icon ?? null;
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
@@ -63,7 +67,10 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
disabled={loading ?? disabled}
{...props}
>
{loading ? <Loader2 className="animate-spin" /> : <>{children}</>}
{Icon && (
<Icon className={cn('h-4 w-4 mr-2', loading && 'animate-spin')} />
)}
{children}
</Comp>
);
}

View File

@@ -15,11 +15,14 @@ import {
import { cn } from '@/utils/cn';
import { Check, ChevronsUpDown } from 'lucide-react';
import { ScrollArea } from './scroll-area';
interface ComboboxProps {
placeholder: string;
items: {
value: string;
label: string;
disabled?: boolean;
}[];
value: string;
onChange: (value: string) => void;
@@ -51,7 +54,7 @@ export function Combobox({
variant="outline"
role="combobox"
aria-expanded={open}
className="w-full min-w-0 justify-between"
className="w-full justify-between min-w-[150px]"
>
<span className="overflow-hidden text-ellipsis">
{value ? find(value)?.label ?? 'No match' : placeholder}
@@ -82,27 +85,30 @@ export function Combobox({
) : (
<CommandEmpty>Nothing selected</CommandEmpty>
)}
<CommandGroup className="max-h-[200px] overflow-auto">
{items.map((item) => (
<CommandItem
key={item.value}
value={item.value}
onSelect={(currentValue) => {
const value = find(currentValue)?.value ?? currentValue;
onChange(value);
setOpen(false);
}}
>
<Check
className={cn(
'mr-2 h-4 w-4 flex-shrink-0',
value === item.value ? 'opacity-100' : 'opacity-0'
)}
/>
{item.label}
</CommandItem>
))}
</CommandGroup>
<div className="max-h-[300px] overflow-scroll">
<CommandGroup>
{items.map((item) => (
<CommandItem
key={item.value}
value={item.value}
onSelect={(currentValue) => {
const value = find(currentValue)?.value ?? currentValue;
onChange(value);
setOpen(false);
}}
{...(item.disabled && { disabled: true })}
>
<Check
className={cn(
'mr-2 h-4 w-4 flex-shrink-0',
value === item.value ? 'opacity-100' : 'opacity-0'
)}
/>
{item.label}
</CommandItem>
))}
</CommandGroup>
</div>
</Command>
</PopoverContent>
</Popover>

View File

@@ -114,9 +114,10 @@ const CommandItem = React.forwardRef<
<CommandPrimitive.Item
ref={ref}
className={cn(
'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none aria-selected:bg-accent aria-selected:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
'relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none aria-selected:bg-accent aria-selected:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
className
)}
data-disabled={props.disabled}
{...props}
/>
));

View File

@@ -10,7 +10,7 @@ const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = 'center', sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal>
<>
<PopoverPrimitive.Content
ref={ref}
align={align}
@@ -21,7 +21,7 @@ const PopoverContent = React.forwardRef<
)}
{...props}
/>
</PopoverPrimitive.Portal>
</>
));
PopoverContent.displayName = PopoverPrimitive.Content.displayName;

View File

@@ -0,0 +1,51 @@
import * as React from "react"
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
import { cn } from "@/utils/cn"
const ScrollArea = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root
ref={ref}
className={cn("relative overflow-hidden", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
))
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
const ScrollBar = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
>(({ className, orientation = "vertical", ...props }, ref) => (
<ScrollAreaPrimitive.ScrollAreaScrollbar
ref={ref}
orientation={orientation}
className={cn(
"flex touch-none select-none transition-colors",
orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent p-[1px]",
orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
className
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb
className={cn(
"relative rounded-full bg-border",
orientation === "vertical" && "flex-1"
)}
/>
</ScrollAreaPrimitive.ScrollAreaScrollbar>
))
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
export { ScrollArea, ScrollBar }

View File

@@ -0,0 +1,138 @@
import * as React from 'react';
import { cn } from '@/utils/cn';
import * as SheetPrimitive from '@radix-ui/react-dialog';
import { cva } from 'class-variance-authority';
import type { VariantProps } from 'class-variance-authority';
import { X } from 'lucide-react';
const Sheet = SheetPrimitive.Root;
const SheetTrigger = SheetPrimitive.Trigger;
const SheetClose = SheetPrimitive.Close;
const SheetPortal = SheetPrimitive.Portal;
const SheetOverlay = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Overlay
className={cn(
'fixed inset-0 z-50 bg-background/80 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
className
)}
{...props}
ref={ref}
/>
));
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
const sheetVariants = cva(
'fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500',
{
variants: {
side: {
top: 'inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top',
bottom:
'inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom',
left: 'inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm',
right:
'inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm',
},
},
defaultVariants: {
side: 'right',
},
}
);
interface SheetContentProps
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
VariantProps<typeof sheetVariants> {}
const SheetContent = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Content>,
SheetContentProps
>(({ side = 'right', className, children, ...props }, ref) => (
<SheetPortal>
<SheetOverlay className="backdrop-blur-none bg-transparent" />
<SheetPrimitive.Content
ref={ref}
className={cn(sheetVariants({ side }), className)}
{...props}
>
{children}
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
</SheetPrimitive.Content>
</SheetPortal>
));
SheetContent.displayName = SheetPrimitive.Content.displayName;
const SheetHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
'flex flex-col space-y-2 text-center sm:text-left',
className
)}
{...props}
/>
);
SheetHeader.displayName = 'SheetHeader';
const SheetFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
className
)}
{...props}
/>
);
SheetFooter.displayName = 'SheetFooter';
const SheetTitle = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Title
ref={ref}
className={cn('text-lg font-semibold text-foreground', className)}
{...props}
/>
));
SheetTitle.displayName = SheetPrimitive.Title.displayName;
const SheetDescription = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Description
ref={ref}
className={cn('text-sm text-muted-foreground', className)}
{...props}
/>
));
SheetDescription.displayName = SheetPrimitive.Description.displayName;
export {
Sheet,
SheetPortal,
SheetOverlay,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription,
};