mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-04-08 02:52:39 -05:00
Post analytics prototype router (#22034)
ref https://linear.app/ghost/issue/DES-1082/router-prototype - The current router from `admin-x-framework` looks super complex. This PR is about testing React Router in the Post analytics prototype.
This commit is contained in:
parent
3211a146d4
commit
996c9d8c68
14 changed files with 255 additions and 36 deletions
|
@ -26,8 +26,8 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/react": "14.3.1",
|
||||
"@tryghost/shade": "0.0.0",
|
||||
"@tryghost/admin-x-framework": "0.0.0",
|
||||
"@tryghost/shade": "0.0.0",
|
||||
"@types/react": "18.3.3",
|
||||
"@types/react-dom": "18.3.0",
|
||||
"react": "18.3.1",
|
||||
|
@ -51,5 +51,8 @@
|
|||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"react-router": "^7.1.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import PostAnalytics from './views/post-analytics/PostAnalytics';
|
||||
import {FrameworkProvider, TopLevelFrameworkProps} from '@tryghost/admin-x-framework';
|
||||
import {RoutingProvider} from '@tryghost/admin-x-framework/routing';
|
||||
import {RouterProvider} from 'react-router';
|
||||
import {ShadeApp, ShadeAppProps, SidebarProvider} from '@tryghost/shade';
|
||||
import {router} from './routes';
|
||||
|
||||
interface AppProps {
|
||||
framework: TopLevelFrameworkProps;
|
||||
|
@ -11,13 +11,11 @@ interface AppProps {
|
|||
const App: React.FC<AppProps> = ({framework, designSystem}) => {
|
||||
return (
|
||||
<FrameworkProvider {...framework}>
|
||||
<RoutingProvider basePath='posts-x'>
|
||||
<ShadeApp className='posts' {...designSystem}>
|
||||
<SidebarProvider>
|
||||
<PostAnalytics />
|
||||
</SidebarProvider>
|
||||
</ShadeApp>
|
||||
</RoutingProvider>
|
||||
<ShadeApp className='posts' {...designSystem}>
|
||||
<SidebarProvider>
|
||||
<RouterProvider router={router} />
|
||||
</SidebarProvider>
|
||||
</ShadeApp>
|
||||
</FrameworkProvider>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import {Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator, Button, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuTrigger, H1, LucideIcon} from '@tryghost/shade';
|
||||
import {Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator, Button, Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuTrigger, H1, LucideIcon} from '@tryghost/shade';
|
||||
|
||||
interface headerProps {};
|
||||
|
||||
|
@ -22,7 +22,19 @@ const Header: React.FC<headerProps> = () => {
|
|||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Button variant='outline'><LucideIcon.Share />Share</Button>
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant='outline'><LucideIcon.Share />Share</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className='max-w-2xl'>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Share</DialogTitle>
|
||||
<DialogDescription>
|
||||
This is a dialog, lets see how it works.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<Button variant='outline'><LucideIcon.Ellipsis /></Button>
|
||||
|
|
30
apps/posts/src/routes.ts
Normal file
30
apps/posts/src/routes.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
import Newsletter from './views/post-analytics/components/Newsletter';
|
||||
import Overview from './views/post-analytics/components/Overview';
|
||||
import PostAnalytics from './views/post-analytics/PostAnalytics';
|
||||
import {createHashRouter} from 'react-router';
|
||||
|
||||
export const BASE_PATH = '/posts-x';
|
||||
export const ANALYTICS = `${BASE_PATH}/analytics`;
|
||||
|
||||
const postAnalyticsRoutes = [
|
||||
{
|
||||
path: `${BASE_PATH}/analytics/:postId`,
|
||||
Component: PostAnalytics,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
Component: Overview
|
||||
},
|
||||
{
|
||||
path: 'overview',
|
||||
Component: Overview
|
||||
},
|
||||
{
|
||||
path: 'newsletter',
|
||||
Component: Newsletter
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
export const router = createHashRouter(postAnalyticsRoutes);
|
|
@ -1,26 +1,44 @@
|
|||
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';
|
||||
import {ANALYTICS} from '../../routes';
|
||||
import {Outlet, useLocation, useNavigate, useParams} from 'react-router';
|
||||
import {Page, Tabs, TabsList, TabsTrigger} from '@tryghost/shade';
|
||||
|
||||
interface postAnalyticsProps {};
|
||||
|
||||
const PostAnalytics: React.FC<postAnalyticsProps> = () => {
|
||||
const navigate = useNavigate();
|
||||
const {postId} = useParams();
|
||||
const location = useLocation();
|
||||
|
||||
let currentTab = location.pathname.split('/').pop();
|
||||
if (currentTab === postId || !currentTab) {
|
||||
currentTab = 'overview';
|
||||
}
|
||||
|
||||
const handleTabChange = (value: string) => {
|
||||
if (value === 'overview') {
|
||||
navigate(`${ANALYTICS}/${postId}`);
|
||||
} else {
|
||||
navigate(`${ANALYTICS}/${postId}/${value}`);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<Header />
|
||||
<Tabs className='my-8 flex grow flex-col' defaultValue="overview" variant="button">
|
||||
<Tabs
|
||||
className='my-8 flex grow flex-col'
|
||||
value={currentTab}
|
||||
variant="underline"
|
||||
onValueChange={handleTabChange}
|
||||
>
|
||||
<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>
|
||||
<TabsTrigger value="overview">Overview</TabsTrigger>
|
||||
<TabsTrigger value="newsletter">Newsletter</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="overview">
|
||||
<Overview />
|
||||
</TabsContent>
|
||||
<TabsContent className='mt-0 flex grow flex-col' value="newsletter">
|
||||
<Newsletter />
|
||||
</TabsContent>
|
||||
<div>
|
||||
<Outlet />
|
||||
</div>
|
||||
</Tabs>
|
||||
</Page>
|
||||
);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import OpenedList from './newsletter/OpenedList';
|
||||
import React from 'react';
|
||||
import SentList from './newsletter/SentList';
|
||||
import {Badge} from '@tryghost/shade';
|
||||
import {Badge, Card, CardContent} from '@tryghost/shade';
|
||||
import {StatsTabItem, StatsTabTitle, StatsTabValue, StatsTabs, StatsTabsGroup} from './StatsTabs';
|
||||
|
||||
interface newsletterProps {};
|
||||
|
@ -13,7 +13,7 @@ const Newsletter: React.FC<newsletterProps> = () => {
|
|||
key: 'sent',
|
||||
title: 'Sent',
|
||||
value: '1,697',
|
||||
badge: '',
|
||||
badge: '100%',
|
||||
content: <SentList />
|
||||
},
|
||||
{
|
||||
|
@ -73,10 +73,12 @@ const Newsletter: React.FC<newsletterProps> = () => {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className='grid grow grid-cols-[auto_300px] gap-8 py-5'>
|
||||
<div className='border-t border-border'>
|
||||
<Content />
|
||||
</div>
|
||||
<div className='grid grow grid-cols-[auto_300px] gap-6 py-6'>
|
||||
<Card className='self-start'>
|
||||
<CardContent>
|
||||
<Content />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div className='-mt-px flex basis-[300px] flex-col'>
|
||||
<StatsTabs>
|
||||
{tabs.map(group => (
|
||||
|
|
|
@ -6,7 +6,7 @@ interface statsTabsProps
|
|||
extends React.HTMLAttributes<HTMLDivElement> {}
|
||||
|
||||
const StatsTabs: React.FC<statsTabsProps> = ({className, ...props}) => {
|
||||
return <div className={cn('flex flex-col gap-8', className)} {...props} />;
|
||||
return <div className={cn('flex flex-col gap-6', className)} {...props} />;
|
||||
};
|
||||
|
||||
interface statsTabsGroupProps
|
||||
|
|
|
@ -35,7 +35,7 @@ const NewsletterPerformance: React.FC<NewsletterPerformanceProps> = (props) => {
|
|||
<div className='grid grid-cols-3 py-5'>
|
||||
<Metric className='pl-6'>
|
||||
<MetricLabel>Sent</MetricLabel>
|
||||
<MetricValue>1,697</MetricValue>
|
||||
<MetricValue>1,697 <MetricPercentage>100%</MetricPercentage></MetricValue>
|
||||
</Metric>
|
||||
|
||||
<Metric className='pl-6'>
|
||||
|
|
122
apps/shade/src/components/ui/dialog.tsx
Normal file
122
apps/shade/src/components/ui/dialog.tsx
Normal file
|
@ -0,0 +1,122 @@
|
|||
import * as React from 'react';
|
||||
import * as DialogPrimitive from '@radix-ui/react-dialog';
|
||||
import {X} from 'lucide-react';
|
||||
|
||||
import {cn} from '@/lib/utils';
|
||||
|
||||
const Dialog = DialogPrimitive.Root;
|
||||
|
||||
const DialogTrigger = DialogPrimitive.Trigger;
|
||||
|
||||
const DialogPortal = DialogPrimitive.Portal;
|
||||
|
||||
const DialogClose = DialogPrimitive.Close;
|
||||
|
||||
const DialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||
>(({className, ...props}, ref) => (
|
||||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
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}
|
||||
/>
|
||||
));
|
||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
||||
|
||||
const DialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
||||
>(({className, children, ...props}, ref) => (
|
||||
<DialogPortal>
|
||||
<div className='shade'>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<DialogPrimitive.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-accent data-[state=open]:text-muted-foreground">
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</div>
|
||||
</DialogPortal>
|
||||
));
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
||||
|
||||
const DialogHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col space-y-1.5 text-center sm:text-left',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
DialogHeader.displayName = 'DialogHeader';
|
||||
|
||||
const DialogFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
DialogFooter.displayName = 'DialogFooter';
|
||||
|
||||
const DialogTitle = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
||||
>(({className, ...props}, ref) => (
|
||||
<DialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'text-lg font-semibold leading-none tracking-tight',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DialogTitle.displayName = DialogPrimitive.Title.displayName;
|
||||
|
||||
const DialogDescription = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||
>(({className, ...props}, ref) => (
|
||||
<DialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn('text-sm text-muted-foreground', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DialogDescription.displayName = DialogPrimitive.Description.displayName;
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogPortal,
|
||||
DialogOverlay,
|
||||
DialogTrigger,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
DialogDescription
|
||||
};
|
|
@ -76,7 +76,7 @@ const tabsTriggerVariants = cva(
|
|||
variant: {
|
||||
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-[""]'
|
||||
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-0.5 data-[state=active]:after:bg-black data-[state=active]:after:content-[""]'
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
|
|
|
@ -5,6 +5,7 @@ export * from './components/ui/breadcrumb';
|
|||
export * from './components/ui/button';
|
||||
export * from './components/ui/card';
|
||||
export * from './components/ui/chart';
|
||||
export * from './components/ui/dialog';
|
||||
export * from './components/ui/dropdown-menu';
|
||||
export * from './components/ui/input';
|
||||
export * from './components/ui/separator';
|
||||
|
|
|
@ -164,7 +164,7 @@
|
|||
{{/if}}
|
||||
{{#if (feature "postsX")}}
|
||||
<li>
|
||||
<LinkTo @route="posts-x" @current-when="posts-x">{{svg-jar "chart"}}Post analytics</LinkTo>
|
||||
<LinkTo @route="posts-x.analytics.123" @current-when="posts-x">{{svg-jar "chart"}}Post analytics</LinkTo>
|
||||
</li>
|
||||
{{/if}}
|
||||
</ul>
|
||||
|
|
|
@ -54,6 +54,9 @@ Router.map(function () {
|
|||
|
||||
this.route('posts-x', function () {
|
||||
this.route('posts-x', {path: '/*sub'});
|
||||
this.route('analytics', function () {
|
||||
this.route('123');
|
||||
});
|
||||
});
|
||||
|
||||
this.route('settings-x', {path: '/settings'}, function () {
|
||||
|
|
30
yarn.lock
30
yarn.lock
|
@ -8276,6 +8276,11 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.4.1.tgz#bfd02c1f2224567676c1545199f87c3a861d878d"
|
||||
integrity sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==
|
||||
|
||||
"@types/cookie@^0.6.0":
|
||||
version "0.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.6.0.tgz#eac397f28bf1d6ae0ae081363eca2f425bedf0d5"
|
||||
integrity sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==
|
||||
|
||||
"@types/cookiejar@^2.1.5":
|
||||
version "2.1.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/cookiejar/-/cookiejar-2.1.5.tgz#14a3e83fa641beb169a2dd8422d91c3c345a9a78"
|
||||
|
@ -13562,6 +13567,11 @@ cookie@0.7.2:
|
|||
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.2.tgz#556369c472a2ba910f2979891b526b3436237ed7"
|
||||
integrity sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==
|
||||
|
||||
cookie@^1.0.1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/cookie/-/cookie-1.0.2.tgz#27360701532116bd3f1f9416929d176afe1e4610"
|
||||
integrity sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==
|
||||
|
||||
cookie@~0.4.1:
|
||||
version "0.4.2"
|
||||
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432"
|
||||
|
@ -27580,6 +27590,16 @@ react-remove-scroll@^2.6.1:
|
|||
use-callback-ref "^1.3.3"
|
||||
use-sidecar "^1.1.2"
|
||||
|
||||
react-router@^7.1.3:
|
||||
version "7.1.3"
|
||||
resolved "https://registry.yarnpkg.com/react-router/-/react-router-7.1.3.tgz#6c15c28838b799cb3058943e8e8015dbd6c16c7b"
|
||||
integrity sha512-EezYymLY6Guk/zLQ2vRA8WvdUhWFEj5fcE3RfWihhxXBW7+cd1LsIiA3lmx+KCmneAGQuyBv820o44L2+TtkSA==
|
||||
dependencies:
|
||||
"@types/cookie" "^0.6.0"
|
||||
cookie "^1.0.1"
|
||||
set-cookie-parser "^2.6.0"
|
||||
turbo-stream "2.4.0"
|
||||
|
||||
react-select@5.8.2:
|
||||
version "5.8.2"
|
||||
resolved "https://registry.yarnpkg.com/react-select/-/react-select-5.8.2.tgz#0d7ccb1895d61aafcd090fbf65aa9e506225a854"
|
||||
|
@ -28871,6 +28891,11 @@ set-blocking@^2.0.0, set-blocking@~2.0.0:
|
|||
resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
|
||||
integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==
|
||||
|
||||
set-cookie-parser@^2.6.0:
|
||||
version "2.7.1"
|
||||
resolved "https://registry.yarnpkg.com/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz#3016f150072202dfbe90fadee053573cc89d2943"
|
||||
integrity sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==
|
||||
|
||||
set-function-length@^1.2.2:
|
||||
version "1.2.2"
|
||||
resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449"
|
||||
|
@ -31071,6 +31096,11 @@ tunnel@^0.0.6:
|
|||
resolved "https://registry.yarnpkg.com/tunnel/-/tunnel-0.0.6.tgz#72f1314b34a5b192db012324df2cc587ca47f92c"
|
||||
integrity sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==
|
||||
|
||||
turbo-stream@2.4.0:
|
||||
version "2.4.0"
|
||||
resolved "https://registry.yarnpkg.com/turbo-stream/-/turbo-stream-2.4.0.tgz#1e4fca6725e90fa14ac4adb782f2d3759a5695f0"
|
||||
integrity sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==
|
||||
|
||||
tweetnacl@^0.14.3, tweetnacl@~0.14.0:
|
||||
version "0.14.5"
|
||||
resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
|
||||
|
|
Loading…
Add table
Reference in a new issue