0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-03-25 02:31:59 -05:00

Shade updates (#22025)

ref
https://linear.app/ghost/issue/DES-1022/overview-tab-for-post-analytics

- A static version of a React-only Post analytics page needed to be
worked out to learn how Charts, Tabs, Sidebars etc. work in Shade. This
also is a basis for learning more about React patterns.
This commit is contained in:
Peter Zimon 2025-01-20 13:56:21 +01:00 committed by GitHub
parent 4d93defea0
commit e1f5ff1533
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
45 changed files with 2804 additions and 245 deletions

View file

@ -1,10 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<defs>
<style>
circle{fill:currentColor}
.dotdotdotfill {fill:currentColor}
</style>
</defs>
<circle cx="3.25" cy="12" r="2.6"/>
<circle cx="12" cy="12" r="2.6"/>
<circle cx="20.75" cy="12" r="2.6"/>
</svg>
<circle class="dotdotdotfill" cx="3.25" cy="12" r="2.6"/>
<circle class="dotdotdotfill" cx="12" cy="12" r="2.6"/>
<circle class="dotdotdotfill" cx="20.75" cy="12" r="2.6"/>
</svg>

Before

Width:  |  Height:  |  Size: 256 B

After

Width:  |  Height:  |  Size: 332 B

View file

@ -1,7 +1,7 @@
import PostAnalytics from './pages/PostAnalytics';
import PostAnalytics from './views/post-analytics/PostAnalytics';
import {FrameworkProvider, TopLevelFrameworkProps} from '@tryghost/admin-x-framework';
import {RoutingProvider} from '@tryghost/admin-x-framework/routing';
import {ShadeApp, ShadeAppProps} from '@tryghost/shade';
import {ShadeApp, ShadeAppProps, SidebarProvider} from '@tryghost/shade';
interface AppProps {
framework: TopLevelFrameworkProps;
@ -13,7 +13,9 @@ const App: React.FC<AppProps> = ({framework, designSystem}) => {
<FrameworkProvider {...framework}>
<RoutingProvider basePath='posts-x'>
<ShadeApp className='posts' {...designSystem}>
<PostAnalytics />
<SidebarProvider>
<PostAnalytics />
</SidebarProvider>
</ShadeApp>
</RoutingProvider>
</FrameworkProvider>

View file

@ -0,0 +1,50 @@
import {Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator, Button, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuTrigger, H1, LucideIcon} from '@tryghost/shade';
interface headerProps {};
const Header: React.FC<headerProps> = () => {
return (
<div className="flex flex-col items-start justify-between gap-1 pt-9">
<div className='flex h-8 w-full items-center justify-between gap-3'>
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="/ghost/posts">
Posts
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>
Analytics
</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
<div className='flex items-center gap-2'>
<Button variant='outline'><LucideIcon.Share />Share</Button>
<DropdownMenu>
<DropdownMenuTrigger>
<Button variant='outline'><LucideIcon.Ellipsis /></Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="min-w-48">
<DropdownMenuItem>
<span>Edit post</span>
<DropdownMenuShortcut>E</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem>
<span>View in browser</span>
<DropdownMenuShortcut>O</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem className="text-red">Delete</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
<H1 className='mt-2 max-w-[960px]'>The Evolution of Basketball: From Pastime to Professional and One of the Most Popular Sports</H1>
</div>
);
};
export default Header;

View file

@ -1,26 +0,0 @@
import {Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator, H1} from '@tryghost/shade';
const Header = () => {
return (
<div className="pt-9">
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="/ghost/posts">
Posts
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>
Analytics
</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
<H1 className='mt-1 max-w-[1024px]'>The Evolution of Basketball: From Pastime to Professional and One of the Most Popular Sports</H1>
</div>
);
};
export default Header;

View file

@ -1,22 +0,0 @@
import * as React from 'react';
import {Card, CardContent, CardDescription, CardHeader, CardTitle} from '@tryghost/shade';
interface ClickPerformanceProps extends React.ComponentProps<typeof Card> {};
const ClickPerformance: React.FC<ClickPerformanceProps> = (props) => {
return (
<Card {...props}>
<CardHeader>
<CardTitle>Click performance</CardTitle>
<CardDescription>
Links in this newsletter
</CardDescription>
</CardHeader>
<CardContent>
Card contents
</CardContent>
</Card>
);
};
export default ClickPerformance;

View file

@ -1,22 +0,0 @@
import * as React from 'react';
import {Card, CardContent, CardDescription, CardHeader, CardTitle} from '@tryghost/shade';
interface ConversionsProps extends React.ComponentProps<typeof Card> {};
const Conversions: React.FC<ConversionsProps> = (props) => {
return (
<Card {...props}>
<CardHeader>
<CardTitle>Conversions</CardTitle>
<CardDescription>
3 members signed up on this post
</CardDescription>
</CardHeader>
<CardContent>
Card contents
</CardContent>
</Card>
);
};
export default Conversions;

View file

@ -1,22 +0,0 @@
import * as React from 'react';
import {Card, CardContent, CardDescription, CardHeader, CardTitle} from '@tryghost/shade';
interface FeedbackProps extends React.ComponentProps<typeof Card> {};
const Feedback: React.FC<FeedbackProps> = (props) => {
return (
<Card {...props}>
<CardHeader>
<CardTitle>Feedback</CardTitle>
<CardDescription>
188 reactions
</CardDescription>
</CardHeader>
<CardContent>
Card contents
</CardContent>
</Card>
);
};
export default Feedback;

View file

@ -1,22 +0,0 @@
import * as React from 'react';
import {Card, CardContent, CardDescription, CardHeader, CardTitle} from '@tryghost/shade';
interface NewsletterPerformanceProps extends React.ComponentProps<typeof Card> {};
const NewsletterPerformance: React.FC<NewsletterPerformanceProps> = (props) => {
return (
<Card {...props}>
<CardHeader>
<CardTitle>Newsletter performance</CardTitle>
<CardDescription>
Sent 19 Sept 2024
</CardDescription>
</CardHeader>
<CardContent>
Card contents
</CardContent>
</Card>
);
};
export default NewsletterPerformance;

View file

@ -1,47 +0,0 @@
import Header from '../components/layout/Header';
import Overview from '../components/post-analytics/Overview';
import {Button, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuTrigger, Icon, Page, Tabs, TabsContent, TabsList, TabsTrigger} from '@tryghost/shade';
const PostAnalytics = () => {
return (
<Page>
<Header />
<Tabs className='mt-7' defaultValue="overview" variant="page">
<div className='flex w-full items-center border-b pb-2'>
<TabsList className='flex w-full items-center justify-start border-none pb-0'>
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="newsletter">Newsletter</TabsTrigger>
</TabsList>
<div className='flex items-center gap-1'>
<Button variant='outline'><Icon.Share className='-mt-0.5' />Share</Button>
<DropdownMenu>
<DropdownMenuTrigger>
<Button variant='outline'><Icon.Dotdotdot /></Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="min-w-48">
<DropdownMenuItem>
<span>Edit post</span>
<DropdownMenuShortcut>E</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem>
<span>View in browser</span>
<DropdownMenuShortcut>O</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem className="text-red">Delete</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
<TabsContent value="overview">
<Overview />
</TabsContent>
<TabsContent value="newsletter">
Newsletter details
</TabsContent>
</Tabs>
</Page>
);
};
export default PostAnalytics;

View file

@ -0,0 +1,29 @@
import Header from '../../components/Header';
import Newsletter from './components/Newsletter';
import Overview from './components/Overview';
import {LucideIcon, Page, Tabs, TabsContent, TabsList, TabsTrigger} from '@tryghost/shade';
interface postAnalyticsProps {};
const PostAnalytics: React.FC<postAnalyticsProps> = () => {
return (
<Page>
<Header />
<Tabs className='my-8 flex grow flex-col' defaultValue="overview" variant="button">
<TabsList className='w-full'>
<TabsTrigger value="overview"><LucideIcon.Gauge /> Overview</TabsTrigger>
<TabsTrigger value="newsletter"><LucideIcon.Mail /> Newsletter</TabsTrigger>
<TabsTrigger value="web"><LucideIcon.Earth /> Web</TabsTrigger>
</TabsList>
<TabsContent value="overview">
<Overview />
</TabsContent>
<TabsContent className='mt-0 flex grow flex-col' value="newsletter">
<Newsletter />
</TabsContent>
</Tabs>
</Page>
);
};
export default PostAnalytics;

View file

@ -0,0 +1,104 @@
import OpenedList from './newsletter/OpenedList';
import React from 'react';
import SentList from './newsletter/SentList';
import {Badge} from '@tryghost/shade';
import {StatsTabItem, StatsTabTitle, StatsTabValue, StatsTabs, StatsTabsGroup} from './StatsTabs';
interface newsletterProps {};
const Newsletter: React.FC<newsletterProps> = () => {
const tabs = [
[
{
key: 'sent',
title: 'Sent',
value: '1,697',
badge: '',
content: <SentList />
},
{
key: 'opened',
title: 'Opened',
value: '560',
badge: '75%',
content: <OpenedList />
},
{
key: 'clicked',
title: 'Clicked',
value: '21',
badge: '18%',
content: <SentList />
}
],
[
{
key: 'unsubscribed',
title: 'Unsubscribed',
value: '21',
badge: '',
content: <SentList />
},
{
key: 'feedback',
title: 'Feedback',
value: '5',
badge: '',
content: <SentList />
},
{
key: 'spam',
title: 'Marked as spam',
value: '17',
badge: '',
content: <SentList />
},
{
key: 'bounced',
title: 'Bounced',
value: '81',
badge: '',
content: <SentList />
}
]
];
const [currentTab, setCurrentTab] = React.useState(tabs[0][0].key);
const Content: React.FC = () => {
return tabs.map((tabGroup) => {
const selectedTab = tabGroup.find(tab => tab.key === currentTab);
return selectedTab?.content;
});
};
return (
<div className='grid grow grid-cols-[auto_300px] gap-8 py-5'>
<div className='border-t border-border'>
<Content />
</div>
<div className='-mt-px flex basis-[300px] flex-col'>
<StatsTabs>
{tabs.map(group => (
<StatsTabsGroup>
{group.map(item => (
<StatsTabItem isActive={currentTab === item.key} onClick={() => {
setCurrentTab(item.key);
}}>
<StatsTabTitle>
{item.title}
{item.badge && <Badge variant='secondary'>{item.badge}</Badge>}
</StatsTabTitle>
<StatsTabValue>{item.value}</StatsTabValue>
</StatsTabItem>
))}
</StatsTabsGroup>
)
)}
</StatsTabs>
</div>
</div>
);
};
export default Newsletter;

View file

@ -3,9 +3,11 @@ import Conversions from './overview/Conversions';
import Feedback from './overview/Feedback';
import NewsletterPerformance from './overview/NewsletterPerformance';
const Overview = () => {
interface overviewProps {};
const Overview: React.FC<overviewProps> = () => {
return (
<div className="grid w-full grid-cols-3 gap-6 py-4">
<div className="grid w-full grid-cols-3 gap-5 py-5">
<NewsletterPerformance className='col-span-2' />
<Feedback />
<ClickPerformance className='col-span-2' />

View file

@ -0,0 +1,57 @@
import * as React from 'react';
import {Button, ButtonProps} from '@tryghost/shade';
import {cn} from '@tryghost/shade';
interface statsTabsProps
extends React.HTMLAttributes<HTMLDivElement> {}
const StatsTabs: React.FC<statsTabsProps> = ({className, ...props}) => {
return <div className={cn('flex flex-col gap-8', className)} {...props} />;
};
interface statsTabsGroupProps
extends React.HTMLAttributes<HTMLDivElement> {};
const StatsTabsGroup: React.FC<statsTabsGroupProps> = ({className, ...props}) => {
return <div className={cn('flex flex-col gap-2', className)} {...props} />;
};
interface subNavItemProps extends ButtonProps {
isActive?: boolean;
}
const StatsTabItem: React.FC<subNavItemProps> = ({isActive, ...props}) => {
const subNavItemClasses = cn(
'flex flex-col items-start h-auto py-3 gap-0 border border-border group/item',
isActive ? 'bg-muted/70' : 'border-gray-200 hover:bg-muted/50'
);
return (
<Button
className={subNavItemClasses}
data-state={isActive ? 'active' : 'inactive'}
variant='ghost' {...props}
/>
);
};
interface statsTabTitleProps
extends React.HTMLAttributes<HTMLDivElement> {}
const StatsTabTitle: React.FC<statsTabTitleProps> = ({className, ...props}) => {
return <div className={cn('min-h-5 w-full font-medium flex justify-between items-start text-gray-600 group-data-[state=active]/item:text-gray-800 gap-2', className)} {...props} />;
};
interface statsTabValueProps
extends React.HTMLAttributes<HTMLDivElement> {}
const StatsTabValue: React.FC<statsTabValueProps> = ({className, ...props}) => {
return <div className={cn('text-2xl text-black tracking-tight font-semibold -mt-1', className)} {...props} />;
};
export {
StatsTabs,
StatsTabsGroup,
StatsTabItem,
StatsTabTitle,
StatsTabValue
};

View file

@ -0,0 +1,68 @@
import * as React from 'react';
import {Avatar, AvatarFallback, AvatarImage, Table, TableBody, TableCell, TableHead, TableHeader, TableRow} from '@tryghost/shade';
interface openedListProps {};
const OpenedList: React.FC<openedListProps> = () => {
const mockData = [
{
name: 'Kaylynn Torff',
tier: 'Silver',
avatarImage: 'https://i.pravatar.cc/150?img=3',
avatarFallback: 'KT',
receiveDate: 'A month ago'
},
{
name: 'Abram Vaccaro',
tier: 'Gold',
avatarImage: 'https://i.pravatar.cc/150?img=4',
avatarFallback: 'AV',
receiveDate: 'A month ago'
},
{
name: 'Jaxson Westervelt',
tier: 'Free',
avatarImage: '',
avatarFallback: 'JW',
receiveDate: 'A month ago'
},
{
name: 'Kierra Bergson',
tier: 'Gold',
avatarImage: 'https://i.pravatar.cc/150?img=5',
avatarFallback: 'KB',
receiveDate: 'A month ago'
}
];
return (
<Table>
<TableHeader>
<TableRow className='h-[44px]'>
<TableHead className="w-1/3">Member</TableHead>
<TableHead className="w-1/3">Tier</TableHead>
<TableHead className="w-1/3">Opened</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{mockData.map(member => (
<TableRow key={member.name}>
<TableCell className="font-medium">
<div className="flex items-center gap-1.5">
<Avatar>
<AvatarImage src={member.avatarImage} />
<AvatarFallback>{member.avatarFallback}</AvatarFallback>
</Avatar>
<span className='whitespace-nowrap'>{member.name}</span>
</div>
</TableCell>
<TableCell className='text-gray-800'>{member.tier}</TableCell>
<TableCell className='text-gray-800'>{member.receiveDate}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
);
};
export default OpenedList;

View file

@ -0,0 +1,89 @@
import * as React from 'react';
import {Avatar, AvatarFallback, AvatarImage, Table, TableBody, TableCell, TableHead, TableHeader, TableRow} from '@tryghost/shade';
interface sentListProps {};
const SentList: React.FC<sentListProps> = () => {
const mockData = [
{
name: 'Gustavo Kenter',
tier: 'Gold',
avatarImage: 'https://i.pravatar.cc/150?img=1',
avatarFallback: 'GK',
receiveDate: 'A month ago'
},
{
name: 'Kadin Botosh',
tier: 'Free',
avatarImage: '',
avatarFallback: 'KB',
receiveDate: 'A month ago'
},
{
name: 'Skylar Lipshutz',
tier: 'Free',
avatarImage: 'https://i.pravatar.cc/150?img=2',
avatarFallback: 'SL',
receiveDate: 'A month ago'
},
{
name: 'Kaylynn Torff',
tier: 'Silver',
avatarImage: 'https://i.pravatar.cc/150?img=3',
avatarFallback: 'KT',
receiveDate: 'A month ago'
},
{
name: 'Abram Vaccaro',
tier: 'Gold',
avatarImage: 'https://i.pravatar.cc/150?img=4',
avatarFallback: 'AV',
receiveDate: 'A month ago'
},
{
name: 'Jaxson Westervelt',
tier: 'Free',
avatarImage: '',
avatarFallback: 'JW',
receiveDate: 'A month ago'
},
{
name: 'Kierra Bergson',
tier: 'Gold',
avatarImage: 'https://i.pravatar.cc/150?img=5',
avatarFallback: 'KB',
receiveDate: 'A month ago'
}
];
return (
<Table>
<TableHeader>
<TableRow className='h-[44px]'>
<TableHead className="w-1/3">Member</TableHead>
<TableHead className="w-1/3">Tier</TableHead>
<TableHead className="w-1/3">Received</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{mockData.map(member => (
<TableRow key={member.name}>
<TableCell className="font-medium">
<div className="flex items-center gap-1.5">
<Avatar>
<AvatarImage src={member.avatarImage} />
<AvatarFallback>{member.avatarFallback}</AvatarFallback>
</Avatar>
<span className='whitespace-nowrap'>{member.name}</span>
</div>
</TableCell>
<TableCell className='text-gray-800'>{member.tier}</TableCell>
<TableCell className='text-gray-800'>{member.receiveDate}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
);
};
export default SentList;

View file

@ -0,0 +1,65 @@
import * as React from 'react';
import {Button, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Table, TableBody, TableCell, TableHead, TableHeader, TableRow} from '@tryghost/shade';
interface ClickPerformanceProps extends React.ComponentProps<typeof Card> {};
const ClickPerformance: React.FC<ClickPerformanceProps> = (props) => {
const mockData = [
{
url: 'activitypub.ghost.org/archive',
clicks: '250'
},
{
url: 'nytimes.com',
clicks: '134'
},
{
url: 'activitypub.ghost.org/unsubscribe',
clicks: '71'
},
{
url: 'example.com',
clicks: '29'
},
{
url: 'example.com/clickme',
clicks: '13'
}
];
return (
<Card {...props}>
<CardHeader>
<CardTitle>Click performance</CardTitle>
<CardDescription>
Links in this newsletter
</CardDescription>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[100px]">URL</TableHead>
<TableHead className="text-right">No. of members</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{mockData.map((link) => {
return (
<TableRow key={link.url}>
<TableCell className="font-medium">{link.url}</TableCell>
<TableCell className="text-right text-gray-700">{link.clicks}</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</CardContent>
<CardFooter>
<Button variant="outline">See all &rarr;</Button>
</CardFooter>
</Card>
);
};
export default ClickPerformance;

View file

@ -0,0 +1,72 @@
import * as React from 'react';
import {Avatar, AvatarFallback, AvatarImage, Button, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Table, TableBody, TableCell, TableHead, TableHeader, TableRow} from '@tryghost/shade';
interface ConversionsProps extends React.ComponentProps<typeof Card> {};
const Conversions: React.FC<ConversionsProps> = (props) => {
const mockData = [
{
name: 'Gustavo Kenter',
tier: 'Gold',
avatarImage: 'https://i.pravatar.cc/150?img=1',
avatarFallback: 'GK',
receiveDate: 'A month ago'
},
{
name: 'Kadin Botosh',
tier: 'Free',
avatarImage: '',
avatarFallback: 'KB',
receiveDate: 'A month ago'
},
{
name: 'Skylar Lipshutz',
tier: 'Free',
avatarImage: 'https://i.pravatar.cc/150?img=2',
avatarFallback: 'SL',
receiveDate: 'A month ago'
}
];
return (
<Card {...props}>
<CardHeader>
<CardTitle>Conversions</CardTitle>
<CardDescription>
3 members signed up on this post
</CardDescription>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Member</TableHead>
<TableHead className="w-[100px] text-right">Tier</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{mockData.map(member => (
<TableRow key={member.name}>
<TableCell className="font-medium">
<div className="flex items-center gap-1.5">
<Avatar className='h-5 w-5'>
<AvatarImage src={member.avatarImage} />
<AvatarFallback>{member.avatarFallback}</AvatarFallback>
</Avatar>
<span className='whitespace-nowrap'>{member.name}</span>
</div>
</TableCell>
<TableCell className='text-right text-gray-700'>{member.tier}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
<CardFooter>
<Button variant="outline">See all &rarr;</Button>
</CardFooter>
</Card>
);
};
export default Conversions;

View file

@ -0,0 +1,107 @@
import * as React from 'react';
import {Card, CardContent, CardDescription, CardHeader, CardTitle, ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent, Recharts, Separator} from '@tryghost/shade';
import {Metric, MetricLabel, MetricValue} from './Metric';
interface FeedbackProps extends React.ComponentProps<typeof Card> {};
const Feedback: React.FC<FeedbackProps> = (props) => {
const chartData = React.useMemo(() => {
return [
{browser: 'chrome', visitors: 98, fill: 'var(--color-chrome)'},
{browser: 'safari', visitors: 17, fill: 'var(--color-safari)'}
];
}, []);
const chartConfig = {
visitors: {
label: 'Reactions'
},
chrome: {
label: 'More like this',
color: 'hsl(var(--chart-1))'
},
safari: {
label: 'Less like this',
color: 'hsl(var(--chart-5))'
}
} satisfies ChartConfig;
const totalVisitors = React.useMemo(() => {
return chartData.reduce((acc, curr) => acc + curr.visitors, 0);
}, [chartData]);
return (
<Card {...props}>
<CardHeader>
<CardTitle>Feedback</CardTitle>
<CardDescription>
188 reactions
</CardDescription>
</CardHeader>
<CardContent>
<Separator />
<div className='grid grid-cols-2 gap-5 py-5'>
<Metric>
<MetricLabel>More like this</MetricLabel>
<MetricValue>98</MetricValue>
</Metric>
<Metric>
<MetricLabel>Less like this</MetricLabel>
<MetricValue>17</MetricValue>
</Metric>
</div>
<ChartContainer
className="mx-auto aspect-square h-[250px] min-h-[250px] w-full"
config={chartConfig}
>
<Recharts.PieChart>
<ChartTooltip
content={<ChartTooltipContent hideLabel />}
cursor={false}
/>
<Recharts.Pie
data={chartData}
dataKey="visitors"
innerRadius={60}
nameKey="browser"
strokeWidth={5}
>
<Recharts.Label
content={({viewBox}) => {
if (viewBox && 'cx' in viewBox && 'cy' in viewBox) {
return (
<text
dominantBaseline="middle"
textAnchor="middle"
x={viewBox.cx}
y={viewBox.cy}
>
<tspan
className="fill-foreground text-2xl font-semibold tracking-tight"
x={viewBox.cx}
y={viewBox.cy}
>
{totalVisitors.toLocaleString()}
</tspan>
<tspan
className="fill-muted-foreground"
x={viewBox.cx}
y={(viewBox.cy || 0) + 20}
>
Reactions
</tspan>
</text>
);
}
}}
/>
</Recharts.Pie>
</Recharts.PieChart>
</ChartContainer>
</CardContent>
</Card>
);
};
export default Feedback;

View file

@ -0,0 +1,31 @@
import * as React from 'react';
import {cn} from '@tryghost/shade';
export interface metricDivProps
extends React.HTMLAttributes<HTMLDivElement> {}
const Metric = ({className, ...props}: metricDivProps) => {
return (
<div className={cn('flex flex-col gap-0.5', className)} {...props} />
);
};
const MetricLabel = ({className, ...props}: metricDivProps) => {
return (
<div className={cn('text-sm text-gray-700 font-medium', className)} {...props} />
);
};
const MetricValue = ({className, ...props}: metricDivProps) => {
return (
<div className={cn('inline-flex gap-2 items-center font-semibold text-2xl tracking-tight leading-none', className)} {...props} />
);
};
const MetricPercentage = ({className, ...props}: metricDivProps) => {
return (
<div className={cn('text-xs tracking-normal inline-block bg-gray-100 text-gray-700 p-1 rounded-md', className)} {...props} />
);
};
export {Metric, MetricLabel, MetricValue, MetricPercentage};

View file

@ -0,0 +1,72 @@
import * as React from 'react';
import {Badge, Card, CardContent, CardDescription, CardHeader, CardTitle, ChartConfig, ChartContainer, ChartLegend, ChartLegendContent, ChartTooltip, ChartTooltipContent, Recharts, Separator} from '@tryghost/shade';
import {Metric, MetricLabel, MetricPercentage, MetricValue} from './Metric';
interface NewsletterPerformanceProps extends React.ComponentProps<typeof Card> {};
const NewsletterPerformance: React.FC<NewsletterPerformanceProps> = (props) => {
const chartData = [
{metric: 'Sent', current: 1697, avg: 1524},
{metric: 'Opened', current: 1184, avg: 867},
{metric: 'Clicked', current: 750, avg: 478}
];
const chartConfig = {
current: {
label: 'This post',
color: 'hsl(var(--chart-1))'
},
avg: {
label: 'Your average post',
color: 'hsl(var(--chart-5))'
}
} satisfies ChartConfig;
return (
<Card {...props}>
<CardHeader>
<CardTitle>Newsletter performance</CardTitle>
<CardDescription>
<Badge variant='success'>Sent</Badge> 19 Sept 2024
</CardDescription>
</CardHeader>
<CardContent>
<Separator />
<div className='grid grid-cols-3 py-5'>
<Metric className='pl-6'>
<MetricLabel>Sent</MetricLabel>
<MetricValue>1,697</MetricValue>
</Metric>
<Metric className='pl-6'>
<MetricLabel>Opened</MetricLabel>
<MetricValue>1,184 <MetricPercentage>69%</MetricPercentage></MetricValue>
</Metric>
<Metric className='pl-6'>
<MetricLabel>Clicked</MetricLabel>
<MetricValue>750 <MetricPercentage>44%</MetricPercentage></MetricValue>
</Metric>
</div>
<ChartContainer className='-mx-1 aspect-auto h-[250px] min-h-[250px] w-[calc(100%+8px)]' config={chartConfig}>
<Recharts.BarChart data={chartData} dataKey='metric' accessibilityLayer>
<Recharts.CartesianGrid vertical={false} />
<Recharts.XAxis
axisLine={false}
dataKey="metric"
tickLine={false}
tickMargin={10}
hide
/>
<ChartTooltip content={<ChartTooltipContent />} />
<ChartLegend content={<ChartLegendContent />} />
<Recharts.Bar dataKey="current" fill="var(--color-current)" radius={4} />
<Recharts.Bar dataKey="avg" fill="var(--color-avg)" radius={4} />
</Recharts.BarChart>
</ChartContainer>
</CardContent>
</Card>
);
};
export default NewsletterPerformance;

View file

@ -67,28 +67,30 @@
"@ebay/nice-modal-react": "1.2.13",
"@radix-ui/react-avatar": "1.1.0",
"@radix-ui/react-checkbox": "1.1.1",
"@radix-ui/react-dialog": "^1.1.4",
"@radix-ui/react-dropdown-menu": "2.1.3",
"@radix-ui/react-form": "0.0.3",
"@radix-ui/react-popover": "1.1.1",
"@radix-ui/react-radio-group": "1.2.0",
"@radix-ui/react-separator": "1.1.0",
"@radix-ui/react-slot": "1.1.1",
"@radix-ui/react-separator": "^1.1.1",
"@radix-ui/react-slot": "^1.1.1",
"@radix-ui/react-switch": "1.1.0",
"@radix-ui/react-tabs": "1.1.2",
"@radix-ui/react-tooltip": "1.1.2",
"@radix-ui/react-tooltip": "^1.1.6",
"@sentry/react": "7.119.2",
"@tailwindcss/forms": "0.5.9",
"@tailwindcss/line-clamp": "0.4.4",
"@uiw/react-codemirror": "4.23.7",
"autoprefixer": "10.4.19",
"class-variance-authority": "0.7.1",
"class-variance-authority": "^0.7.1",
"clsx": "2.1.1",
"lucide-react": "0.468.0",
"lucide-react": "^0.471.1",
"postcss": "8.4.39",
"postcss-import": "16.1.0",
"react-colorful": "5.6.1",
"react-hot-toast": "2.5.1",
"react-select": "5.8.2",
"recharts": "^2.15.0",
"tailwind-merge": "2.6.0",
"tailwindcss": "3.4.14",
"tailwindcss-animate": "1.0.7"
@ -97,4 +99,4 @@
"react": "^18.2.0",
"react-dom": "^18.2.0"
}
}
}

View file

@ -1,10 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<defs>
<style>
circle{fill:currentColor}
.dotdotdotfill {fill:currentColor}
</style>
</defs>
<circle cx="3.25" cy="12" r="2.6"/>
<circle cx="12" cy="12" r="2.6"/>
<circle cx="20.75" cy="12" r="2.6"/>
</svg>
<circle class="dotdotdotfill" cx="3.25" cy="12" r="2.6"/>
<circle class="dotdotdotfill" cx="12" cy="12" r="2.6"/>
<circle class="dotdotdotfill" cx="20.75" cy="12" r="2.6"/>
</svg>

Before

Width:  |  Height:  |  Size: 256 B

After

Width:  |  Height:  |  Size: 332 B

View file

@ -9,7 +9,7 @@ const Page = React.forwardRef<HTMLDivElement, PageProps>(
return (
<div
ref={ref}
className={cn('max-w-page mx-auto w-full px-12', className)}
className={cn('max-w-page mx-auto w-full min-h-full px-8 flex flex-col', className)}
{...props}
/>
);

View file

@ -0,0 +1,48 @@
import * as React from 'react';
import * as AvatarPrimitive from '@radix-ui/react-avatar';
import {cn} from '@/lib/utils';
const Avatar = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
>(({className, ...props}, ref) => (
<AvatarPrimitive.Root
ref={ref}
className={cn(
'relative flex h-8 w-8 shrink-0 overflow-hidden rounded-full',
className
)}
{...props}
/>
));
Avatar.displayName = AvatarPrimitive.Root.displayName;
const AvatarImage = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Image>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
>(({className, ...props}, ref) => (
<AvatarPrimitive.Image
ref={ref}
className={cn('aspect-square h-full w-full', className)}
{...props}
/>
));
AvatarImage.displayName = AvatarPrimitive.Image.displayName;
const AvatarFallback = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Fallback>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
>(({className, ...props}, ref) => (
<AvatarPrimitive.Fallback
ref={ref}
className={cn(
'flex h-full w-full items-center justify-center rounded-full bg-muted',
className
)}
{...props}
/>
));
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
export {Avatar, AvatarImage, AvatarFallback};

View file

@ -0,0 +1,38 @@
import * as React from 'react';
import {cva, type VariantProps} from 'class-variance-authority';
import {cn} from '@/lib/utils';
const badgeVariants = cva(
'inline-flex items-center rounded-sm border px-1.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
{
variants: {
variant: {
default:
'border-transparent bg-primary text-primary-foreground',
secondary:
'border-transparent bg-secondary text-secondary-foreground/70',
destructive:
'border-transparent bg-destructive/20 text-destructive',
success:
'border-transparent bg-green/20 text-green',
outline: 'text-foreground'
}
},
defaultVariants: {
variant: 'default'
}
}
);
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({className, variant, ...props}: BadgeProps) {
return (
<div className={cn(badgeVariants({variant}), className)} {...props} />
);
}
export {Badge, badgeVariants};

View file

@ -9,7 +9,7 @@ const Card = React.forwardRef<
<div
ref={ref}
className={cn(
'rounded-xl border bg-card text-card-foreground',
'rounded-xl border bg-card text-card-foreground flex flex-col',
className
)}
{...props}
@ -23,7 +23,7 @@ const CardHeader = React.forwardRef<
>(({className, ...props}, ref) => (
<div
ref={ref}
className={cn('flex flex-col space-y-1.5 p-6', className)}
className={cn('flex flex-col space-y-1.5 px-6 py-5', className)}
{...props}
/>
));
@ -35,7 +35,7 @@ const CardTitle = React.forwardRef<
>(({className, ...props}, ref) => (
<div
ref={ref}
className={cn('font-semibold leading-none', className)}
className={cn('text-lg tracking-tight font-semibold leading-none', className)}
{...props}
/>
));
@ -65,11 +65,13 @@ const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({className, ...props}, ref) => (
<div
ref={ref}
className={cn('flex items-center p-6 pt-0', className)}
{...props}
/>
<div className='flex grow items-end'>
<div
ref={ref}
className={cn('flex items-center p-6 pt-0', className)}
{...props}
/>
</div>
));
CardFooter.displayName = 'CardFooter';

View file

@ -0,0 +1,363 @@
import * as React from 'react';
import * as RechartsPrimitive from 'recharts';
import {cn} from '@/lib/utils';
// Format: { THEME_NAME: CSS_SELECTOR }
const THEMES = {light: '', dark: '.dark'} as const;
export type ChartConfig = {
[k in string]: {
label?: React.ReactNode
icon?: React.ComponentType
} & (
| { color?: string; theme?: never }
| { color?: never; theme: Record<keyof typeof THEMES, string> }
)
}
type ChartContextProps = {
config: ChartConfig
}
const ChartContext = React.createContext<ChartContextProps | null>(null);
function useChart() {
const context = React.useContext(ChartContext);
if (!context) {
throw new Error('useChart must be used within a <ChartContainer />');
}
return context;
}
const ChartContainer = React.forwardRef<
HTMLDivElement,
React.ComponentProps<'div'> & {
config: ChartConfig
children: React.ComponentProps<
typeof RechartsPrimitive.ResponsiveContainer
>['children']
}
>(({id, className, children, config, ...props}, ref) => {
const uniqueId = React.useId();
const chartId = `chart-${id || uniqueId.replace(/:/g, '')}`;
return (
<ChartContext.Provider value={{config}}>
<div
ref={ref}
className={cn(
'flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke=\'#ccc\']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke=\'#fff\']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke=\'#ccc\']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke=\'#ccc\']]:stroke-border [&_.recharts-sector[stroke=\'#fff\']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none',
className
)}
data-chart={chartId}
{...props}
>
<ChartStyle config={config} id={chartId} />
<RechartsPrimitive.ResponsiveContainer>
{children}
</RechartsPrimitive.ResponsiveContainer>
</div>
</ChartContext.Provider>
);
});
ChartContainer.displayName = 'Chart';
const ChartStyle = ({id, config}: { id: string; config: ChartConfig }) => {
const colorConfig = Object.entries(config).filter(
([, themeConfig]) => themeConfig.theme || themeConfig.color
);
if (!colorConfig.length) {
return null;
}
return (
<style
dangerouslySetInnerHTML={{
__html: Object.entries(THEMES)
.map(
([theme, prefix]) => `
${prefix} [data-chart=${id}] {
${colorConfig
.map(([key, itemConfig]) => {
const color =
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
itemConfig.color;
return color ? ` --color-${key}: ${color};` : null;
})
.join('\n')}
}
`
)
.join('\n')
}}
/>
);
};
const ChartTooltip = RechartsPrimitive.Tooltip;
const ChartTooltipContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
React.ComponentProps<'div'> & {
hideLabel?: boolean
hideIndicator?: boolean
indicator?: 'line' | 'dot' | 'dashed'
nameKey?: string
labelKey?: string
}
>(
(
{
active,
payload,
className,
indicator = 'dot',
hideLabel = false,
hideIndicator = false,
label,
labelFormatter,
labelClassName,
formatter,
color,
nameKey,
labelKey
},
ref
) => {
const {config} = useChart();
const tooltipLabel = React.useMemo(() => {
if (hideLabel || !payload?.length) {
return null;
}
const [item] = payload;
const key = `${labelKey || item.dataKey || item.name || 'value'}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key);
const value =
!labelKey && typeof label === 'string'
? config[label as keyof typeof config]?.label || label
: itemConfig?.label;
if (labelFormatter) {
return (
<div className={cn('font-medium', labelClassName)}>
{labelFormatter(value, payload)}
</div>
);
}
if (!value) {
return null;
}
return <div className={cn('font-medium', labelClassName)}>{value}</div>;
}, [
label,
labelFormatter,
payload,
hideLabel,
labelClassName,
config,
labelKey
]);
if (!active || !payload?.length) {
return null;
}
const nestLabel = payload.length === 1 && indicator !== 'dot';
return (
<div
ref={ref}
className={cn(
'grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl',
className
)}
>
{!nestLabel ? tooltipLabel : null}
<div className="grid gap-1.5">
{payload.map((item, index) => {
const key = `${nameKey || item.name || item.dataKey || 'value'}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key);
const indicatorColor = color || item.payload.fill || item.color;
return (
<div
key={item.dataKey}
className={cn(
'flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground',
indicator === 'dot' && 'items-center'
)}
>
{formatter && item?.value !== undefined && item.name ? (
formatter(item.value, item.name, item, index, item.payload)
) : (
<>
{itemConfig?.icon ? (
<itemConfig.icon />
) : (
!hideIndicator && (
<div
className={cn(
'shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]',
{
'h-2.5 w-2.5': indicator === 'dot',
'w-1': indicator === 'line',
'w-0 border-[1.5px] border-dashed bg-transparent':
indicator === 'dashed',
'my-0.5': nestLabel && indicator === 'dashed'
}
)}
style={
{
'--color-bg': indicatorColor,
'--color-border': indicatorColor
} as React.CSSProperties
}
/>
)
)}
<div
className={cn(
'flex flex-1 justify-between leading-none gap-1.5',
nestLabel ? 'items-end' : 'items-center'
)}
>
<div className="grid gap-1.5">
{nestLabel ? tooltipLabel : null}
<span className="text-muted-foreground">
{itemConfig?.label || item.name}
</span>
</div>
{item.value && (
<span className="font-mono font-medium tabular-nums text-foreground">
{item.value.toLocaleString()}
</span>
)}
</div>
</>
)}
</div>
);
})}
</div>
</div>
);
}
);
ChartTooltipContent.displayName = 'ChartTooltip';
const ChartLegend = RechartsPrimitive.Legend;
const ChartLegendContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<'div'> &
Pick<RechartsPrimitive.LegendProps, 'payload' | 'verticalAlign'> & {
hideIcon?: boolean
nameKey?: string
}
>(
(
{className, hideIcon = false, payload, verticalAlign = 'bottom', nameKey},
ref
) => {
const {config} = useChart();
if (!payload?.length) {
return null;
}
return (
<div
ref={ref}
className={cn(
'flex items-center justify-center gap-4',
verticalAlign === 'top' ? 'pb-3' : 'pt-3',
className
)}
>
{payload.map((item) => {
const key = `${nameKey || item.dataKey || 'value'}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key);
return (
<div
key={item.value}
className={cn(
'flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground'
)}
>
{itemConfig?.icon && !hideIcon ? (
<itemConfig.icon />
) : (
<div
className="h-2 w-2 shrink-0 rounded-[2px]"
style={{
backgroundColor: item.color
}}
/>
)}
{itemConfig?.label}
</div>
);
})}
</div>
);
}
);
ChartLegendContent.displayName = 'ChartLegend';
// Helper to extract item config from a payload.
function getPayloadConfigFromPayload(
config: ChartConfig,
payload: unknown,
key: string
) {
if (typeof payload !== 'object' || payload === null) {
return undefined;
}
const payloadPayload =
'payload' in payload &&
typeof payload.payload === 'object' &&
payload.payload !== null
? payload.payload
: undefined;
let configLabelKey: string = key;
if (
key in payload &&
typeof payload[key as keyof typeof payload] === 'string'
) {
configLabelKey = payload[key as keyof typeof payload] as string;
} else if (
payloadPayload &&
key in payloadPayload &&
typeof payloadPayload[key as keyof typeof payloadPayload] === 'string'
) {
configLabelKey = payloadPayload[
key as keyof typeof payloadPayload
] as string;
}
return configLabelKey in config
? config[configLabelKey]
: config[key as keyof typeof config];
}
export {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
ChartLegend,
ChartLegendContent,
ChartStyle
};

View file

@ -177,7 +177,7 @@ const DropdownMenuShortcut = ({
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn('ml-auto text-xs tracking-widest opacity-60', className)}
className={cn('ml-auto text-xs tracking-wider opacity-60', className)}
{...props}
/>
);

View file

@ -0,0 +1,22 @@
import * as React from 'react';
import {cn} from '@/lib/utils';
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<'input'>>(
({className, type, ...props}, ref) => {
return (
<input
ref={ref}
className={cn(
'flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
className
)}
type={type}
{...props}
/>
);
}
);
Input.displayName = 'Input';
export {Input};

View file

@ -0,0 +1,29 @@
import * as React from 'react';
import * as SeparatorPrimitive from '@radix-ui/react-separator';
import {cn} from '@/lib/utils';
const Separator = React.forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>(
(
{className, orientation = 'horizontal', decorative = true, ...props},
ref
) => (
<SeparatorPrimitive.Root
ref={ref}
className={cn(
'shrink-0 bg-border',
orientation === 'horizontal' ? 'h-[1px] w-full' : 'h-full w-[1px]',
className
)}
decorative={decorative}
orientation={orientation}
{...props}
/>
)
);
Separator.displayName = SeparatorPrimitive.Root.displayName;
export {Separator};

View file

@ -0,0 +1,140 @@
'use client';
import * as React from 'react';
import * as SheetPrimitive from '@radix-ui/react-dialog';
import {cva, type VariantProps} from 'class-variance-authority';
import {X} from 'lucide-react';
import {cn} from '@/lib/utils';
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-black/80 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=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out',
{
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 />
<SheetPrimitive.Content
ref={ref}
className={cn(sheetVariants({side}), className)}
{...props}
>
<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>
{children}
</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
};

View file

@ -0,0 +1,765 @@
import * as React from 'react';
import {Slot} from '@radix-ui/react-slot';
import {VariantProps, cva} from 'class-variance-authority';
import {PanelLeft} from 'lucide-react';
import {useIsMobile} from '@/hooks/use-mobile';
import {cn} from '@/lib/utils';
import {Button} from '@/components/ui/button';
import {Input} from '@/components/ui/input';
import {Separator} from '@/components/ui/separator';
import {Sheet, SheetContent} from '@/components/ui/sheet';
import {Skeleton} from '@/components/ui/skeleton';
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger
} from '@/components/ui/tooltip';
const SIDEBAR_COOKIE_NAME = 'sidebar:state';
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
const SIDEBAR_WIDTH = '16rem';
const SIDEBAR_WIDTH_MOBILE = '18rem';
const SIDEBAR_WIDTH_ICON = '3rem';
const SIDEBAR_KEYBOARD_SHORTCUT = 'b';
const SIDEBAR_MENU_HEIGHT = '9';
type SidebarContext = {
state: 'expanded' | 'collapsed'
open: boolean
setOpen: (open: boolean) => void
openMobile: boolean
setOpenMobile: (open: boolean) => void
isMobile: boolean
toggleSidebar: () => void
}
const SidebarContext = React.createContext<SidebarContext | null>(null);
function useSidebar() {
const context = React.useContext(SidebarContext);
if (!context) {
throw new Error('useSidebar must be used within a SidebarProvider.');
}
return context;
}
const SidebarProvider = React.forwardRef<
HTMLDivElement,
React.ComponentProps<'div'> & {
defaultOpen?: boolean
open?: boolean
onOpenChange?: (open: boolean) => void
}
>(
(
{
defaultOpen = true,
open: openProp,
onOpenChange: setOpenProp,
className,
style,
children,
...props
},
ref
) => {
const isMobile = useIsMobile();
const [openMobile, setOpenMobile] = React.useState(false);
// This is the internal state of the sidebar.
// We use openProp and setOpenProp for control from outside the component.
const [_open, _setOpen] = React.useState(defaultOpen);
const open = openProp ?? _open;
const setOpen = React.useCallback(
(newValue: boolean | ((value: boolean) => boolean)) => {
const openState = typeof newValue === 'function' ? newValue(open) : newValue;
if (setOpenProp) {
setOpenProp(openState);
} else {
_setOpen(openState);
}
// This sets the cookie to keep the sidebar state.
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
},
[setOpenProp, open]
);
// Helper to toggle the sidebar.
const toggleSidebar = React.useCallback(() => {
return isMobile
? setOpenMobile(prevOpen => !prevOpen)
: setOpen(prevOpen => !prevOpen);
}, [isMobile, setOpen, setOpenMobile]);
// Adds a keyboard shortcut to toggle the sidebar.
React.useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (
event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
(event.metaKey || event.ctrlKey)
) {
event.preventDefault();
toggleSidebar();
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [toggleSidebar]);
// We add a state so that we can do data-state="expanded" or "collapsed".
// This makes it easier to style the sidebar with Tailwind classes.
const state = open ? 'expanded' : 'collapsed';
const contextValue = React.useMemo<SidebarContext>(
() => ({
state,
open,
setOpen,
isMobile,
openMobile,
setOpenMobile,
toggleSidebar
}),
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
);
return (
<SidebarContext.Provider value={contextValue}>
<TooltipProvider delayDuration={0}>
<div
ref={ref}
className={cn(
'group/sidebar-wrapper flex min-h-svh w-full has-[[data-variant=inset]]:bg-sidebar',
className
)}
style={
{
'--sidebar-width': SIDEBAR_WIDTH,
'--sidebar-width-icon': SIDEBAR_WIDTH_ICON,
...style
} as React.CSSProperties
}
{...props}
>
{children}
</div>
</TooltipProvider>
</SidebarContext.Provider>
);
}
);
SidebarProvider.displayName = 'SidebarProvider';
const Sidebar = React.forwardRef<
HTMLDivElement,
React.ComponentProps<'div'> & {
side?: 'left' | 'right'
variant?: 'sidebar' | 'floating' | 'inset'
collapsible?: 'offcanvas' | 'icon' | 'none'
}
>(
(
{
side = 'left',
variant = 'sidebar',
collapsible = 'offcanvas',
className,
children,
...props
},
ref
) => {
const {isMobile, state, openMobile, setOpenMobile} = useSidebar();
if (collapsible === 'none') {
return (
<div
ref={ref}
className={cn(
'flex h-full w-[--sidebar-width] flex-col bg-sidebar text-sidebar-foreground',
className
)}
{...props}
>
{children}
</div>
);
}
if (isMobile) {
return (
<Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
<SheetContent
className="w-[--sidebar-width] bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden"
data-mobile="true"
data-sidebar="sidebar"
side={side}
style={
{
'--sidebar-width': SIDEBAR_WIDTH_MOBILE
} as React.CSSProperties
}
>
<div className="flex h-full w-full flex-col">{children}</div>
</SheetContent>
</Sheet>
);
}
return (
<div
ref={ref}
className="group peer hidden text-sidebar-foreground md:block"
data-collapsible={state === 'collapsed' ? collapsible : ''}
data-side={side}
data-state={state}
data-variant={variant}
>
{/* This is what handles the sidebar gap on desktop */}
<div
className={cn(
'duration-200 relative h-svh w-[--sidebar-width] bg-transparent transition-[width] ease-linear',
'group-data-[collapsible=offcanvas]:w-0',
'group-data-[side=right]:rotate-180',
variant === 'floating' || variant === 'inset'
? 'group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4))]'
: 'group-data-[collapsible=icon]:w-[--sidebar-width-icon]'
)}
/>
<div
className={cn(
'duration-200 fixed inset-y-0 z-10 hidden h-svh w-[--sidebar-width] transition-[left,right,width] ease-linear md:flex',
side === 'left'
? 'left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]'
: 'right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]',
// Adjust the padding for floating and inset variants.
variant === 'floating' || variant === 'inset'
? 'p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4)_+2px)]'
: 'group-data-[collapsible=icon]:w-[--sidebar-width-icon] group-data-[side=left]:border-r group-data-[side=right]:border-l',
className
)}
{...props}
>
<div
className="flex h-full w-full flex-col bg-sidebar group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:border-sidebar-border group-data-[variant=floating]:shadow"
data-sidebar="sidebar"
>
{children}
</div>
</div>
</div>
);
}
);
Sidebar.displayName = 'Sidebar';
const SidebarTrigger = React.forwardRef<
React.ElementRef<typeof Button>,
React.ComponentProps<typeof Button>
>(({className, onClick, ...props}, ref) => {
const {toggleSidebar} = useSidebar();
return (
<Button
ref={ref}
className={cn('h-7 w-7', className)}
data-sidebar="trigger"
size="icon"
variant="ghost"
onClick={(event) => {
onClick?.(event);
toggleSidebar();
}}
{...props}
>
<PanelLeft />
<span className="sr-only">Toggle Sidebar</span>
</Button>
);
});
SidebarTrigger.displayName = 'SidebarTrigger';
const SidebarRail = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<'button'>
>(({className, ...props}, ref) => {
const {toggleSidebar} = useSidebar();
return (
<button
ref={ref}
aria-label="Toggle Sidebar"
className={cn(
'absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] hover:after:bg-sidebar-border group-data-[side=left]:-right-4 group-data-[side=right]:left-0 sm:flex',
'[[data-side=left]_&]:cursor-w-resize [[data-side=right]_&]:cursor-e-resize',
'[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize',
'group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full group-data-[collapsible=offcanvas]:hover:bg-sidebar',
'[[data-side=left][data-collapsible=offcanvas]_&]:-right-2',
'[[data-side=right][data-collapsible=offcanvas]_&]:-left-2',
className
)}
data-sidebar="rail"
tabIndex={-1}
title="Toggle Sidebar"
type='button'
onClick={toggleSidebar}
{...props}
/>
);
});
SidebarRail.displayName = 'SidebarRail';
const SidebarInset = React.forwardRef<
HTMLDivElement,
React.ComponentProps<'main'>
>(({className, ...props}, ref) => {
return (
<main
ref={ref}
className={cn(
'relative flex min-h-svh flex-1 flex-col bg-background',
'peer-data-[variant=inset]:min-h-[calc(100svh-theme(spacing.4))] md:peer-data-[variant=inset]:m-2 md:peer-data-[state=collapsed]:peer-data-[variant=inset]:ml-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow',
className
)}
{...props}
/>
);
});
SidebarInset.displayName = 'SidebarInset';
const SidebarInput = React.forwardRef<
React.ElementRef<typeof Input>,
React.ComponentProps<typeof Input>
>(({className, ...props}, ref) => {
return (
<Input
ref={ref}
className={cn(
'w-full bg-background shadow-none focus-visible:ring-2 focus-visible:ring-sidebar-ring',
`h-${SIDEBAR_MENU_HEIGHT}`,
className
)}
data-sidebar="input"
{...props}
/>
);
});
SidebarInput.displayName = 'SidebarInput';
const SidebarHeader = React.forwardRef<
HTMLDivElement,
React.ComponentProps<'div'>
>(({className, ...props}, ref) => {
return (
<div
ref={ref}
className={cn('flex flex-col gap-2 p-2', className)}
data-sidebar="header"
{...props}
/>
);
});
SidebarHeader.displayName = 'SidebarHeader';
const SidebarFooter = React.forwardRef<
HTMLDivElement,
React.ComponentProps<'div'>
>(({className, ...props}, ref) => {
return (
<div
ref={ref}
className={cn('flex flex-col gap-2 p-2', className)}
data-sidebar="footer"
{...props}
/>
);
});
SidebarFooter.displayName = 'SidebarFooter';
const SidebarSeparator = React.forwardRef<
React.ElementRef<typeof Separator>,
React.ComponentProps<typeof Separator>
>(({className, ...props}, ref) => {
return (
<Separator
ref={ref}
className={cn('mx-2 w-auto bg-sidebar-border', className)}
data-sidebar="separator"
{...props}
/>
);
});
SidebarSeparator.displayName = 'SidebarSeparator';
const SidebarContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<'div'>
>(({className, ...props}, ref) => {
return (
<div
ref={ref}
className={cn(
'flex min-h-0 flex-1 flex-col gap-5 overflow-auto group-data-[collapsible=icon]:overflow-hidden',
className
)}
data-sidebar="content"
{...props}
/>
);
});
SidebarContent.displayName = 'SidebarContent';
const SidebarGroup = React.forwardRef<
HTMLDivElement,
React.ComponentProps<'div'>
>(({className, ...props}, ref) => {
return (
<div
ref={ref}
className={cn('relative flex w-full min-w-0 flex-col p-2', className)}
data-sidebar="group"
{...props}
/>
);
});
SidebarGroup.displayName = 'SidebarGroup';
const SidebarGroupLabel = React.forwardRef<
HTMLDivElement,
React.ComponentProps<'div'> & { asChild?: boolean }
>(({className, asChild = false, ...props}, ref) => {
const Comp = asChild ? Slot : 'div';
return (
<Comp
ref={ref}
className={cn(
'duration-200 flex shrink-0 items-center rounded-md px-3 text-xs font-medium text-sidebar-foreground/70 outline-none ring-sidebar-ring transition-[margin,opa] ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0',
'group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0',
`h-${SIDEBAR_MENU_HEIGHT}`,
className
)}
data-sidebar="group-label"
{...props}
/>
);
});
SidebarGroupLabel.displayName = 'SidebarGroupLabel';
const SidebarGroupAction = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<'button'> & { asChild?: boolean }
>(({className, asChild = false, ...props}, ref) => {
const Comp = asChild ? Slot : 'button';
return (
<Comp
ref={ref}
className={cn(
'absolute right-3 top-3.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0',
// Increases the hit area of the button on mobile.
'after:absolute after:-inset-2 after:md:hidden',
'group-data-[collapsible=icon]:hidden',
className
)}
data-sidebar="group-action"
{...props}
/>
);
});
SidebarGroupAction.displayName = 'SidebarGroupAction';
const SidebarGroupContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<'div'>
>(({className, ...props}, ref) => (
<div
ref={ref}
className={cn('w-full text-sm', className)}
data-sidebar="group-content"
{...props}
/>
));
SidebarGroupContent.displayName = 'SidebarGroupContent';
const SidebarMenu = React.forwardRef<
HTMLUListElement,
React.ComponentProps<'ul'>
>(({className, ...props}, ref) => (
<ul
ref={ref}
className={cn('flex w-full min-w-0 flex-col gap-px', className)}
data-sidebar="menu"
{...props}
/>
));
SidebarMenu.displayName = 'SidebarMenu';
const SidebarMenuItem = React.forwardRef<
HTMLLIElement,
React.ComponentProps<'li'>
>(({className, ...props}, ref) => (
<li
ref={ref}
className={cn('group/menu-item relative', className)}
data-sidebar="menu-item"
{...props}
/>
));
SidebarMenuItem.displayName = 'SidebarMenuItem';
const sidebarMenuButtonVariants = cva(
'peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md px-3 py-2 text-left text-md font-medium text-gray-800 outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-semibold data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0',
{
variants: {
variant: {
default: 'hover:bg-sidebar-accent hover:text-sidebar-accent-foreground',
outline:
'bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]'
},
size: {
default: `h-${SIDEBAR_MENU_HEIGHT} text-md`,
sm: 'h-7 text-xs',
lg: 'h-12 text-sm group-data-[collapsible=icon]:!p-0'
}
},
defaultVariants: {
variant: 'default',
size: 'default'
}
}
);
const SidebarMenuButton = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<'button'> & {
asChild?: boolean
isActive?: boolean
tooltip?: string | React.ComponentProps<typeof TooltipContent>
} & VariantProps<typeof sidebarMenuButtonVariants>
>(
(
{
asChild = false,
isActive = false,
variant = 'default',
size = 'default',
tooltip,
className,
...props
},
ref
) => {
const Comp = asChild ? Slot : 'button';
const {isMobile, state} = useSidebar();
const button = (
<Comp
ref={ref}
className={cn(sidebarMenuButtonVariants({variant, size}), className)}
data-active={isActive}
data-sidebar="menu-button"
data-size={size}
{...props}
/>
);
if (!tooltip) {
return button;
}
if (typeof tooltip === 'string') {
tooltip = {
children: tooltip
};
}
return (
<Tooltip>
<TooltipTrigger asChild>{button}</TooltipTrigger>
<TooltipContent
align="center"
hidden={state !== 'collapsed' || isMobile}
side="right"
{...tooltip}
/>
</Tooltip>
);
}
);
SidebarMenuButton.displayName = 'SidebarMenuButton';
const SidebarMenuAction = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<'button'> & {
asChild?: boolean
showOnHover?: boolean
}
>(({className, asChild = false, showOnHover = false, ...props}, ref) => {
const Comp = asChild ? Slot : 'button';
return (
<Comp
ref={ref}
className={cn(
'absolute right-1 top-1.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 peer-hover/menu-button:text-sidebar-accent-foreground [&>svg]:size-4 [&>svg]:shrink-0',
// Increases the hit area of the button on mobile.
'after:absolute after:-inset-2 after:md:hidden',
'peer-data-[size=sm]/menu-button:top-1',
'peer-data-[size=default]/menu-button:top-1.5',
'peer-data-[size=lg]/menu-button:top-2.5',
'group-data-[collapsible=icon]:hidden',
showOnHover &&
'group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0',
className
)}
data-sidebar="menu-action"
{...props}
/>
);
});
SidebarMenuAction.displayName = 'SidebarMenuAction';
const SidebarMenuBadge = React.forwardRef<
HTMLDivElement,
React.ComponentProps<'div'>
>(({className, ...props}, ref) => (
<div
ref={ref}
className={cn(
'absolute right-2 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-sm font-medium tabular-nums text-gray-600 select-none pointer-events-none',
'peer-hover/menu-button: peer-data-[active=true]/menu-button:text-sidebar-accent-foreground',
'peer-data-[size=sm]/menu-button:top-1.5',
'peer-data-[size=default]/menu-button:top-2',
'peer-data-[size=lg]/menu-button:top-3',
'group-data-[collapsible=icon]:hidden',
className
)}
data-sidebar="menu-badge"
{...props}
/>
));
SidebarMenuBadge.displayName = 'SidebarMenuBadge';
const SidebarMenuSkeleton = React.forwardRef<
HTMLDivElement,
React.ComponentProps<'div'> & {
showIcon?: boolean
}
>(({className, showIcon = false, ...props}, ref) => {
// Random width between 50 to 90%.
const width = React.useMemo(() => {
return `${Math.floor(Math.random() * 40) + 50}%`;
}, []);
return (
<div
ref={ref}
className={cn('rounded-md flex gap-2 px-3 items-center', `h-${SIDEBAR_MENU_HEIGHT}`, className)}
data-sidebar="menu-skeleton"
{...props}
>
{showIcon && (
<Skeleton
className="size-4 rounded-md"
data-sidebar="menu-skeleton-icon"
/>
)}
<Skeleton
className="h-4 max-w-[--skeleton-width] flex-1"
data-sidebar="menu-skeleton-text"
style={
{
'--skeleton-width': width
} as React.CSSProperties
}
/>
</div>
);
});
SidebarMenuSkeleton.displayName = 'SidebarMenuSkeleton';
const SidebarMenuSub = React.forwardRef<
HTMLUListElement,
React.ComponentProps<'ul'>
>(({className, ...props}, ref) => (
<ul
ref={ref}
className={cn(
'mx-3.5 flex min-w-0 translate-x-px flex-col gap-px border-l border-sidebar-border px-3 py-0.5',
'group-data-[collapsible=icon]:hidden',
className
)}
data-sidebar="menu-sub"
{...props}
/>
));
SidebarMenuSub.displayName = 'SidebarMenuSub';
const SidebarMenuSubItem = React.forwardRef<
HTMLLIElement,
React.ComponentProps<'li'>
>(({...props}, ref) => <li ref={ref} {...props} />);
SidebarMenuSubItem.displayName = 'SidebarMenuSubItem';
const SidebarMenuSubButton = React.forwardRef<
HTMLAnchorElement,
React.ComponentProps<'a'> & {
asChild?: boolean
size?: 'sm' | 'md'
isActive?: boolean
}
>(({asChild = false, size = 'md', isActive, className, ...props}, ref) => {
const Comp = asChild ? Slot : 'a';
return (
<Comp
ref={ref}
className={cn(
'flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-3 text-sidebar-foreground outline-none ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground',
'data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground',
size === 'sm' && 'text-xs',
size === 'md' && 'text-sm',
'group-data-[collapsible=icon]:hidden',
className
)}
data-active={isActive}
data-sidebar="menu-sub-button"
data-size={size}
{...props}
/>
);
});
SidebarMenuSubButton.displayName = 'SidebarMenuSubButton';
export {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupAction,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarInput,
SidebarInset,
SidebarMenu,
SidebarMenuAction,
SidebarMenuBadge,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSkeleton,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
SidebarProvider,
SidebarRail,
SidebarSeparator,
SidebarTrigger,
useSidebar
};

View file

@ -0,0 +1,15 @@
import {cn} from '@/lib/utils';
function Skeleton({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn('animate-pulse rounded-md bg-primary/10', className)}
{...props}
/>
);
}
export {Skeleton};

View file

@ -0,0 +1,120 @@
import * as React from 'react';
import {cn} from '@/lib/utils';
const Table = React.forwardRef<
HTMLTableElement,
React.HTMLAttributes<HTMLTableElement>
>(({className, ...props}, ref) => (
<div className="relative w-full">
<table
ref={ref}
className={cn('w-full caption-bottom text-sm', className)}
{...props}
/>
</div>
));
Table.displayName = 'Table';
const TableHeader = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({className, ...props}, ref) => (
<thead ref={ref} className={cn('[&_tr]:border-transparent [&_tr:hover:before]:bg-transparent', className)} {...props} />
));
TableHeader.displayName = 'TableHeader';
const TableBody = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({className, ...props}, ref) => (
<tbody
ref={ref}
className={cn('', className)}
{...props}
/>
));
TableBody.displayName = 'TableBody';
const TableFooter = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({className, ...props}, ref) => (
<tfoot
ref={ref}
className={cn(
'border-t bg-muted/50 font-medium [&>tr]:last:border-b-0',
className
)}
{...props}
/>
));
TableFooter.displayName = 'TableFooter';
const TableRow = React.forwardRef<
HTMLTableRowElement,
React.HTMLAttributes<HTMLTableRowElement>
>(({className, ...props}, ref) => (
<tr
ref={ref}
className={cn(
'relative border-t data-[state=selected]:bg-muted before:absolute before:content-[""] before:-inset-y-px before:-inset-x-2 before:rounded-md hover:border-transparent hover:before:bg-muted/50 [:hover_+_&]:border-transparent',
className
)}
{...props}
/>
));
TableRow.displayName = 'TableRow';
const TableHead = React.forwardRef<
HTMLTableCellElement,
React.ThHTMLAttributes<HTMLTableCellElement>
>(({className, ...props}, ref) => (
<th
ref={ref}
className={cn(
'relative h-10 px-2 first-of-type:pl-0 last-of-type:pr-0 text-left align-middle font-medium text-black [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
className
)}
{...props}
/>
));
TableHead.displayName = 'TableHead';
const TableCell = React.forwardRef<
HTMLTableCellElement,
React.TdHTMLAttributes<HTMLTableCellElement>
>(({className, ...props}, ref) => (
<td
ref={ref}
className={cn(
'relative p-2 first-of-type:pl-0 last-of-type:pr-0 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
className
)}
{...props}
/>
));
TableCell.displayName = 'TableCell';
const TableCaption = React.forwardRef<
HTMLTableCaptionElement,
React.HTMLAttributes<HTMLTableCaptionElement>
>(({className, ...props}, ref) => (
<caption
ref={ref}
className={cn('mt-4 text-sm text-muted-foreground', className)}
{...props}
/>
));
TableCaption.displayName = 'TableCaption';
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption
};

View file

@ -4,7 +4,7 @@ import * as TabsPrimitive from '@radix-ui/react-tabs';
import {cn} from '@/lib/utils';
import {cva} from 'class-variance-authority';
type TabsVariant = 'segmented' | 'page';
type TabsVariant = 'segmented' | 'button' | 'underline';
const TabsVariantContext = React.createContext<TabsVariant>('segmented');
@ -18,7 +18,8 @@ const tabsVariants = cva(
variants: {
variant: {
segmented: '',
page: ''
button: '',
underline: ''
}
},
defaultVariants: {
@ -38,12 +39,13 @@ const Tabs = React.forwardRef<
Tabs.displayName = TabsPrimitive.Root.displayName;
const tabsListVariants = cva(
'inline-flex items-center justify-center text-muted-foreground',
'inline-flex items-center text-muted-foreground',
{
variants: {
variant: {
segmented: 'h-[34px] rounded-lg bg-muted px-[3px]',
page: 'gap-2 border-b pb-2'
button: 'gap-2',
underline: 'gap-7 border-b pb-1'
}
},
defaultVariants: {
@ -68,12 +70,13 @@ const TabsList = React.forwardRef<
TabsList.displayName = TabsPrimitive.List.displayName;
const tabsTriggerVariants = cva(
'inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground',
'inline-flex items-center justify-center whitespace-nowrap px-3 py-1 ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground [&_svg]:size-4 [&_svg]:shrink-0',
{
variants: {
variant: {
segmented: 'h-7 data-[state=active]:shadow-md',
page: 'h-[34px] border data-[state=active]:bg-muted/70'
segmented: 'h-7 rounded-md text-sm font-medium data-[state=active]:shadow-md',
button: 'h-[34px] gap-1.5 rounded-md border border-input py-2 text-sm font-medium hover:bg-muted/50 data-[state=active]:bg-muted/70 data-[state=active]:font-semibold',
underline: 'relative h-[34px] px-0 text-md font-medium text-gray-700 data-[state=active]:font-semibold data-[state=active]:text-black data-[state=active]:after:absolute data-[state=active]:after:inset-x-0 data-[state=active]:after:bottom-[-5px] data-[state=active]:after:h-px data-[state=active]:after:bg-black data-[state=active]:after:content-[""]'
}
},
defaultVariants: {
@ -98,12 +101,13 @@ const TabsTrigger = React.forwardRef<
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
const tabsContentVariants = cva(
'mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
'ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
{
variants: {
variant: {
segmented: '',
page: ''
button: '',
underline: ''
}
},
defaultVariants: {

View file

@ -0,0 +1,30 @@
import * as React from 'react';
import * as TooltipPrimitive from '@radix-ui/react-tooltip';
import {cn} from '@/lib/utils';
const TooltipProvider = TooltipPrimitive.Provider;
const Tooltip = TooltipPrimitive.Root;
const TooltipTrigger = TooltipPrimitive.Trigger;
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({className, sideOffset = 4, ...props}, ref) => (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
ref={ref}
className={cn(
'z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
className
)}
sideOffset={sideOffset}
{...props}
/>
</TooltipPrimitive.Portal>
));
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
export {Tooltip, TooltipTrigger, TooltipContent, TooltipProvider};

View file

@ -0,0 +1,19 @@
import * as React from 'react';
const MOBILE_BREAKPOINT = 768;
export function useIsMobile() {
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined);
React.useEffect(() => {
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
const onChange = () => {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
};
mql.addEventListener('change', onChange);
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
return () => mql.removeEventListener('change', onChange);
}, []);
return !!isMobile;
}

View file

@ -1,14 +1,31 @@
// UI components
export * from './components/ui/button';
export {IconComponents as Icon} from './components/ui/icon';
export * from './components/ui/avatar';
export * from './components/ui/badge';
export * from './components/ui/breadcrumb';
export * from './components/ui/dropdown-menu';
export * from './components/ui/button';
export * from './components/ui/card';
export * from './components/ui/chart';
export * from './components/ui/dropdown-menu';
export * from './components/ui/input';
export * from './components/ui/separator';
export * from './components/ui/sheet';
export * from './components/ui/sidebar';
export * from './components/ui/skeleton';
export * from './components/ui/table';
export * from './components/ui/tabs';
// export {Tooltip as ShadeTooltip, TooltipTrigger, TooltipContent, TooltipProvider} from './components/ui/tooltip';
export * from './components/ui/tooltip';
// Layout components
export * from './components/layout/page';
export * from './components/layout/heading';
// Third party components
export * as Recharts from 'recharts';
export * as LucideIcon from 'lucide-react';
export {IconComponents as Icon} from './components/ui/icon';
// Assets
export {ReactComponent as FacebookLogo} from './assets/images/facebook-logo.svg';
export {ReactComponent as GhostLogo} from './assets/images/ghost-logo.svg';
@ -21,6 +38,7 @@ export {default as useGlobalDirtyState} from './hooks/useGlobalDirtyState';
// Utils
export * from '@/lib/utils';
export {cn} from '@/lib/utils';
export {debounce} from './utils/debounce';
export {formatUrl} from './utils/formatUrl';

View file

@ -30,25 +30,38 @@
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--foreground: 216 11% 9%;
--muted: 200 12% 96%;
--muted-foreground: 211 11% 43%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 47.4% 11.2%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--popover-foreground: 216 11% 9%;
--border: 204 14% 93%;
--input: 204 14% 93%;
--card: 0 0% 100%;
--card-foreground: 222.2 47.4% 11.2%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 100% 50%;
--destructive-foreground: 210 40% 98%;
--ring: 215 20.2% 65.1%;
--card-foreground: 216 11% 9%;;
--primary: 216 11% 9%;;
--primary-foreground: 0 0% 100%;
--secondary: 204 14% 93%;
--secondary-foreground: 216 11% 9%;
--accent: 200 12% 96%;
--accent-foreground: 216 11% 9%;
--destructive: 354 92% 50%;
--destructive-foreground: 0 0% 100%;
--ring: 215 13% 63%;
--radius: 9px;
--chart-1: 221.2 83.2% 53.3%;
--chart-2: 212 95% 68%;
--chart-3: 216 92% 60%;
--chart-4: 210 98% 78%;
--chart-5: 212 97% 87%;
--sidebar-background: 0 0% 100%;
--sidebar-foreground: 216 11% 9%;
--sidebar-primary: 216 11% 9%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 200 12% 96%;
--sidebar-accent-foreground: 216 11% 9%;
--sidebar-border: 210 13% 88%;
--sidebar-ring: 215 13% 63%;
}
.dark {
@ -71,6 +84,19 @@
--destructive: 0 63% 31%;
--destructive-foreground: 210 40% 98%;
--ring: 216 34% 17%;
--chart-1: 221.2 83.2% 53.3%;
--chart-2: 212 95% 68%;
--chart-3: 216 92% 60%;
--chart-4: 210 98% 78%;
--chart-5: 212 97% 87%;
--sidebar-background: 240 5.9% 10%;
--sidebar-foreground: 240 4.8% 95.9%;
--sidebar-primary: 224.3 76.3% 48%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 240 3.7% 15.9%;
--sidebar-accent-foreground: 240 4.8% 95.9%;
--sidebar-border: 240 3.7% 15.9%;
--sidebar-ring: 217.2 91.2% 59.8%;
}
/* This just serves as a placeholder; we actually load Inter from a font file in Ember admin */
@ -144,3 +170,11 @@
.gh-prose-links a {
color: #30cf43;
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}

View file

@ -115,6 +115,15 @@ module.exports = {
'fira-mono': 'Fira Mono',
'jetbrains-mono': 'JetBrains Mono'
},
letterSpacing: {
tightest: '-.05em',
tighter: '-.025em',
tight: '-.01em',
normal: '0',
wide: '.01em',
wider: '.025em',
widest: '.5em'
},
boxShadow: {
DEFAULT: '0 0 1px rgba(0,0,0,.05), 0 5px 18px rgba(0,0,0,.08)',
xs: '0 0 1px rgba(0,0,0,0.04), 0 1px 3px rgba(0,0,0,0.03), 0 8px 10px -12px rgba(0,0,0,.1)',
@ -288,7 +297,7 @@ module.exports = {
max: 'max-content',
fit: 'fit-content',
prose: '65ch',
page: '128rem'
page: '148rem'
},
borderRadius: {
sm: 'calc(var(--radius) - 4px)',
@ -306,16 +315,31 @@ module.exports = {
xs: '1.2rem',
sm: '1.3rem',
md: '1.4rem',
lg: '1.65rem',
xl: '2rem',
'2xl': '2.4rem',
'3xl': '3.2rem',
'4xl': '3.6rem',
'5xl': ['4.2rem', '1.15'],
'6xl': ['6rem', '1'],
'7xl': ['7.2rem', '1'],
'8xl': ['9.6rem', '1'],
'9xl': ['12.8rem', '1'],
lg: '1.5rem',
xl: '1.7rem',
'2xl': '2.2rem',
'3xl': '2.7rem',
'4xl': '3.2rem',
'5xl': [
'4.0rem',
'1.15'
],
'6xl': [
'5.8rem',
'1'
],
'7xl': [
'7.0rem',
'1'
],
'8xl': [
'9.6rem',
'1'
],
'9xl': [
'12.8rem',
'1'
],
inherit: 'inherit'
},
lineHeight: {
@ -364,6 +388,16 @@ module.exports = {
3: 'hsl(var(--chart-3))',
4: 'hsl(var(--chart-4))',
5: 'hsl(var(--chart-5))'
},
sidebar: {
DEFAULT: 'hsl(var(--sidebar-background))',
foreground: 'hsl(var(--sidebar-foreground))',
primary: 'hsl(var(--sidebar-primary))',
'primary-foreground': 'hsl(var(--sidebar-primary-foreground))',
accent: 'hsl(var(--sidebar-accent))',
'accent-foreground': 'hsl(var(--sidebar-accent-foreground))',
border: 'hsl(var(--sidebar-border))',
ring: 'hsl(var(--sidebar-ring))'
}
}
}

View file

@ -1,10 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<defs>
<style>
circle{fill:currentColor}
.dotdotdotfill {fill:currentColor}
</style>
</defs>
<circle cx="3.25" cy="12" r="2.6"/>
<circle cx="12" cy="12" r="2.6"/>
<circle cx="20.75" cy="12" r="2.6"/>
</svg>
<circle class="dotdotdotfill" cx="3.25" cy="12" r="2.6"/>
<circle class="dotdotdotfill" cx="12" cy="12" r="2.6"/>
<circle class="dotdotdotfill" cx="20.75" cy="12" r="2.6"/>
</svg>

Before

Width:  |  Height:  |  Size: 256 B

After

Width:  |  Height:  |  Size: 332 B

307
yarn.lock
View file

@ -4351,6 +4351,26 @@
resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-1.1.1.tgz#82074aa83a472353bb22e86f11bcbd1c61c4c71a"
integrity sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==
"@radix-ui/react-dialog@^1.1.4":
version "1.1.4"
resolved "https://registry.yarnpkg.com/@radix-ui/react-dialog/-/react-dialog-1.1.4.tgz#d68e977acfcc0d044b9dab47b6dd2c179d2b3191"
integrity sha512-Ur7EV1IwQGCyaAuyDRiOLA5JIUZxELJljF+MbM/2NC0BYwfuRrbpS30BiQBJrVruscgUkieKkqXYDOoByaxIoA==
dependencies:
"@radix-ui/primitive" "1.1.1"
"@radix-ui/react-compose-refs" "1.1.1"
"@radix-ui/react-context" "1.1.1"
"@radix-ui/react-dismissable-layer" "1.1.3"
"@radix-ui/react-focus-guards" "1.1.1"
"@radix-ui/react-focus-scope" "1.1.1"
"@radix-ui/react-id" "1.1.0"
"@radix-ui/react-portal" "1.1.3"
"@radix-ui/react-presence" "1.1.2"
"@radix-ui/react-primitive" "2.0.1"
"@radix-ui/react-slot" "1.1.1"
"@radix-ui/react-use-controllable-state" "1.1.0"
aria-hidden "^1.1.1"
react-remove-scroll "^2.6.1"
"@radix-ui/react-direction@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-direction/-/react-direction-1.0.1.tgz#9cb61bf2ccf568f3421422d182637b7f47596c9b"
@ -4397,6 +4417,17 @@
"@radix-ui/react-use-callback-ref" "1.1.0"
"@radix-ui/react-use-escape-keydown" "1.1.0"
"@radix-ui/react-dismissable-layer@1.1.3":
version "1.1.3"
resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.3.tgz#4ee0f0f82d53bf5bd9db21665799bb0d1bad5ed8"
integrity sha512-onrWn/72lQoEucDmJnr8uczSNTujT0vJnA/X5+3AkChVPowr8n1yvIKIabhWyMQeMvvmdpsvcyDqx3X1LEXCPg==
dependencies:
"@radix-ui/primitive" "1.1.1"
"@radix-ui/react-compose-refs" "1.1.1"
"@radix-ui/react-primitive" "2.0.1"
"@radix-ui/react-use-callback-ref" "1.1.0"
"@radix-ui/react-use-escape-keydown" "1.1.0"
"@radix-ui/react-dropdown-menu@2.1.3":
version "2.1.3"
resolved "https://registry.yarnpkg.com/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.3.tgz#02665f99bfdcefc33a8a15dc130e9b98ebdf7671"
@ -4752,6 +4783,13 @@
dependencies:
"@radix-ui/react-primitive" "2.0.0"
"@radix-ui/react-separator@^1.1.1":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-separator/-/react-separator-1.1.1.tgz#dd60621553c858238d876be9b0702287424866d2"
integrity sha512-RRiNRSrD8iUiXriq/Y5n4/3iE8HzqgLHsusUSg5jVpU2+3tqcUFPJXHDymwEypunc2sWxDUS3UC+rkZRlHedsw==
dependencies:
"@radix-ui/react-primitive" "2.0.1"
"@radix-ui/react-slot@1.0.2":
version "1.0.2"
resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.0.2.tgz#a9ff4423eade67f501ffb32ec22064bc9d3099ab"
@ -4767,7 +4805,7 @@
dependencies:
"@radix-ui/react-compose-refs" "1.1.0"
"@radix-ui/react-slot@1.1.1":
"@radix-ui/react-slot@1.1.1", "@radix-ui/react-slot@^1.1.1":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.1.1.tgz#ab9a0ffae4027db7dc2af503c223c978706affc3"
integrity sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==
@ -4871,6 +4909,24 @@
"@radix-ui/react-use-controllable-state" "1.1.0"
"@radix-ui/react-visually-hidden" "1.1.0"
"@radix-ui/react-tooltip@^1.1.6":
version "1.1.6"
resolved "https://registry.yarnpkg.com/@radix-ui/react-tooltip/-/react-tooltip-1.1.6.tgz#eab98e9a5c876ef0abfae3cfeee229870528ed06"
integrity sha512-TLB5D8QLExS1uDn7+wH/bjEmRurNMTzNrtq7IjaS4kjion9NtzsTGkvR5+i7yc9q01Pi2KMM2cN3f8UG4IvvXA==
dependencies:
"@radix-ui/primitive" "1.1.1"
"@radix-ui/react-compose-refs" "1.1.1"
"@radix-ui/react-context" "1.1.1"
"@radix-ui/react-dismissable-layer" "1.1.3"
"@radix-ui/react-id" "1.1.0"
"@radix-ui/react-popper" "1.2.1"
"@radix-ui/react-portal" "1.1.3"
"@radix-ui/react-presence" "1.1.2"
"@radix-ui/react-primitive" "2.0.1"
"@radix-ui/react-slot" "1.1.1"
"@radix-ui/react-use-controllable-state" "1.1.0"
"@radix-ui/react-visually-hidden" "1.1.1"
"@radix-ui/react-use-callback-ref@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz#f4bb1f27f2023c984e6534317ebc411fc181107a"
@ -4982,6 +5038,13 @@
dependencies:
"@radix-ui/react-primitive" "2.0.0"
"@radix-ui/react-visually-hidden@1.1.1":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.1.tgz#f7b48c1af50dfdc366e92726aee6d591996c5752"
integrity sha512-vVfA2IZ9q/J+gEamvj761Oq1FpWgCDaNOOIfbPVp2MVPLEomUr5+Vf7kJGwQ24YxZSlQVar7Bes8kyTo5Dshpg==
dependencies:
"@radix-ui/react-primitive" "2.0.1"
"@radix-ui/rect@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@radix-ui/rect/-/rect-1.0.1.tgz#bf8e7d947671996da2e30f4904ece343bc4a883f"
@ -8240,6 +8303,57 @@
dependencies:
"@types/node" "*"
"@types/d3-array@^3.0.3":
version "3.2.1"
resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-3.2.1.tgz#1f6658e3d2006c4fceac53fde464166859f8b8c5"
integrity sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==
"@types/d3-color@*":
version "3.1.3"
resolved "https://registry.yarnpkg.com/@types/d3-color/-/d3-color-3.1.3.tgz#368c961a18de721da8200e80bf3943fb53136af2"
integrity sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==
"@types/d3-ease@^3.0.0":
version "3.0.2"
resolved "https://registry.yarnpkg.com/@types/d3-ease/-/d3-ease-3.0.2.tgz#e28db1bfbfa617076f7770dd1d9a48eaa3b6c51b"
integrity sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==
"@types/d3-interpolate@^3.0.1":
version "3.0.4"
resolved "https://registry.yarnpkg.com/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz#412b90e84870285f2ff8a846c6eb60344f12a41c"
integrity sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==
dependencies:
"@types/d3-color" "*"
"@types/d3-path@*":
version "3.1.0"
resolved "https://registry.yarnpkg.com/@types/d3-path/-/d3-path-3.1.0.tgz#2b907adce762a78e98828f0b438eaca339ae410a"
integrity sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ==
"@types/d3-scale@^4.0.2":
version "4.0.8"
resolved "https://registry.yarnpkg.com/@types/d3-scale/-/d3-scale-4.0.8.tgz#d409b5f9dcf63074464bf8ddfb8ee5a1f95945bb"
integrity sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==
dependencies:
"@types/d3-time" "*"
"@types/d3-shape@^3.1.0":
version "3.1.7"
resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-3.1.7.tgz#2b7b423dc2dfe69c8c93596e673e37443348c555"
integrity sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==
dependencies:
"@types/d3-path" "*"
"@types/d3-time@*", "@types/d3-time@^3.0.0":
version "3.0.4"
resolved "https://registry.yarnpkg.com/@types/d3-time/-/d3-time-3.0.4.tgz#8472feecd639691450dd8000eb33edd444e1323f"
integrity sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==
"@types/d3-timer@^3.0.0":
version "3.0.2"
resolved "https://registry.yarnpkg.com/@types/d3-timer/-/d3-timer-3.0.2.tgz#70bbda77dc23aa727413e22e214afa3f0e852f70"
integrity sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==
"@types/detect-port@^1.3.0":
version "1.3.2"
resolved "https://registry.yarnpkg.com/@types/detect-port/-/detect-port-1.3.2.tgz#8c06a975e472803b931ee73740aeebd0a2eb27ae"
@ -12795,7 +12909,7 @@ class-utils@^0.3.5:
isobject "^3.0.0"
static-extend "^0.1.1"
class-variance-authority@0.7.1:
class-variance-authority@^0.7.1:
version "0.7.1"
resolved "https://registry.yarnpkg.com/class-variance-authority/-/class-variance-authority-0.7.1.tgz#4008a798a0e4553a781a57ac5177c9fb5d043787"
integrity sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==
@ -12986,7 +13100,7 @@ clone@~0.1.9:
resolved "https://registry.yarnpkg.com/clone/-/clone-0.1.19.tgz#613fb68639b26a494ac53253e15b1a6bd88ada85"
integrity sha512-IO78I0y6JcSpEPHzK4obKdsL7E7oLdRVDVOLwr2Hkbjsb+Eoz0dxW6tef0WizoKu0gLC4oZSZuEF4U2K6w1WQw==
clsx@2.1.1, clsx@^2.1.1:
clsx@2.1.1, clsx@^2.0.0, clsx@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999"
integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==
@ -14057,6 +14171,77 @@ cyclist@^1.0.1:
resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9"
integrity sha512-NJGVKPS81XejHcLhaLJS7plab0fK3slPh11mESeeDq2W4ZI5kUKK/LRRdVDvjJseojbPB7ZwjnyOybg3Igea/A==
"d3-array@2 - 3", "d3-array@2.10.0 - 3", d3-array@^3.1.6:
version "3.2.4"
resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-3.2.4.tgz#15fec33b237f97ac5d7c986dc77da273a8ed0bb5"
integrity sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==
dependencies:
internmap "1 - 2"
"d3-color@1 - 3":
version "3.1.0"
resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-3.1.0.tgz#395b2833dfac71507f12ac2f7af23bf819de24e2"
integrity sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==
d3-ease@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-3.0.1.tgz#9658ac38a2140d59d346160f1f6c30fda0bd12f4"
integrity sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==
"d3-format@1 - 3":
version "3.1.0"
resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-3.1.0.tgz#9260e23a28ea5cb109e93b21a06e24e2ebd55641"
integrity sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==
"d3-interpolate@1.2.0 - 3", d3-interpolate@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz#3c47aa5b32c5b3dfb56ef3fd4342078a632b400d"
integrity sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==
dependencies:
d3-color "1 - 3"
d3-path@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-3.1.0.tgz#22df939032fb5a71ae8b1800d61ddb7851c42526"
integrity sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==
d3-scale@^4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-4.0.2.tgz#82b38e8e8ff7080764f8dcec77bd4be393689396"
integrity sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==
dependencies:
d3-array "2.10.0 - 3"
d3-format "1 - 3"
d3-interpolate "1.2.0 - 3"
d3-time "2.1.1 - 3"
d3-time-format "2 - 4"
d3-shape@^3.1.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-3.2.0.tgz#a1a839cbd9ba45f28674c69d7f855bcf91dfc6a5"
integrity sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==
dependencies:
d3-path "^3.1.0"
"d3-time-format@2 - 4":
version "4.1.0"
resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-4.1.0.tgz#7ab5257a5041d11ecb4fe70a5c7d16a195bb408a"
integrity sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==
dependencies:
d3-time "1 - 3"
"d3-time@1 - 3", "d3-time@2.1.1 - 3", d3-time@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-3.1.0.tgz#9310db56e992e3c0175e1ef385e545e48a9bb5c7"
integrity sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==
dependencies:
d3-array "2 - 3"
d3-timer@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-3.0.1.tgz#6284d2a2708285b1abb7e201eda4380af35e63b0"
integrity sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==
dag-map@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/dag-map/-/dag-map-2.0.2.tgz#9714b472de82a1843de2fba9b6876938cab44c68"
@ -14187,6 +14372,11 @@ decamelize@^5.0.0:
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-5.0.1.tgz#db11a92e58c741ef339fb0a2868d8a06a9a7b1e9"
integrity sha512-VfxadyCECXgQlkoEAjeghAr5gY3Hf+IKjKb+X8tGVDtveCjN+USwprd2q3QXBR9T1+x2DG0XZF5/w+7HAtSaXA==
decimal.js-light@^2.4.1:
version "2.5.1"
resolved "https://registry.yarnpkg.com/decimal.js-light/-/decimal.js-light-2.5.1.tgz#134fd32508f19e208f4fb2f8dac0d2626a867934"
integrity sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==
decimal.js@^10.2.1, decimal.js@^10.4.3:
version "10.4.3"
resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.4.3.tgz#1044092884d245d1b7f65725fa4ad4c6f781cc23"
@ -17185,7 +17375,7 @@ etag@~1.8.1:
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==
eventemitter3@^4.0.0:
eventemitter3@^4.0.0, eventemitter3@^4.0.1:
version "4.0.7"
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f"
integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==
@ -17621,6 +17811,11 @@ fast-deep-equal@^3, fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
fast-equals@^5.0.1:
version "5.2.2"
resolved "https://registry.yarnpkg.com/fast-equals/-/fast-equals-5.2.2.tgz#885d7bfb079fac0ce0e8450374bce29e9b742484"
integrity sha512-V7/RktU11J3I36Nwq2JnZEM7tNm17eBJz+u25qdxBZeCKiX6BkVSZQjwWIr+IobgnZy+ag73tTZgZi7tr0LrBw==
fast-fifo@^1.1.0, fast-fifo@^1.2.0:
version "1.3.2"
resolved "https://registry.yarnpkg.com/fast-fifo/-/fast-fifo-1.3.2.tgz#286e31de96eb96d38a97899815740ba2a4f3640c"
@ -19978,6 +20173,11 @@ internal-slot@^1.0.3, internal-slot@^1.0.4, internal-slot@^1.0.5:
has "^1.0.3"
side-channel "^1.0.4"
"internmap@1 - 2":
version "2.0.3"
resolved "https://registry.yarnpkg.com/internmap/-/internmap-2.0.3.tgz#6685f23755e43c524e251d29cbc97248e3061009"
integrity sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==
interpret@^2.0.0, interpret@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/interpret/-/interpret-2.2.0.tgz#1a78a0b5965c40a5416d007ad6f50ad27c417df9"
@ -22791,10 +22991,10 @@ ltgt@^2.1.2:
resolved "https://registry.yarnpkg.com/ltgt/-/ltgt-2.2.1.tgz#f35ca91c493f7b73da0e07495304f17b31f87ee5"
integrity sha512-AI2r85+4MquTw9ZYqabu4nMwy9Oftlfa/e/52t9IjtfG+mGBbTNdAoZ3RQKLHR6r0wQnwZnPIEh/Ya6XTWAKNA==
lucide-react@0.468.0:
version "0.468.0"
resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.468.0.tgz#830c1bfd905575ddd23b832baa420c87db166910"
integrity sha512-6koYRhnM2N0GGZIdXzSeiNwguv1gt/FAjZOiPl76roBi3xKEXa4WmfpxgQwTTL4KipXjefrnf3oV4IsYhi4JFA==
lucide-react@^0.471.1:
version "0.471.1"
resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.471.1.tgz#16f29cd65c7c847eceab0bbf8592443104423249"
integrity sha512-syOxwPhf62gg2YOsz72HRn+CIpeudFy67AeKnSR8Hn/fIIF4ubhNbRF+pQ2CaJrl+X9Os4PL87z2DXQi3DVeDA==
luxon@3.5.0, luxon@^3.5.0:
version "3.5.0"
@ -27310,6 +27510,11 @@ react-is@^18.0.0:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b"
integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==
react-is@^18.3.1:
version "18.3.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e"
integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==
react-refresh@^0.14.0:
version "0.14.0"
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.14.0.tgz#4e02825378a5f227079554d4284889354e5f553e"
@ -27323,6 +27528,14 @@ react-remove-scroll-bar@^2.3.3, react-remove-scroll-bar@^2.3.4, react-remove-scr
react-style-singleton "^2.2.1"
tslib "^2.0.0"
react-remove-scroll-bar@^2.3.7:
version "2.3.8"
resolved "https://registry.yarnpkg.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz#99c20f908ee467b385b68a3469b4a3e750012223"
integrity sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==
dependencies:
react-style-singleton "^2.2.2"
tslib "^2.0.0"
react-remove-scroll@2.5.5:
version "2.5.5"
resolved "https://registry.yarnpkg.com/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz#1e31a1260df08887a8a0e46d09271b52b3a37e77"
@ -27356,6 +27569,17 @@ react-remove-scroll@2.6.0:
use-callback-ref "^1.3.0"
use-sidecar "^1.1.2"
react-remove-scroll@^2.6.1:
version "2.6.2"
resolved "https://registry.yarnpkg.com/react-remove-scroll/-/react-remove-scroll-2.6.2.tgz#2518d2c5112e71ea8928f1082a58459b5c7a2a97"
integrity sha512-KmONPx5fnlXYJQqC62Q+lwIeAk64ws/cUw6omIumRzMRPqgnYqhSSti99nbj0Ry13bv7dF+BKn7NB+OqkdZGTw==
dependencies:
react-remove-scroll-bar "^2.3.7"
react-style-singleton "^2.2.1"
tslib "^2.1.0"
use-callback-ref "^1.3.3"
use-sidecar "^1.1.2"
react-select@5.8.2:
version "5.8.2"
resolved "https://registry.yarnpkg.com/react-select/-/react-select-5.8.2.tgz#0d7ccb1895d61aafcd090fbf65aa9e506225a854"
@ -27371,6 +27595,15 @@ react-select@5.8.2:
react-transition-group "^4.3.0"
use-isomorphic-layout-effect "^1.1.2"
react-smooth@^4.0.0:
version "4.0.4"
resolved "https://registry.yarnpkg.com/react-smooth/-/react-smooth-4.0.4.tgz#a5875f8bb61963ca61b819cedc569dc2453894b4"
integrity sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==
dependencies:
fast-equals "^5.0.1"
prop-types "^15.8.1"
react-transition-group "^4.4.5"
react-string-replace@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/react-string-replace/-/react-string-replace-1.1.1.tgz#8413a598c60e397fe77df3464f2889f00ba25989"
@ -27385,7 +27618,15 @@ react-style-singleton@^2.2.1:
invariant "^2.2.4"
tslib "^2.0.0"
react-transition-group@^4.3.0:
react-style-singleton@^2.2.2:
version "2.2.3"
resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.2.3.tgz#4265608be69a4d70cfe3047f2c6c88b2c3ace388"
integrity sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==
dependencies:
get-nonce "^1.0.0"
tslib "^2.0.0"
react-transition-group@^4.3.0, react-transition-group@^4.4.5:
version "4.4.5"
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.5.tgz#e53d4e3f3344da8521489fbef8f2581d42becdd1"
integrity sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==
@ -27548,6 +27789,27 @@ recast@^0.23.1, recast@^0.23.3:
source-map "~0.6.1"
tslib "^2.0.1"
recharts-scale@^0.4.4:
version "0.4.5"
resolved "https://registry.yarnpkg.com/recharts-scale/-/recharts-scale-0.4.5.tgz#0969271f14e732e642fcc5bd4ab270d6e87dd1d9"
integrity sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==
dependencies:
decimal.js-light "^2.4.1"
recharts@^2.15.0:
version "2.15.0"
resolved "https://registry.yarnpkg.com/recharts/-/recharts-2.15.0.tgz#0b77bff57a43885df9769ae649a14cb1a7fe19aa"
integrity sha512-cIvMxDfpAmqAmVgc4yb7pgm/O1tmmkl/CjrvXuW+62/+7jj/iF9Ykm+hb/UJt42TREHMyd3gb+pkgoa2MxgDIw==
dependencies:
clsx "^2.0.0"
eventemitter3 "^4.0.1"
lodash "^4.17.21"
react-is "^18.3.1"
react-smooth "^4.0.0"
recharts-scale "^0.4.4"
tiny-invariant "^1.3.1"
victory-vendor "^36.6.8"
rechoir@^0.6.2:
version "0.6.2"
resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384"
@ -31331,6 +31593,13 @@ use-callback-ref@^1.3.0:
dependencies:
tslib "^2.0.0"
use-callback-ref@^1.3.3:
version "1.3.3"
resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.3.3.tgz#98d9fab067075841c5b2c6852090d5d0feabe2bf"
integrity sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==
dependencies:
tslib "^2.0.0"
use-debounce@10.0.4:
version "10.0.4"
resolved "https://registry.yarnpkg.com/use-debounce/-/use-debounce-10.0.4.tgz#2135be498ad855416c4495cfd8e0e130bd33bb24"
@ -31557,6 +31826,26 @@ vfile@^4.0.0:
unist-util-stringify-position "^2.0.0"
vfile-message "^2.0.0"
victory-vendor@^36.6.8:
version "36.9.2"
resolved "https://registry.yarnpkg.com/victory-vendor/-/victory-vendor-36.9.2.tgz#668b02a448fa4ea0f788dbf4228b7e64669ff801"
integrity sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==
dependencies:
"@types/d3-array" "^3.0.3"
"@types/d3-ease" "^3.0.0"
"@types/d3-interpolate" "^3.0.1"
"@types/d3-scale" "^4.0.2"
"@types/d3-shape" "^3.1.0"
"@types/d3-time" "^3.0.0"
"@types/d3-timer" "^3.0.0"
d3-array "^3.1.6"
d3-ease "^3.0.1"
d3-interpolate "^3.0.1"
d3-scale "^4.0.2"
d3-shape "^3.1.0"
d3-time "^3.0.0"
d3-timer "^3.0.1"
video-extensions@~1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/video-extensions/-/video-extensions-1.2.0.tgz#62f449f403b853f02da40964cbf34143f7d96731"