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:
parent
4d93defea0
commit
e1f5ff1533
45 changed files with 2804 additions and 245 deletions
|
@ -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 |
|
@ -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>
|
||||
|
|
50
apps/posts/src/components/Header.tsx
Normal file
50
apps/posts/src/components/Header.tsx
Normal 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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
29
apps/posts/src/views/post-analytics/PostAnalytics.tsx
Normal file
29
apps/posts/src/views/post-analytics/PostAnalytics.tsx
Normal 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;
|
104
apps/posts/src/views/post-analytics/components/Newsletter.tsx
Normal file
104
apps/posts/src/views/post-analytics/components/Newsletter.tsx
Normal 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;
|
|
@ -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' />
|
57
apps/posts/src/views/post-analytics/components/StatsTabs.tsx
Normal file
57
apps/posts/src/views/post-analytics/components/StatsTabs.tsx
Normal 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
|
||||
};
|
|
@ -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;
|
|
@ -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;
|
|
@ -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 →</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClickPerformance;
|
|
@ -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 →</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default Conversions;
|
|
@ -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;
|
|
@ -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};
|
|
@ -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;
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 |
|
@ -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}
|
||||
/>
|
||||
);
|
||||
|
|
48
apps/shade/src/components/ui/avatar.tsx
Normal file
48
apps/shade/src/components/ui/avatar.tsx
Normal 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};
|
38
apps/shade/src/components/ui/badge.tsx
Normal file
38
apps/shade/src/components/ui/badge.tsx
Normal 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};
|
|
@ -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';
|
||||
|
||||
|
|
363
apps/shade/src/components/ui/chart.tsx
Normal file
363
apps/shade/src/components/ui/chart.tsx
Normal 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
|
||||
};
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
|
|
22
apps/shade/src/components/ui/input.tsx
Normal file
22
apps/shade/src/components/ui/input.tsx
Normal 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};
|
29
apps/shade/src/components/ui/separator.tsx
Normal file
29
apps/shade/src/components/ui/separator.tsx
Normal 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};
|
140
apps/shade/src/components/ui/sheet.tsx
Normal file
140
apps/shade/src/components/ui/sheet.tsx
Normal 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
|
||||
};
|
765
apps/shade/src/components/ui/sidebar.tsx
Normal file
765
apps/shade/src/components/ui/sidebar.tsx
Normal 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
|
||||
};
|
15
apps/shade/src/components/ui/skeleton.tsx
Normal file
15
apps/shade/src/components/ui/skeleton.tsx
Normal 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};
|
120
apps/shade/src/components/ui/table.tsx
Normal file
120
apps/shade/src/components/ui/table.tsx
Normal 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
|
||||
};
|
|
@ -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: {
|
||||
|
|
30
apps/shade/src/components/ui/tooltip.tsx
Normal file
30
apps/shade/src/components/ui/tooltip.tsx
Normal 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};
|
19
apps/shade/src/hooks/use-mobile.tsx
Normal file
19
apps/shade/src/hooks/use-mobile.tsx
Normal 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;
|
||||
}
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
307
yarn.lock
|
@ -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"
|
||||
|
|
Loading…
Add table
Reference in a new issue