Merge branch 'staging' into feat/add-more-shortcuts

This commit is contained in:
Ahmet Kilinc
2025-05-11 14:42:46 +01:00
committed by GitHub
28 changed files with 789 additions and 384 deletions

View File

@@ -99,14 +99,14 @@ export default function PricingPage() {
return (
<main className="relative flex h-screen flex-1 flex-col overflow-x-hidden bg-[#0F0F0F]">
{/* <PixelatedBackground
className="z-1 absolute -top-32 left-1/2 h-auto w-screen min-w-[1920px] -translate-x-1/2 object-cover opacity-5"
<PixelatedBackground
className="z-1 absolute -top-72 left-1/2 h-auto w-screen min-w-[1920px] -translate-x-1/2 object-cover opacity-5"
style={{ mixBlendMode: 'screen' }}
/> */}
/>
{/* Desktop Navigation - Hidden on mobile */}
<header className="fixed z-50 hidden w-full items-center justify-center px-4 pt-6 md:flex">
<nav className="border-input/50 bg-popover flex w-full max-w-3xl items-center justify-between gap-2 rounded-xl border-t p-2 px-4">
<nav className="border-input/50 flex w-full max-w-3xl items-center justify-between gap-2 rounded-xl border-t bg-[#1E1E1E] p-2 px-4">
<div className="flex items-center gap-6">
<Link href="/" className="relative cursor-pointer">
<Image src="white-icon.svg" alt="Zero Email" width={22} height={22} />
@@ -218,57 +218,61 @@ export default function PricingPage() {
</Sheet>
</div>
<div className="container mx-auto mt-40 h-screen px-4 py-16">
<div className="container mx-auto mt-12 h-screen px-4 py-16 md:mt-24">
<div className="mb-12 text-center">
<h1 className="mb-4 text-4xl font-bold text-white">Simple, Transparent Pricing</h1>
<p className="text-muted-foreground text-lg">Choose the plan that's right for you</p>
<h1 className="mb-2 text-4xl font-bold text-white md:text-6xl">Pricing</h1>
<p className="text-white/50 text-lg">Choose the plan that's right for you</p>
</div>
<div className="mx-auto flex max-w-4xl items-center justify-center gap-8">
<div className="mx-auto max-w-5xl grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{/* Free Plan */}
<div className="relative flex w-full flex-col rounded-xl border bg-[#141414] p-8">
<div className="relative flex flex-col rounded-xl border bg-[#121212] px-8 pt-8 pb-4 h-full">
<h1 className="mb-4 text-center text-lg font-normal text-white/50">Free</h1>
<div className="mb-4 text-center text-3xl font-bold dark:text-white">
Free <span className="text-lg font-medium"></span>
$0 <span className="text-lg font-medium">/ mo</span>
</div>
<ul className="mb-6 w-full flex-grow space-y-2 text-left">
<li className="flex items-center gap-2">
<CircleCheck className="h-4 w-4 fill-[#2FAD71]" /> Unlimited email connections
<CircleCheck className="h-4 w-4 fill-[#2FAD71]" /> 1 email connection
</li>
<li className="flex items-center gap-2">
<CircleCheck className="h-4 w-4 fill-[#2FAD71]" /> AI-powered chat with your inbox
<CircleCheck className="h-4 w-4 fill-[#2FAD71]" /> AI chat <span className="text-white/50 text-xs">(25 per day)</span>
</li>
<li className="flex items-center gap-2">
<CircleCheck className="h-4 w-4 fill-[#2FAD71]" /> One-click AI email writing &
replies
<CircleCheck className="h-4 w-4 fill-[#2FAD71]" /> AI writing assistant
</li>
<li className="flex items-center gap-2">
<CircleCheck className="h-4 w-4 fill-[#2FAD71]" /> Auto labeling<span className="text-white/50 text-xs">(25 per day)</span>
</li>
<li className="flex items-center gap-2">
<CircleCheck className="h-4 w-4 fill-[#2FAD71]" />
<span>AI thread summaries <span className="text-white/50 text-xs">(25 per day)</span></span>
</li>
{/* <li className="flex items-center gap-2">
<CircleX className="h-4 w-4 fill-white opacity-50" /> Instant thread AI-generated
summaries
</li>
<li className="flex items-center gap-2">
<CircleX className="h-4 w-4 fill-white opacity-50" /> Auto labeling
</li>
<li className="flex items-center gap-2">
<CircleX className="h-4 w-4 fill-white opacity-50" /> Verified checkmark
</li>
<li className="flex items-center gap-2">
<CircleX className="h-4 w-4 fill-white opacity-50" /> Priority customer support
</li>
</li> */}
</ul>
<Button
variant="outline"
className="h-10 w-full"
onClick={handleUpgrade}
disabled={!isPro}
>
{isPro ? 'Downgrade' : 'Current Plan'}
</Button>
<Link href="/login">
<Button className="h-8 w-full" onClick={handleUpgrade}>
Get Started
</Button>
</Link>
</div>
{/* Pro Plan */}
<div className="relative flex w-full flex-col rounded-xl border bg-[#141414] p-8">
<div className="relative flex flex-col rounded-xl border bg-[#121212] px-8 pt-8 pb-4 h-full">
<h1 className="mb-4 text-center text-lg font-normal text-white/50">Pro</h1>
<div className="mb-4 text-center text-3xl font-bold dark:text-white">
$20 <span className="text-lg font-medium">/ mo</span>
</div>
@@ -278,30 +282,70 @@ export default function PricingPage() {
</li>
<li className="flex items-center gap-2">
<CircleCheck className="h-4 w-4 fill-[#2FAD71]" /> AI-powered chat with your inbox
<CircleCheck className="h-4 w-4 fill-[#2FAD71]" /> AI chat
</li>
<li className="flex items-center gap-2">
<CircleCheck className="h-4 w-4 fill-[#2FAD71]" /> One-click AI email writing &
replies
</li>
<li className="flex items-center gap-2">
<CircleCheck className="h-4 w-4 fill-[#2FAD71]" /> Instant thread AI-generated
summaries
<CircleCheck className="h-4 w-4 fill-[#2FAD71]" /> AI writing assistant
</li>
<li className="flex items-center gap-2">
<CircleCheck className="h-4 w-4 fill-[#2FAD71]" /> Auto labeling
</li>
<li className="flex items-center gap-2">
<CircleCheck className="h-4 w-4 fill-[#2FAD71]" /> AI thread summaries
</li>
<li className="flex items-center gap-2">
<CircleCheck className="h-4 w-4 fill-[#2FAD71]" /> Verified checkmark
</li>
</ul>
<Button className="h-8 w-full" onClick={handleUpgrade}>
Get Started
</Button>
</div>
{/* Enterprise Plan */}
<div className="relative flex flex-col rounded-xl border bg-[#121212] px-8 pt-8 pb-4 h-full">
<h1 className="mb-4 text-center text-lg font-normal text-white/50">Enterprise</h1>
<div className="mb-4 text-center text-3xl font-bold dark:text-white">Contact us</div>
<ul className="mb-6 w-full flex-grow space-y-2 text-left">
<li className="flex items-center gap-2">
<CircleCheck className="h-4 w-4 fill-[#2FAD71]" /> Unlimited email connections
</li>
<li className="flex items-center gap-2">
<CircleCheck className="h-4 w-4 fill-[#2FAD71]" /> AI chat
</li>
<li className="flex items-center gap-2">
<CircleCheck className="h-4 w-4 fill-[#2FAD71]" /> AI writing assistant
</li>
<li className="flex items-center gap-2">
<CircleCheck className="h-4 w-4 fill-[#2FAD71]" /> Auto labeling
</li>
<li className="flex items-center gap-2">
<CircleCheck className="h-4 w-4 fill-[#2FAD71]" /> AI thread summaries
</li>
<li className="flex items-center gap-2">
<CircleCheck className="h-4 w-4 fill-[#2FAD71]" /> Verified checkmark
</li>
{/* <li className="flex items-center gap-2">
<CircleCheck className="h-4 w-4 fill-[#2FAD71]" /> Management dashboard
</li> */}
<li className="flex items-center gap-2">
<CircleCheck className="h-4 w-4 fill-[#2FAD71]" /> Priority customer support
</li>
</ul>
<Button className="h-10 w-full" onClick={handleUpgrade} disabled={isPro}>
{isPro ? 'Current Plan' : 'Upgrade'}
</Button>
<Link href="https://cal.com/team/0/chat" target="_blank">
<Button className="h-8 w-full" onClick={handleUpgrade}>
Contact us
</Button>
</Link>
</div>
</div>
</div>

View File

@@ -1,13 +1,16 @@
import { HotkeyProviderWrapper } from '@/components/providers/hotkey-provider-wrapper';
import { AppSidebar } from '@/components/ui/app-sidebar';
import { OnboardingWrapper } from '@/components/onboarding';
export default function MailLayout({ children }: { children: React.ReactNode }) {
import { NotificationProvider } from '@/components/party';
import { AppSidebar } from '@/components/ui/app-sidebar';
import { headers } from 'next/headers';
export default async function MailLayout({ children }: { children: React.ReactNode }) {
const headersList = await headers();
return (
<HotkeyProviderWrapper>
<AppSidebar />
<div className="bg-lightBackground dark:bg-darkBackground w-full">{children}</div>
<OnboardingWrapper />
<NotificationProvider headers={Object.fromEntries(headersList.entries())} />
</HotkeyProviderWrapper>
);
}

View File

@@ -158,6 +158,7 @@ export function AIChat() {
if (threadId) refetchThread();
queryClient.invalidateQueries({ queryKey: trpc.mail.get.queryKey() });
refetch();
console.log('refetching all');
}, [threadId, queryClient, trpc.mail.get.queryKey]);
useEffect(() => {
@@ -165,7 +166,7 @@ export function AIChat() {
refetchAll();
}
prevStatusRef.current = status;
}, [status, refetchAll]);
}, [status]);
const scrollToBottom = useCallback(() => {
if (messagesEndRef.current) {

View File

@@ -253,6 +253,7 @@ export function EmailComposer({
const handleSend = async () => {
try {
if (isLoading) return;
setIsLoading(true);
setAiGeneratedMessage(null);
const values = getValues();
@@ -869,7 +870,7 @@ export function EmailComposer({
<Sparkles className="h-3.5 w-3.5 fill-black dark:fill-white" />
)}
</div>
<div className="text-center text-sm leading-none text-black dark:text-white hidden md:block">
<div className="hidden text-center text-sm leading-none text-black md:block dark:text-white">
Generate
</div>
</div>
@@ -879,7 +880,7 @@ export function EmailComposer({
<TooltipTrigger asChild>
<button
disabled
className="h-7 items-center gap-0.5 overflow-hidden rounded-md bg-white/5 px-1.5 shadow-sm hover:bg-white/10 disabled:opacity-50 hidden md:flex"
className="hidden h-7 items-center gap-0.5 overflow-hidden rounded-md bg-white/5 px-1.5 shadow-sm hover:bg-white/10 disabled:opacity-50 md:flex"
>
<Smile className="h-3 w-3 fill-[#9A9A9A]" />
<span className="px-0.5 text-sm">Casual</span>
@@ -893,7 +894,7 @@ export function EmailComposer({
<TooltipTrigger asChild>
<button
disabled
className="flex h-7 items-center gap-0.5 overflow-hidden rounded-md bg-white/5 px-1.5 shadow-sm hover:bg-white/10 disabled:opacity-50 hidden md:flex"
className="flex hidden h-7 items-center gap-0.5 overflow-hidden rounded-md bg-white/5 px-1.5 shadow-sm hover:bg-white/10 disabled:opacity-50 md:flex"
>
{messageLength < 50 && <ShortStack className="h-3 w-3 fill-[#9A9A9A]" />}
{messageLength >= 50 && messageLength < 200 && (

View File

@@ -11,7 +11,7 @@ export function PixelatedBackground(props: SVGProps<SVGSVGElement>) {
xmlnsXlink="http://www.w3.org/1999/xlink"
{...props}
>
<title>Pixelated Background</title>
<g mask="url(#mask0_11_932)">
<rect
width="1438.5"

View File

@@ -590,7 +590,7 @@ const Thread = memo(
) : (
<p
className={cn(
'mt-1 line-clamp-1 max-w-[50ch] text-sm text-[#8C8C8C] md:max-w-[40ch]',
'mt-1 line-clamp-1 max-w-[25ch] sm:max-w-[50ch] text-sm text-[#8C8C8C] md:max-w-[40ch]',
)}
>
{highlightText(latestMessage.subject, searchValue.highlight)}

View File

@@ -29,10 +29,11 @@ import {
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '@/components/ui/resizable';
import { Drawer, DrawerContent, DrawerHeader, DrawerTitle } from '@/components/ui/drawer';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { ThreadDemo, ThreadDisplay } from '@/components/mail/thread-display';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { MailList, MailListDemo } from '@/components/mail/mail-list';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { Command, RefreshCcw, Settings2Icon } from 'lucide-react';
import { trpcClient, useTRPC } from '@/providers/query-provider';
import { backgroundQueueAtom } from '@/store/backgroundQueue';
import { handleUnsubscribe } from '@/lib/email-utils.client';
@@ -46,7 +47,7 @@ import { SidebarToggle } from '../ui/sidebar-toggle';
import { Skeleton } from '@/components/ui/skeleton';
import { useBrainState } from '@/hooks/use-summary';
import { clearBulkSelectionAtom } from './use-mail';
import { Command, RefreshCcw } from 'lucide-react';
import { cleanSearchValue, cn } from '@/lib/utils';
import { useThreads } from '@/hooks/use-threads';
import { useIsMobile } from '@/hooks/use-mobile';
import { Button } from '@/components/ui/button';
@@ -55,10 +56,97 @@ import { useStats } from '@/hooks/use-stats';
import { useTranslations } from 'next-intl';
import { SearchBar } from './search-bar';
import { useQueryState } from 'nuqs';
import { cn } from '@/lib/utils';
import { TagInput } from 'emblor';
import { useAtom } from 'jotai';
import { toast } from 'sonner';
interface Tag {
id: string;
name: string;
text: string;
}
const defaultLabels = [
'urgent',
'review',
'followup',
'decision',
'work',
'finance',
'legal',
'hiring',
'sales',
'product',
'support',
'vendors',
'marketing',
'meeting',
'investors',
];
const AutoLabelingSettings = () => {
const trpc = useTRPC();
const [open, setOpen] = useState(false);
const { data: storedLabels } = useQuery(trpc.brain.getLabels.queryOptions());
const { mutateAsync: updateLabels, isPending } = useMutation(
trpc.brain.updateLabels.mutationOptions(),
);
const [labels, setLabels] = useState<Tag[]>([]);
const [activeTagIndex, setActiveTagIndex] = useState(0);
useEffect(() => {
if (storedLabels) {
setLabels(storedLabels.map((label) => ({ id: label, name: label, text: label })));
}
}, [storedLabels]);
const handleResetToDefault = useCallback(() => {
setLabels(defaultLabels.map((label) => ({ id: label, name: label, text: label })));
}, [storedLabels]);
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button variant="ghost" className="md:h-fit md:px-2">
<Settings2Icon className="text-muted-foreground h-4 w-4 cursor-pointer" />
</Button>
</DialogTrigger>
<DialogContent showOverlay>
<DialogHeader>
<DialogTitle>Autolabeling Settings</DialogTitle>
</DialogHeader>
<DialogDescription className="mb-4">
These are the labels Zero uses to autolabel your incoming emails. Feel free to modify them
however you like. Zero will create a new label in your account for each label you add - if
it does not exist already.
</DialogDescription>
<TagInput
setTags={setLabels as any}
tags={labels}
activeTagIndex={activeTagIndex}
setActiveTagIndex={setActiveTagIndex as any}
/>
<DialogFooter className="mt-4">
<Button onClick={handleResetToDefault} variant="outline" size={'sm'}>
Use default labels
</Button>
<Button
disabled={isPending}
onClick={() => {
updateLabels({ labels: labels.map((label) => label.id) }).then(() => {
setOpen(false);
toast.success('Labels updated successfully, Zero will start using them.');
});
}}
>
Save
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
export function MailLayout() {
const params = useParams<{ folder: string }>();
const folder = params?.folder ?? 'inbox';
@@ -86,10 +174,16 @@ export function MailLayout() {
}, [session?.user, isPending]);
const [{ isLoading, isFetching, refetch: refetchThreads }] = useThreads();
const trpc = useTRPC();
const isDesktop = useMediaQuery('(min-width: 768px)');
const { mutateAsync: EnableBrain, isPending: isEnablingBrain } = useMutation(
trpc.brain.enableBrain.mutationOptions(),
);
const { mutateAsync: DisableBrain, isPending: isDisablingBrain } = useMutation(
trpc.brain.disableBrain.mutationOptions(),
);
const [threadId, setThreadId] = useQueryState('threadId');
const { refetch: refetchBrainState } = useBrainState();
useEffect(() => {
if (threadId) {
@@ -115,6 +209,36 @@ export function MailLayout() {
setActiveReplyId(null);
}, [setThreadId]);
const handleEnableBrain = useCallback(async () => {
toast.promise(EnableBrain({}), {
loading: 'Enabling autolabeling...',
success: 'Autolabeling enabled successfully',
error: 'Failed to enable autolabeling',
finally: () => {
refetchBrainState();
},
});
}, []);
const handleDisableBrain = useCallback(async () => {
toast.promise(DisableBrain({}), {
loading: 'Disabling autolabeling...',
success: 'Autolabeling disabled successfully',
error: 'Failed to disable autolabeling',
finally: () => {
refetchBrainState();
},
});
}, []);
const handleToggleAutolabeling = useCallback(() => {
if (brainState?.enabled) {
handleDisableBrain();
} else {
handleEnableBrain();
}
}, [brainState?.enabled]);
// Add mailto protocol handler registration
useEffect(() => {
// Register as a mailto protocol handler if browser supports it
@@ -183,16 +307,22 @@ export function MailLayout() {
) : null}
</div>
<div className="flex items-center gap-2">
{brainState?.enabled ? (
<Button
variant="outline"
size={'sm'}
className="text-muted-foreground h-fit min-h-0 px-2 py-1 text-[10px] uppercase"
>
<div className="h-2 w-2 animate-pulse rounded-full bg-green-400" />
Auto Labeling
</Button>
) : null}
{true ? <AutoLabelingSettings /> : null}
<Button
disabled={isEnablingBrain || isDisablingBrain}
onClick={handleToggleAutolabeling}
variant="outline"
size={'sm'}
className="text-muted-foreground h-fit min-h-0 px-2 py-1 text-[10px] uppercase"
>
<div
className={cn(
'h-2 w-2 animate-pulse rounded-full',
brainState?.enabled ? 'bg-green-400' : 'bg-red-400',
)}
/>
Auto Labeling
</Button>
<Button
onClick={() => {
refetchThreads();
@@ -637,7 +767,7 @@ function CategorySelect({ isMultiSelectMode }: { isMultiSelectMode: boolean }) {
onClick={() => {
setCategory(cat.id);
setSearchValue({
value: cat.searchValue || '',
value: `${cat.searchValue} ${cleanSearchValue(searchValue.value).trim().length ? `AND ${cleanSearchValue(searchValue.value)}` : ''}`,
highlight: searchValue.highlight,
folder: '',
});

View File

@@ -169,6 +169,7 @@ export function ThreadDisplay() {
const [focusedIndex, setFocusedIndex] = useAtom(focusedIndexAtom);
const trpc = useTRPC();
const { mutateAsync: markAsRead } = useMutation(trpc.mail.markAsRead.mutationOptions());
const [, setIsComposeOpen] = useQueryState('isComposeOpen');
const handlePrevious = useCallback(() => {
if (!id || !items.length || focusedIndex === null) return;
@@ -348,7 +349,6 @@ export function ThreadDisplay() {
isFullscreen ? 'fixed inset-0 z-50' : '',
)}
>
<div></div>
{!id ? (
<div className="flex h-full items-center justify-center">
<div className="flex flex-col items-center justify-center gap-2 text-center">
@@ -361,8 +361,24 @@ export function ThreadDisplay() {
<div className="mt-5">
<p className="text-lg">It's empty here</p>
<p className="text-md text-[#6D6D6D] dark:text-white/50">
Choose an email to view details
Choose an email to view details or
</p>
<div className="mt-4 flex gap-2">
<Button onClick={toggleAISidebar} variant="outline">
Chat with Zero AI
</Button>
<Button onClick={() => setIsComposeOpen('true')} variant="outline">
Write an email
</Button>
<Tooltip>
<TooltipTrigger asChild>
<Button className="opacity-50" variant="outline">
Label last 50 emails
</Button>
</TooltipTrigger>
<TooltipContent>Coming soon</TooltipContent>
</Tooltip>
</div>
</div>
</div>
{!isSidebarOpen && (

View File

@@ -1,15 +1,14 @@
'use client';
import { Dialog, DialogContent, DialogTitle } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { useState, useEffect, useMemo } from 'react';
import { useBilling } from '@/hooks/use-billing';
import { Button } from '@/components/ui/button';
import { useCustomer } from 'autumn-js/next';
import confetti from 'canvas-confetti';
import Image from 'next/image';
import { CircleCheck } from './icons/icons';
import { useBilling } from '@/hooks/use-billing';
import { useCustomer } from 'autumn-js/next';
const getSteps = (isPro: boolean) => [
const steps = [
{
title: 'Welcome to Zero Email!',
description: 'Your new intelligent email experience starts here.',
@@ -17,62 +16,19 @@ const getSteps = (isPro: boolean) => [
},
{
title: 'Chat with your inbox',
description: 'Zero allows you to chat with your inbox and do tasks on your behalf.',
description: 'Zero allows you to chat with your inbox, and take actions on your behalf.',
video: 'https://assets.0.email/step2.gif',
},
{
title: 'AI Compose & Reply',
description: 'Our AI assistant allows you to write emails with a single click.',
description: 'Our AI assistant allows you to write emails that sound like you.',
video: 'https://assets.0.email/step1.gif',
},
{
title: 'Label your emails',
description: 'Zero helps you label your emails and helps you focus on what matters.',
video: '/onboarding/step3.gif',
description: 'Zero helps you label your emails to focus on what matters.',
video: 'https://assets.0.email/step3.gif',
},
// ...(isPro ? [] : [{
// title: 'Upgrade to Zero Pro',
// description: (handleUpgrade: () => void) => (
// <>
// <div className="flex flex-col items-center justify-center p-4 bg-[#141414] rounded-xl border max-w-md mx-auto mt-4">
// <div className="text-3xl font-bold mb-2 dark:text-white">$20 <span className="text-lg font-medium">/ mo</span></div>
// <ul className="text-left w-full mb-6 space-y-2">
// <li className="flex items-center gap-2">
// <CircleCheck className="w-4 h-4 fill-[#2FAD71]" /> Unlimited email connections
// </li>
// <li className="flex items-center gap-2">
// <CircleCheck className="w-4 h-4 fill-[#2FAD71]" /> AI-powered chat with your inbox
// </li>
// <li className="flex items-center gap-2">
// <CircleCheck className="w-4 h-4 fill-[#2FAD71]" /> Auto labeling
// </li>
// <li className="flex items-center gap-2">
// <CircleCheck className="w-4 h-4 fill-[#2FAD71]" /> One-click AI email writing & replies
// </li>
// <li className="flex items-center gap-2">
// <CircleCheck className="w-4 h-4 fill-[#2FAD71]" /> Instant thread AI-generated summaries
// </li>
// <li className="flex items-center gap-2">
// <CircleCheck className="w-4 h-4 fill-[#2FAD71]" /> Priority customer support
// </li>
// <li className="flex items-center gap-2">
// <CircleCheck className="w-4 h-4 fill-[#2FAD71]" /> Access to private Discord community
// </li>
// </ul>
// <Button
// className="h-8 w-full"
// onClick={(e) => {
// e.stopPropagation();
// handleUpgrade();
// }}
// >
// Get Zero Pro
// </Button>
// </div>
// </>
// ),
// video: null,
// }]),
{
title: 'Coming Soon',
description: (
@@ -82,12 +38,12 @@ const getSteps = (isPro: boolean) => [
</span>
</>
),
video: '/onboarding/coming-soon.png',
video: 'https://assets.0.email/coming-soon.png',
},
{
title: 'Ready to start?',
description: 'Click below to begin your intelligent email experience!',
video: '/onboarding/ready.png',
video: 'https://assets.0.email/ready.png',
},
];
@@ -99,21 +55,6 @@ export function OnboardingDialog({
onOpenChange: (open: boolean) => void;
}) {
const [currentStep, setCurrentStep] = useState(0);
const { attach } = useBilling();
const { customer } = useCustomer();
const isPro = useMemo(() => {
return (
customer &&
Array.isArray(customer.products) &&
customer.products.some(
(product: any) =>
product.id.includes('pro-example') || product.name.includes('pro-example'),
)
);
}, [customer]);
const steps = useMemo(() => getSteps(isPro), [isPro]);
useEffect(() => {
if (currentStep === steps.length - 1) {
@@ -133,19 +74,6 @@ export function OnboardingDialog({
}
};
const handleUpgrade = async () => {
if (attach) {
try {
await attach({
productId: 'pro-example',
successUrl: `${window.location.origin}/mail/inbox?success=true`,
});
} catch (error) {
console.error('Failed to upgrade:', error);
}
}
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogTitle></DialogTitle>
@@ -169,9 +97,7 @@ export function OnboardingDialog({
<div className="space-y-2 text-center">
<h2 className="text-4xl font-semibold">{steps[currentStep]?.title}</h2>
<p className="text-muted-foreground mx-auto max-w-md text-sm">
{typeof steps[currentStep]?.description === 'function'
? steps[currentStep]?.description(handleUpgrade)
: steps[currentStep]?.description}
{steps[currentStep]?.description}
</p>
</div>
@@ -193,7 +119,7 @@ export function OnboardingDialog({
height={500}
src={step.video}
alt={step.title}
className="h-full w-full object-cover rounded-lg border"
className="h-full w-full rounded-lg border object-cover"
/>
</div>
),

View File

@@ -0,0 +1,30 @@
'use client';
import { atom, useAtomValue, useSetAtom } from 'jotai';
import { useTRPC } from '@/providers/query-provider';
import { usePartySocket } from 'partysocket/react';
import { useSession } from '@/lib/auth-client';
import { useEffect } from 'react';
export const NotificationProvider = ({ headers }: { headers: Record<string, string> }) => {
// const trpc = useTRPC();
const { data: session } = useSession();
usePartySocket({
party: 'durable-mailbox',
room: session?.activeConnection?.email
? `${session.activeConnection.email}:general`
: 'general',
prefix: 'zero',
debug: true,
maxRetries: 1,
query: {
token: headers['cookie'],
},
host: process.env.NEXT_PUBLIC_BACKEND_URL!,
onMessage: (message) => {
console.log(message);
},
});
return <></>;
};

View File

@@ -66,8 +66,6 @@ export function NavUser() {
trpc.connections.setDefault.mutationOptions(),
);
const { openBillingPortal, customer: billingCustomer } = useBilling();
const { mutateAsync: EnableBrain } = useMutation(trpc.brain.enableBrain.mutationOptions());
const { mutateAsync: DisableBrain } = useMutation(trpc.brain.disableBrain.mutationOptions());
const pathname = usePathname();
const searchParams = useSearchParams();
const queryClient = useQueryClient();
@@ -94,20 +92,6 @@ export function NavUser() {
toast.success('Connection ID copied to clipboard');
}, [session]);
const handleEnableBrain = useCallback(async () => {
// This takes too long, not waiting
const enabled = await EnableBrain({});
await refetchBrainState();
if (enabled) toast.success('Brain enabled successfully');
}, []);
const handleDisableBrain = useCallback(async () => {
// This takes too long, not waiting
const enabled = await DisableBrain({});
await refetchBrainState();
if (enabled) toast.success('Brain disabled');
}, []);
const activeAccount = useMemo(() => {
if (!session || !data) return null;
return data.connections?.find((connection) => connection.id === session.connectionId);
@@ -176,7 +160,7 @@ export function NavUser() {
src={activeAccount?.picture || undefined}
alt={activeAccount?.name || activeAccount?.email}
/>
<AvatarFallback className="rounded-[5px] text-[10px]">
{(activeAccount?.name || activeAccount?.email)
.split(' ')
@@ -531,22 +515,6 @@ export function NavUser() {
<p className="text-[13px] opacity-60">Clear Local Cache</p>
</div>
</DropdownMenuItem>
{!brainState?.enabled ? (
<DropdownMenuItem onClick={handleEnableBrain}>
<div className="flex items-center gap-2">
<BrainIcon size={16} className="opacity-60" />
<p className="text-[13px] opacity-60">Enable Auto Labeling</p>
</div>
</DropdownMenuItem>
) : null}
{brainState?.enabled ? (
<DropdownMenuItem onClick={handleDisableBrain}>
<div className="flex items-center gap-2">
<BrainIcon size={16} className="opacity-60" />
<p className="text-[13px] opacity-60">Disable Auto Labeling</p>
</div>
</DropdownMenuItem>
) : null}
</DropdownMenuContent>
</DropdownMenu>
</div>

View File

@@ -576,20 +576,22 @@ export function parseNaturalLanguageDate(query: string): { from?: Date; to?: Dat
return null;
}
export const categorySearchValues = [
'is:important NOT is:sent NOT is:draft',
'NOT is:draft (is:inbox OR (is:sent AND to:me))',
'is:personal NOT is:sent NOT is:draft',
'is:updates NOT is:sent NOT is:draft',
'is:promotions NOT is:sent NOT is:draft',
'is:unread NOT is:sent NOT is:draft',
];
export const cleanSearchValue = (q: string): string => {
if (!q) return '';
const filterRegex = new RegExp(
filterSuggestions
.map((s) => s.filter)
.filter(Boolean) // Remove any empty strings
.join('|'),
'gi', // Case insensitive
const escapedValues = categorySearchValues.map((value) =>
value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'),
);
return q
.replace(filterRegex, '')
.replace(/\s+/g, ' ') // Replace multiple spaces with single space
.replace(new RegExp(escapedValues.join('|'), 'g'), '')
.replace(/\s+/g, ' ')
.trim();
};

View File

@@ -97,6 +97,7 @@
"dompurify": "^3.2.5",
"drizzle-orm": "0.43.1",
"email-addresses": "5.0.0",
"emblor": "^1.4.8",
"flags": "^3.2.0",
"framer-motion": "^12.10.4",
"googleapis": "144.0.0",
@@ -116,6 +117,7 @@
"next-themes": "0.4.4",
"novel": "1.0.2",
"nuqs": "2.4.0",
"partysocket": "^1.1.4",
"pluralize": "^8.0.0",
"postgres": "3.4.5",
"posthog-js": "1.236.6",

View File

@@ -32,8 +32,10 @@
"googleapis": "^148.0.0",
"he": "^1.2.0",
"hono": "^4.7.8",
"hono-party": "^0.0.12",
"jsonrepair": "^3.12.0",
"mimetext": "^3.0.27",
"partyserver": "^0.0.71",
"react": "^19.1.0",
"resend": "^4.5.1",
"sanitize-html": "^2.16.0",

View File

@@ -16,7 +16,9 @@ import { getActiveDriver } from './driver/utils';
import { APIError } from 'better-auth/api';
import { redis, resend } from './services';
import type { HonoContext } from '../ctx';
import { env } from 'cloudflare:workers';
import { createDriver } from './driver';
import { createDb } from '@zero/db';
import { eq } from 'drizzle-orm';
const connectionHandlerHook = async (account: Account, c: HonoContext) => {
@@ -68,32 +70,7 @@ const connectionHandlerHook = async (account: Account, c: HonoContext) => {
};
export const createAuth = (c: HonoContext) => {
const cache = redis();
return betterAuth({
database: drizzleAdapter(c.var.db, { provider: 'pg' }),
secondaryStorage: {
get: async (key: string) => {
return (await cache.get(key)) ?? null;
},
set: async (key: string, value: string, ttl?: number) => {
if (ttl) await cache.set(key, value, { ex: ttl });
else await cache.set(key, value);
},
delete: async (key: string) => {
await cache.del(key);
},
},
advanced: {
ipAddress: {
disableIpTracking: true,
},
crossSubDomainCookies: {
enabled: true,
domain: c.env.COOKIE_DOMAIN,
},
},
baseURL: c.env.NEXT_PUBLIC_BACKEND_URL,
trustedOrigins: [c.env.NEXT_PUBLIC_APP_URL, c.env.NEXT_PUBLIC_BACKEND_URL],
user: {
deleteUser: {
enabled: true,
@@ -120,22 +97,6 @@ export const createAuth = (c: HonoContext) => {
},
},
},
session: {
cookieCache: {
enabled: true,
maxAge: 60 * 60 * 24 * 7, // 7 days
},
expiresIn: 60 * 60 * 24 * 7, // 7 days
updateAge: 60 * 60 * 24, // 1 day (every 1 day the session expiration is updated)
},
socialProviders: getSocialProviders(c.env as unknown as Record<string, string>),
account: {
accountLinking: {
enabled: true,
allowDifferentEmails: true,
trustedProviders: ['google', 'microsoft'],
},
},
databaseHooks: {
account: {
create: {
@@ -181,98 +142,6 @@ export const createAuth = (c: HonoContext) => {
});
},
},
plugins: [
customSession(async ({ user, session }) => {
const foundUser = await c.var.db.query.user.findFirst({
where: eq(_user.id, user.id),
});
let activeConnection = null;
if (foundUser?.defaultConnectionId) {
// Get the active connection details
const [connectionDetails] = await c.var.db
.select()
.from(connection)
.where(eq(connection.id, foundUser.defaultConnectionId))
.limit(1);
if (connectionDetails) {
activeConnection = {
id: connectionDetails.id,
name: connectionDetails.name,
email: connectionDetails.email,
picture: connectionDetails.picture,
};
} else {
await c.var.db
.update(_user)
.set({
defaultConnectionId: null,
})
.where(eq(_user.id, user.id));
}
}
if (!foundUser?.defaultConnectionId) {
const [defaultConnection] = await c.var.db
.select()
.from(connection)
.where(eq(connection.userId, user.id))
.limit(1);
if (defaultConnection) {
activeConnection = {
id: defaultConnection.id,
name: defaultConnection.name,
email: defaultConnection.email,
picture: defaultConnection.picture,
};
}
if (!defaultConnection) {
// find the user account the user has
const [userAccount] = await c.var.db
.select()
.from(account)
.where(eq(account.userId, user.id))
.limit(1);
if (userAccount) {
const newConnectionId = crypto.randomUUID();
// create a new connection
const [newConnection] = await c.var.db.insert(connection).values({
id: newConnectionId,
userId: user.id,
email: user.email,
name: user.name,
picture: user.image,
accessToken: userAccount.accessToken,
refreshToken: userAccount.refreshToken,
scope: userAccount.scope,
providerId: userAccount.providerId,
expiresAt: new Date(
Date.now() + (userAccount.accessTokenExpiresAt?.getTime() || 3600000),
),
createdAt: new Date(),
updatedAt: new Date(),
} as typeof connection.$inferInsert);
// this type error is pissing me tf off
if (newConnection) {
// void enableBrainFunction({ id: newConnectionId, providerId: userAccount.providerId });
console.warn('Created new connection for user', user.email);
}
}
}
}
return {
connectionId: activeConnection?.id || null,
activeConnection,
user,
session,
};
}),
],
hooks: {
after: createAuthMiddleware(async (ctx) => {
// all hooks that run on sign-up routes
@@ -311,7 +180,152 @@ export const createAuth = (c: HonoContext) => {
}
}),
},
...createAuthConfig(),
});
};
const createAuthConfig = () => {
const cache = redis();
const db = createDb(env.HYPERDRIVE.connectionString);
return {
database: drizzleAdapter(db, { provider: 'pg' }),
secondaryStorage: {
get: async (key: string) => {
return ((await cache.get(key)) as string) ?? null;
},
set: async (key: string, value: string, ttl?: number) => {
if (ttl) await cache.set(key, value, { ex: ttl });
else await cache.set(key, value);
},
delete: async (key: string) => {
await cache.del(key);
},
},
advanced: {
ipAddress: {
disableIpTracking: true,
},
crossSubDomainCookies: {
enabled: true,
domain: env.COOKIE_DOMAIN,
},
},
baseURL: env.NEXT_PUBLIC_BACKEND_URL,
trustedOrigins: [env.NEXT_PUBLIC_APP_URL, env.NEXT_PUBLIC_BACKEND_URL],
session: {
cookieCache: {
enabled: true,
maxAge: 60 * 60 * 24 * 7, // 7 days
},
expiresIn: 60 * 60 * 24 * 7, // 7 days
updateAge: 60 * 60 * 24, // 1 day (every 1 day the session expiration is updated)
},
socialProviders: getSocialProviders(env as unknown as Record<string, string>),
account: {
accountLinking: {
enabled: true,
allowDifferentEmails: true,
trustedProviders: ['google', 'microsoft'],
},
},
plugins: [
customSession(async ({ user, session }) => {
const foundUser = await db.query.user.findFirst({
where: eq(_user.id, user.id),
});
let activeConnection = null;
if (foundUser?.defaultConnectionId) {
// Get the active connection details
const [connectionDetails] = await db
.select()
.from(connection)
.where(eq(connection.id, foundUser.defaultConnectionId))
.limit(1);
if (connectionDetails) {
activeConnection = {
id: connectionDetails.id,
name: connectionDetails.name,
email: connectionDetails.email,
picture: connectionDetails.picture,
};
} else {
await db
.update(_user)
.set({
defaultConnectionId: null,
})
.where(eq(_user.id, user.id));
}
}
if (!foundUser?.defaultConnectionId) {
const [defaultConnection] = await db
.select()
.from(connection)
.where(eq(connection.userId, user.id))
.limit(1);
if (defaultConnection) {
activeConnection = {
id: defaultConnection.id,
name: defaultConnection.name,
email: defaultConnection.email,
picture: defaultConnection.picture,
};
}
if (!defaultConnection) {
// find the user account the user has
const [userAccount] = await db
.select()
.from(account)
.where(eq(account.userId, user.id))
.limit(1);
if (userAccount) {
const newConnectionId = crypto.randomUUID();
// create a new connection
const [newConnection] = await db.insert(connection).values({
id: newConnectionId,
userId: user.id,
email: user.email,
name: user.name,
picture: user.image,
accessToken: userAccount.accessToken,
refreshToken: userAccount.refreshToken,
scope: userAccount.scope,
providerId: userAccount.providerId,
expiresAt: new Date(
Date.now() + (userAccount.accessTokenExpiresAt?.getTime() || 3600000),
),
createdAt: new Date(),
updatedAt: new Date(),
} as typeof connection.$inferInsert);
// this type error is pissing me tf off
if (newConnection) {
// void enableBrainFunction({ id: newConnectionId, providerId: userAccount.providerId });
console.warn('Created new connection for user', user.email);
}
}
}
}
return {
connectionId: activeConnection?.id || null,
activeConnection,
user,
session,
};
}),
],
};
};
export const createSimpleAuth = () => {
return betterAuth(createAuthConfig());
};
export type Auth = ReturnType<typeof createAuth>;
export type SimpleAuth = ReturnType<typeof createSimpleAuth>;

View File

@@ -1,23 +1,15 @@
import { env } from 'cloudflare:workers';
export const enableBrainFunction = async (connection: { id: string; providerId: string }) => {
return await fetch(env.BRAIN_URL + `/subscribe/${connection.providerId}`, {
body: JSON.stringify({
connectionId: connection.id,
}),
method: 'PUT',
})
.then(() => true)
.catch(() => false);
return await env.zero.subscribe({
connectionId: connection.id,
providerId: connection.providerId,
});
};
export const disableBrainFunction = async (connection: { id: string; providerId: string }) => {
return await fetch(env.BRAIN_URL + `/unsubscribe/${connection.providerId}`, {
body: JSON.stringify({
connectionId: connection.id,
}),
method: 'PUT',
})
.then(() => true)
.catch(() => false);
return await env.zero.unsubscribe({
connectionId: connection.id,
providerId: connection.providerId,
});
};

View File

@@ -468,7 +468,7 @@ export class GoogleMailManager implements MailManager {
return this.withErrorHandler(
'listDrafts',
async () => {
const { q: normalizedQ } = this.normalizeSearch('', q ?? '');
const { q: normalizedQ } = this.normalizeSearch('draft', q ?? '');
const res = await this.gmail.users.drafts.list({
userId: 'me',
q: normalizedQ ? normalizedQ : undefined,
@@ -721,14 +721,20 @@ export class GoogleMailManager implements MailManager {
private normalizeSearch(folder: string, q: string) {
if (folder !== 'inbox') {
q = cleanSearchValue(q);
if (folder === 'bin') {
return { folder: undefined, q: `in:trash ${q}` };
}
if (folder === 'archive') {
return { folder: undefined, q: `in:archive ${q}` };
return { folder: undefined, q: `in:archive AND (${q})` };
}
return { folder, q: `in:${folder} ${q}` };
if (folder === 'draft') {
return { folder: undefined, q: `is:draft AND (${q})` };
}
return { folder, q: folder.trim().length ? `in:${folder} ${q}` : q };
}
return { folder, q };
}
private parse({

View File

@@ -31,18 +31,14 @@ export const getActiveDriver = async (c: HonoContext) => {
if (!activeConnection || !activeConnection.accessToken || !activeConnection.refreshToken)
throw new Error('Invalid connection');
return createDriver(
activeConnection.providerId,
{
auth: {
accessToken: activeConnection.accessToken,
refreshToken: activeConnection.refreshToken,
email: activeConnection.email,
},
c,
return createDriver(activeConnection.providerId, {
auth: {
accessToken: activeConnection.accessToken,
refreshToken: activeConnection.refreshToken,
email: activeConnection.email,
},
c.env,
);
c,
});
};
export const fromBase64Url = (str: string) => str.replace(/-/g, '+').replace(/_/g, '/');

View File

@@ -80,6 +80,11 @@ export const filterSuggestions: FilterSuggestion[] = [
filter: 'before:date',
description: 'Emails before a specific date',
},
{
prefix: 'draft',
filter: '',
description: 'Emails in the draft folder',
},
];
export const getFilterSuggestionGridColumns = (count: number, isMobile: boolean): string => {

View File

@@ -0,0 +1,34 @@
import { Server, type Connection, type ConnectionContext } from 'partyserver';
import { createSimpleAuth, type SimpleAuth } from './auth';
const parseHeaders = (token: string) => {
const headers = new Headers();
headers.set('Cookie', token);
return headers;
};
export class DurableMailbox extends Server<Env> {
auth: SimpleAuth;
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
this.auth = createSimpleAuth();
}
private async getSession(token: string) {
const session = await this.auth.api.getSession({ headers: parseHeaders(token) });
return session;
}
async onConnect(connection: Connection, ctx: ConnectionContext) {
const url = new URL(ctx.request.url);
const token = url.searchParams.get('token');
if (token) {
const session = await this.getSession(token);
if (session) {
this.ctx.storage.put('email', session.user.email);
} else {
console.log('No session', token);
}
}
}
}

View File

@@ -6,7 +6,19 @@ import { and, eq } from 'drizzle-orm';
export const getActiveConnection = async (c: HonoContext) => {
const { session, db } = c.var;
if (!session?.user) throw new Error('Session Not Found');
if (!session.activeConnection?.id) throw new Error('No active connection found for the user');
if (!session.activeConnection?.id) {
const activeConnection = await db.query.connection.findFirst({
where: and(eq(connection.userId, session.user.id)),
});
if (!activeConnection)
throw new Error(`Active connection not found for user ${session.user.id}`);
if (!activeConnection.refreshToken || !activeConnection.accessToken)
throw new Error(
'Active Connection is not properly authorized, please reconnect the connection',
);
return activeConnection;
}
const activeConnection = await db.query.connection.findFirst({
where: and(

View File

@@ -468,19 +468,22 @@ export function parseNaturalLanguageDate(query: string): { from?: Date; to?: Dat
return null;
}
// Duplicated on server & client
export const categorySearchValues = [
'is:important NOT is:sent NOT is:draft',
'NOT is:draft (is:inbox OR (is:sent AND to:me))',
'is:personal NOT is:sent NOT is:draft',
'is:updates NOT is:sent NOT is:draft',
'is:promotions NOT is:sent NOT is:draft',
'is:unread NOT is:sent NOT is:draft',
];
export const cleanSearchValue = (q: string): string => {
if (!q) return '';
const filterRegex = new RegExp(
filterSuggestions
.map((s) => s.filter)
.filter(Boolean) // Remove any empty strings
.join('|'),
'gi', // Case insensitive
const escapedValues = categorySearchValues.map((value) =>
value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'),
);
return q
.replace(filterRegex, '')
.replace(/\s+/g, ' ') // Replace multiple spaces with single space
.replace(new RegExp(escapedValues.join('|'), 'g'), '')
.replace(/\s+/g, ' ')
.trim();
};

View File

@@ -1,19 +1,24 @@
import { env, WorkerEntrypoint } from 'cloudflare:workers';
import { mailtoHandler } from './routes/mailto-handler';
import type { HonoContext, HonoVariables } from './ctx';
import { routePartykitRequest } from 'partyserver';
import { partyserverMiddleware } from 'hono-party';
import { trpcServer } from '@hono/trpc-server';
import { DurableMailbox } from './lib/party';
import { chatHandler } from './routes/chat';
import { env } from 'cloudflare:workers';
import { createAuth } from './lib/auth';
import { createDb } from '@zero/db';
import { appRouter } from './trpc';
import { cors } from 'hono/cors';
import { Hono } from 'hono';
export { DurableMailbox };
const api = new Hono<{ Variables: HonoVariables; Bindings: Env }>()
.use(
'*',
cors({
origin: (_, c: HonoContext) => c.env.NEXT_PUBLIC_APP_URL,
origin: (_, c: HonoContext) => env.NEXT_PUBLIC_APP_URL,
credentials: true,
allowHeaders: ['Content-Type', 'Authorization'],
}),
@@ -56,9 +61,36 @@ const api = new Hono<{ Variables: HonoVariables; Bindings: Env }>()
const app = new Hono<{ Variables: HonoVariables; Bindings: Env }>()
.route('/api', api)
.get('/health', (c) => c.json({ message: 'Zero Server is Up!' }))
.get('/:path{.+}', (c) => {
const path = c.req.param('path');
return c.redirect(`${c.env.NEXT_PUBLIC_APP_URL}/${path}`);
});
.get('/', (c) => {
return c.redirect(`${env.NEXT_PUBLIC_APP_URL}`);
})
.use(
'*',
partyserverMiddleware({
onError(error) {
console.log('Error in party middleware:', error);
},
options: {
prefix: 'zero',
},
}),
);
export default app;
export default class extends WorkerEntrypoint {
fetch(request: Request): Response | Promise<Response> {
if (request.url.includes('/zero/durable-mailbox')) {
return routePartykitRequest(request, env as any, {
prefix: 'zero',
}) as Promise<Response>;
}
return app.fetch(request);
}
public notifyUser({ email }: { email: string }) {
const durableObject = env.DURABLE_MAILBOX.idFromString(`${email}:general`);
if (env.DURABLE_MAILBOX.get(durableObject)) {
const stub = env.DURABLE_MAILBOX.get(durableObject);
if (stub) stub.broadcast(`HELLO ${email}`);
}
}
}

View File

@@ -34,7 +34,6 @@ export const brainRouter = router({
.mutation(async ({ ctx, input }) => {
let { connection } = input;
if (!connection) connection = ctx.activeConnection;
if (!ctx.brainServerAvailable) return false;
return await enableBrainFunction(connection);
}),
disableBrain: activeConnectionProcedure
@@ -52,7 +51,6 @@ export const brainRouter = router({
.mutation(async ({ ctx, input }) => {
let { connection } = input;
if (!connection) connection = ctx.activeConnection;
if (!ctx.brainServerAvailable) return false;
return await disableBrainFunction(connection);
}),
@@ -78,4 +76,24 @@ export const brainRouter = router({
const limit = await getConnectionLimit(connection.id);
return { limit, enabled: true };
}),
getLabels: activeConnectionProcedure
.use(brainServerAvailableMiddleware)
.output(z.array(z.string()))
.query(async ({ ctx }) => {
const connection = ctx.activeConnection;
const labels = await env.connection_labels.get(connection.id);
return labels?.split(',') ?? [];
}),
updateLabels: activeConnectionProcedure
.use(brainServerAvailableMiddleware)
.input(
z.object({
labels: z.array(z.string()),
}),
)
.mutation(async ({ ctx, input }) => {
const connection = ctx.activeConnection;
await env.connection_labels.put(connection.id, input.labels.join(','));
return { success: true };
}),
});

View File

@@ -44,6 +44,7 @@ export const mailRouter = router({
.query(async ({ ctx, input }) => {
const { folder, max, cursor, q } = input;
const { driver } = ctx;
if (folder === FOLDERS.DRAFT) {
const drafts = await driver.listDrafts({ q, maxResults: max, pageToken: cursor });
return drafts;

View File

@@ -24,13 +24,16 @@ export const privateProcedure = publicProcedure.use(async ({ ctx, next }) => {
});
export const activeConnectionProcedure = privateProcedure.use(async ({ ctx, next }) => {
const activeConnection = await getActiveConnection(ctx.c).catch((err: unknown) => {
try {
const activeConnection = await getActiveConnection(ctx.c);
return next({ ctx: { ...ctx, activeConnection } });
} catch (err) {
await ctx.c.var.auth.api.signOut({ headers: ctx.c.req.raw.headers });
throw new TRPCError({
code: 'BAD_REQUEST',
message: err instanceof Error ? err.message : 'Failed to get active connection',
});
});
return next({ ctx: { ...ctx, activeConnection } });
}
});
export const activeDriverProcedure = activeConnectionProcedure.use(async ({ ctx, next }) => {

View File

@@ -6,6 +6,29 @@
"main": "src/main.ts",
"env": {
"staging": {
"limits": {
"cpu_ms": 300000,
},
"durable_objects": {
"bindings": [
{
"class_name": "DurableMailbox",
"name": "DURABLE_MAILBOX",
},
],
},
"migrations": [
{
"tag": "v1",
"new_classes": ["DurableMailbox"],
},
],
"services": [
{
"binding": "zero",
"service": "zero-worker",
},
],
"observability": {
"enabled": true,
},

147
bun.lock
View File

@@ -105,7 +105,9 @@
"dompurify": "^3.2.5",
"drizzle-orm": "0.43.1",
"email-addresses": "5.0.0",
"emblor": "^1.4.8",
"flags": "^3.2.0",
"framer-motion": "^12.10.4",
"googleapis": "144.0.0",
"he": "1.2.0",
"hono": "^4.7.7",
@@ -123,6 +125,7 @@
"next-themes": "0.4.4",
"novel": "1.0.2",
"nuqs": "2.4.0",
"partysocket": "^1.1.4",
"pluralize": "^8.0.0",
"postgres": "3.4.5",
"posthog-js": "1.236.6",
@@ -209,8 +212,10 @@
"googleapis": "^148.0.0",
"he": "^1.2.0",
"hono": "^4.7.8",
"hono-party": "^0.0.12",
"jsonrepair": "^3.12.0",
"mimetext": "^3.0.27",
"partyserver": "^0.0.71",
"react": "^19.1.0",
"resend": "^4.5.1",
"sanitize-html": "^2.16.0",
@@ -313,6 +318,8 @@
"@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20250507.0", "", { "os": "win32", "cpu": "x64" }, "sha512-c91fhNP8ufycdIDqjVyKTqeb4ewkbAYXFQbLreMVgh4LLQQPDDEte8wCdmaFy5bIL0M9d85PpdCq51RCzq/FaQ=="],
"@cloudflare/workers-types": ["@cloudflare/workers-types@4.20250510.0", "", {}, "sha512-VLdSYUooX2QhdlzyBnnLAqa5B3xWyr5vdvya9NZk2BJNmRt2iblSLunj7iBKiW9J+SIBHz7c+kUzUJKoFLKRjg=="],
"@coinbase/cookie-manager": ["@coinbase/cookie-manager@1.1.8", "", { "dependencies": { "js-cookie": "^3.0.5" } }, "sha512-D57yF4uUuiKjfYRpBZFsGj+ME0dICS14K8JnoexVA/9QY+/W97ORz5tXG53p5upwuPLOBqte8ZoWJa+hBgGCSQ=="],
"@cspotcode/source-map-support": ["@cspotcode/source-map-support@0.8.1", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.9" } }, "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw=="],
@@ -1087,6 +1094,8 @@
"array-includes": ["array-includes@3.1.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.4", "is-string": "^1.0.7" } }, "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ=="],
"array-move": ["array-move@3.0.1", "", {}, "sha512-H3Of6NIn2nNU1gsVDqDnYKY/LCdWvCMMOWifNGhKcVQgiZ6nOek39aESOvro6zmueP07exSl93YLvkN4fZOkSg=="],
"array.prototype.findlast": ["array.prototype.findlast@1.2.5", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "es-shim-unscopables": "^1.0.2" } }, "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ=="],
"array.prototype.findlastindex": ["array.prototype.findlastindex@1.2.6", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-shim-unscopables": "^1.1.0" } }, "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ=="],
@@ -1365,6 +1374,8 @@
"email-addresses": ["email-addresses@5.0.0", "", {}, "sha512-4OIPYlA6JXqtVn8zpHpGiI7vE6EQOAg16aGnDMIAlZVinnoZ8208tW1hAbjWydgN/4PLTT9q+O1K6AH/vALJGw=="],
"emblor": ["emblor@1.4.8", "", { "dependencies": { "@radix-ui/react-dialog": "1.0.4", "@radix-ui/react-popover": "^1.0.7", "@radix-ui/react-slot": "^1.0.2", "class-variance-authority": "latest", "clsx": "latest", "cmdk": "^0.2.0", "react-easy-sort": "^1.6.0", "tailwind-merge": "latest" }, "peerDependencies": { "react": "^18.0.0", "react-dom": "^18.0.0" } }, "sha512-Vqtz4Gepa7CIkmplQ+kvJnsSZJ4sAyHvQqqX2iCmgoRo5iRQFxr+5FJkk6QuLVNH5vrbBZEYxg7sMZuDCnQ/PQ=="],
"emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
"encoding-sniffer": ["encoding-sniffer@0.2.0", "", { "dependencies": { "iconv-lite": "^0.6.3", "whatwg-encoding": "^3.1.1" } }, "sha512-ju7Wq1kg04I3HtiYIOrUrdfdDvkyO9s5XM8QAj/bN61Yo/Vb4vgJxy5vi4Yxk01gWHbrofpPtpxM8bKger9jhg=="],
@@ -1435,6 +1446,8 @@
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
"event-target-polyfill": ["event-target-polyfill@0.0.4", "", {}, "sha512-Gs6RLjzlLRdT8X9ZipJdIZI/Y6/HhRLyq9RdDlCsnpxr/+Nn6bU2EFGuC94GjxqhM+Nmij2Vcq98yoHrU8uNFQ=="],
"event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="],
"eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="],
@@ -1489,7 +1502,7 @@
"formdata-node": ["formdata-node@4.4.1", "", { "dependencies": { "node-domexception": "1.0.0", "web-streams-polyfill": "4.0.0-beta.3" } }, "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ=="],
"framer-motion": ["framer-motion@12.9.2", "", { "dependencies": { "motion-dom": "^12.9.1", "motion-utils": "^12.8.3", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-R0O3Jdqbfwywpm45obP+8sTgafmdEcUoShQTAV+rB5pi+Y1Px/FYL5qLLRe5tPtBdN1J4jos7M+xN2VV2oEAbQ=="],
"framer-motion": ["framer-motion@12.10.5", "", { "dependencies": { "motion-dom": "^12.10.5", "motion-utils": "^12.9.4", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-p6VF1YkwWvNDFzg5IQ5lqPx11Td4TQ6LqDnshV7sWj0Nrp4dwz2/aEzmgh9WA9ridcTIJ625Fr0oiuhgqIoFwQ=="],
"framework-utils": ["framework-utils@1.1.0", "", {}, "sha512-KAfqli5PwpFJ8o3psRNs8svpMGyCSAe8nmGcjQ0zZBWN2H6dZDnq+ABp3N3hdUmFeMrLtjOCTXD4yplUJIWceg=="],
@@ -1573,6 +1586,8 @@
"hono": ["hono@4.7.8", "", {}, "sha512-PCibtFdxa7/Ldud9yddl1G81GjYaeMYYTq4ywSaNsYbB1Lug4mwtOMJf2WXykL0pntYwmpRJeOI3NmoDgD+Jxw=="],
"hono-party": ["hono-party@0.0.12", "", { "peerDependencies": { "@cloudflare/workers-types": "^4.20240729.0", "hono": "^4.6.17", "partyserver": "^0.0.71" } }, "sha512-mvjPDel2GcI93dJ6sreCn3Jx+ta3aw4JFGWwwNYwK7wYpjJG8pqiwVhEnbcneXl+TPheyq0uykQpgNVkruF1Yg=="],
"html-entities": ["html-entities@2.6.0", "", {}, "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ=="],
"html-to-text": ["html-to-text@9.0.5", "", { "dependencies": { "@selderee/plugin-htmlparser2": "^0.11.0", "deepmerge": "^4.3.1", "dom-serializer": "^2.0.0", "htmlparser2": "^8.0.2", "selderee": "^0.11.0" } }, "sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg=="],
@@ -1893,9 +1908,9 @@
"motion": ["motion@12.4.7", "", { "dependencies": { "framer-motion": "^12.4.7", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-mhegHAbf1r80fr+ytC6OkjKvIUegRNXKLWNPrCN2+GnixlNSPwT03FtKqp9oDny1kNcLWZvwbmEr+JqVryFrcg=="],
"motion-dom": ["motion-dom@12.9.1", "", { "dependencies": { "motion-utils": "^12.8.3" } }, "sha512-xqXEwRLDYDTzOgXobSoWtytRtGlf7zdkRfFbrrdP7eojaGQZ5Go4OOKtgnx7uF8sAkfr1ZjMvbCJSCIT2h6fkQ=="],
"motion-dom": ["motion-dom@12.10.5", "", { "dependencies": { "motion-utils": "^12.9.4" } }, "sha512-F7XKmhxXEH/y3aWWf0N2w69wNSN+6PcJ1seqR1WolClmXpPhj+xwzs9j5CpsMFzeHR1D7irl3JcWMToPRwX6Hg=="],
"motion-utils": ["motion-utils@12.8.3", "", {}, "sha512-GYVauZEbca8/zOhEiYOY9/uJeedYQld6co/GJFKOy//0c/4lDqk0zB549sBYqqV2iMuX+uHrY1E5zd8A2L+1Lw=="],
"motion-utils": ["motion-utils@12.9.4", "", {}, "sha512-BW3I65zeM76CMsfh3kHid9ansEJk9Qvl+K5cu4DVHKGsI52n76OJ4z2CUJUV+Mn3uEP9k1JJA3tClG0ggSrRcg=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
@@ -2007,6 +2022,10 @@
"parseley": ["parseley@0.12.1", "", { "dependencies": { "leac": "^0.6.0", "peberminta": "^0.9.0" } }, "sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw=="],
"partyserver": ["partyserver@0.0.71", "", { "dependencies": { "nanoid": "^5.1.5" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20240729.0" } }, "sha512-PJZoX08tyNcNJVXqWJedZ6Jzj8EOFGBA/PJ37KhAnWmTkq6A8SqA4u2ol+zq8zwSfRy9FPvVgABCY0yLpe62Dg=="],
"partysocket": ["partysocket@1.1.4", "", { "dependencies": { "event-target-polyfill": "^0.0.4" } }, "sha512-jXP7PFj2h5/v4UjDS8P7MZy6NJUQ7sspiFyxL4uc/+oKOL+KdtXzHnTV8INPGxBrLTXgalyG3kd12Qm7WrYc3A=="],
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
@@ -2139,6 +2158,8 @@
"react-dom": ["react-dom@19.0.0", "", { "dependencies": { "scheduler": "^0.25.0" }, "peerDependencies": { "react": "^19.0.0" } }, "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ=="],
"react-easy-sort": ["react-easy-sort@1.6.0", "", { "dependencies": { "array-move": "^3.0.1", "tslib": "2.0.1" }, "peerDependencies": { "react": ">=16.4.0", "react-dom": ">=16.4.0" } }, "sha512-zd9Nn90wVlZPEwJrpqElN87sf9GZnFR1StfjgNQVbSpR5QTSzCHjEYK6REuwq49Ip+76KOMSln9tg/ST2KLelg=="],
"react-hook-form": ["react-hook-form@7.54.2", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-eHpAUgUjWbZocoQYUHposymRb4ZP6d0uwUnooL2uOybA9/3tPUvoAKqEWK1WaSiTxxOfTpffNZP7QwlnM3/gEg=="],
"react-hotkeys-hook": ["react-hotkeys-hook@5.0.1", "", { "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-TysTwXrUSj6QclMZIEoxCfvy/6EsoZcrfE970aUVa9fO3c3vcms+IVjv3ljbhUPM/oY1iEoun7O2W8v8INl5hw=="],
@@ -2927,6 +2948,14 @@
"editorconfig/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
"emblor/@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.0.4", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/primitive": "1.0.1", "@radix-ui/react-compose-refs": "1.0.1", "@radix-ui/react-context": "1.0.1", "@radix-ui/react-dismissable-layer": "1.0.4", "@radix-ui/react-focus-guards": "1.0.1", "@radix-ui/react-focus-scope": "1.0.3", "@radix-ui/react-id": "1.0.1", "@radix-ui/react-portal": "1.0.3", "@radix-ui/react-presence": "1.0.1", "@radix-ui/react-primitive": "1.0.3", "@radix-ui/react-slot": "1.0.2", "@radix-ui/react-use-controllable-state": "1.0.1", "aria-hidden": "^1.1.1", "react-remove-scroll": "2.5.5" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-hJtRy/jPULGQZceSAP2Re6/4NpKo8im6V8P2hUqZsdFiSL8l35kYsw3qbRI6Ay5mQd2+wlLqje770eq+RJ3yZg=="],
"emblor/cmdk": ["cmdk@0.2.1", "", { "dependencies": { "@radix-ui/react-dialog": "1.0.0" }, "peerDependencies": { "react": "^18.0.0", "react-dom": "^18.0.0" } }, "sha512-U6//9lQ6JvT47+6OF6Gi8BvkxYQ8SCRRSKIJkthIMsFsLZRG0cKvTtuTaefyIKMQb8rvvXy0wGdpTNq/jPtm+g=="],
"emblor/react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="],
"emblor/tailwind-merge": ["tailwind-merge@3.2.0", "", {}, "sha512-FQT/OVqCD+7edmmJpsgCsY820RTD5AkBryuG5IUqR5YQZSdj5xlH5nLgH7YPths7WsLPSpSBNneJdM8aS8aeFA=="],
"eslint/@eslint/eslintrc": ["@eslint/eslintrc@3.3.1", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ=="],
"eslint-config-next/eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@4.6.2", "", { "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" } }, "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ=="],
@@ -2977,6 +3006,8 @@
"miniflare/zod": ["zod@3.22.3", "", {}, "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug=="],
"motion/framer-motion": ["framer-motion@12.9.2", "", { "dependencies": { "motion-dom": "^12.9.1", "motion-utils": "^12.8.3", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-R0O3Jdqbfwywpm45obP+8sTgafmdEcUoShQTAV+rB5pi+Y1Px/FYL5qLLRe5tPtBdN1J4jos7M+xN2VV2oEAbQ=="],
"next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],
"novel/@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.2", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ=="],
@@ -2997,6 +3028,8 @@
"parse5/entities": ["entities@6.0.0", "", {}, "sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw=="],
"partyserver/nanoid": ["nanoid@5.1.5", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw=="],
"postcss-import/resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="],
"postcss-nested/postcss-selector-parser": ["postcss-selector-parser@6.1.2", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg=="],
@@ -3005,6 +3038,8 @@
"prosemirror-markdown/@types/markdown-it": ["@types/markdown-it@14.1.2", "", { "dependencies": { "@types/linkify-it": "^5", "@types/mdurl": "^2" } }, "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog=="],
"react-easy-sort/tslib": ["tslib@2.0.1", "", {}, "sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ=="],
"react-promise-suspense/fast-deep-equal": ["fast-deep-equal@2.0.1", "", {}, "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w=="],
"react-use/js-cookie": ["js-cookie@2.2.1", "", {}, "sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ=="],
@@ -3297,6 +3332,34 @@
"editorconfig/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
"emblor/@radix-ui/react-dialog/@radix-ui/primitive": ["@radix-ui/primitive@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10" } }, "sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw=="],
"emblor/@radix-ui/react-dialog/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react"] }, "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw=="],
"emblor/@radix-ui/react-dialog/@radix-ui/react-context": ["@radix-ui/react-context@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react"] }, "sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg=="],
"emblor/@radix-ui/react-dialog/@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.0.4", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/primitive": "1.0.1", "@radix-ui/react-compose-refs": "1.0.1", "@radix-ui/react-primitive": "1.0.3", "@radix-ui/react-use-callback-ref": "1.0.1", "@radix-ui/react-use-escape-keydown": "1.0.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7UpBa/RKMoHJYjie1gkF1DlK8l1fdU/VKDpoS3rCCo8YBJR294GwcEHyxHw72yvphJ7ld0AXEcSLAzY2F/WyCg=="],
"emblor/@radix-ui/react-dialog/@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react"] }, "sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA=="],
"emblor/@radix-ui/react-dialog/@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.0.3", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-compose-refs": "1.0.1", "@radix-ui/react-primitive": "1.0.3", "@radix-ui/react-use-callback-ref": "1.0.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-upXdPfqI4islj2CslyfUBNlaJCPybbqRHAi1KER7Isel9Q2AtSJ0zRBZv8mWQiFXD2nyAJ4BhC3yXgZ6kMBSrQ=="],
"emblor/@radix-ui/react-dialog/@radix-ui/react-id": ["@radix-ui/react-id@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-use-layout-effect": "1.0.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react"] }, "sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ=="],
"emblor/@radix-ui/react-dialog/@radix-ui/react-portal": ["@radix-ui/react-portal@1.0.3", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-primitive": "1.0.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-xLYZeHrWoPmA5mEKEfZZevoVRK/Q43GfzRXkWV6qawIWWK8t6ifIiLQdd7rmQ4Vk1bmI21XhqF9BN3jWf+phpA=="],
"emblor/@radix-ui/react-dialog/@radix-ui/react-presence": ["@radix-ui/react-presence@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-compose-refs": "1.0.1", "@radix-ui/react-use-layout-effect": "1.0.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg=="],
"emblor/@radix-ui/react-dialog/@radix-ui/react-primitive": ["@radix-ui/react-primitive@1.0.3", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-slot": "1.0.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g=="],
"emblor/@radix-ui/react-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.0.2", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-compose-refs": "1.0.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react"] }, "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg=="],
"emblor/@radix-ui/react-dialog/@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-use-callback-ref": "1.0.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react"] }, "sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA=="],
"emblor/@radix-ui/react-dialog/react-remove-scroll": ["react-remove-scroll@2.5.5", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.3", "react-style-singleton": "^2.2.1", "tslib": "^2.1.0", "use-callback-ref": "^1.3.0", "use-sidecar": "^1.1.2" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw=="],
"emblor/cmdk/@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/primitive": "1.0.0", "@radix-ui/react-compose-refs": "1.0.0", "@radix-ui/react-context": "1.0.0", "@radix-ui/react-dismissable-layer": "1.0.0", "@radix-ui/react-focus-guards": "1.0.0", "@radix-ui/react-focus-scope": "1.0.0", "@radix-ui/react-id": "1.0.0", "@radix-ui/react-portal": "1.0.0", "@radix-ui/react-presence": "1.0.0", "@radix-ui/react-primitive": "1.0.0", "@radix-ui/react-slot": "1.0.0", "@radix-ui/react-use-controllable-state": "1.0.0", "aria-hidden": "^1.1.1", "react-remove-scroll": "2.5.4" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" } }, "sha512-Yn9YU+QlHYLWwV1XfKiqnGVpWYWk6MeBVM6x/bcoyPvxgjQGoeT35482viLPctTMWoMw0PoHgqfSox7Ig+957Q=="],
"gel/which/isexe": ["isexe@3.1.1", "", {}, "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ=="],
"glob/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
@@ -3307,6 +3370,10 @@
"js-beautify/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
"motion/framer-motion/motion-dom": ["motion-dom@12.9.1", "", { "dependencies": { "motion-utils": "^12.8.3" } }, "sha512-xqXEwRLDYDTzOgXobSoWtytRtGlf7zdkRfFbrrdP7eojaGQZ5Go4OOKtgnx7uF8sAkfr1ZjMvbCJSCIT2h6fkQ=="],
"motion/framer-motion/motion-utils": ["motion-utils@12.8.3", "", {}, "sha512-GYVauZEbca8/zOhEiYOY9/uJeedYQld6co/GJFKOy//0c/4lDqk0zB549sBYqqV2iMuX+uHrY1E5zd8A2L+1Lw=="],
"novel/@radix-ui/react-slot/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw=="],
"novel/cmdk/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw=="],
@@ -3485,6 +3552,62 @@
"cmdk/@radix-ui/react-primitive/@radix-ui/react-slot/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react"] }, "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw=="],
"emblor/@radix-ui/react-dialog/@radix-ui/react-compose-refs/react": ["react@19.0.0", "", {}, "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ=="],
"emblor/@radix-ui/react-dialog/@radix-ui/react-context/react": ["react@19.0.0", "", {}, "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ=="],
"emblor/@radix-ui/react-dialog/@radix-ui/react-dismissable-layer/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react"] }, "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ=="],
"emblor/@radix-ui/react-dialog/@radix-ui/react-dismissable-layer/@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.0.3", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-use-callback-ref": "1.0.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react"] }, "sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg=="],
"emblor/@radix-ui/react-dialog/@radix-ui/react-focus-guards/react": ["react@19.0.0", "", {}, "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ=="],
"emblor/@radix-ui/react-dialog/@radix-ui/react-focus-scope/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react"] }, "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ=="],
"emblor/@radix-ui/react-dialog/@radix-ui/react-id/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react"] }, "sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ=="],
"emblor/@radix-ui/react-dialog/@radix-ui/react-id/react": ["react@19.0.0", "", {}, "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ=="],
"emblor/@radix-ui/react-dialog/@radix-ui/react-presence/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react"] }, "sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ=="],
"emblor/@radix-ui/react-dialog/@radix-ui/react-presence/react": ["react@19.0.0", "", {}, "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ=="],
"emblor/@radix-ui/react-dialog/@radix-ui/react-primitive/react": ["react@19.0.0", "", {}, "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ=="],
"emblor/@radix-ui/react-dialog/@radix-ui/react-slot/react": ["react@19.0.0", "", {}, "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ=="],
"emblor/@radix-ui/react-dialog/@radix-ui/react-use-controllable-state/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react"] }, "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ=="],
"emblor/@radix-ui/react-dialog/@radix-ui/react-use-controllable-state/react": ["react@19.0.0", "", {}, "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ=="],
"emblor/@radix-ui/react-dialog/react-remove-scroll/react": ["react@19.0.0", "", {}, "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ=="],
"emblor/cmdk/@radix-ui/react-dialog/@radix-ui/primitive": ["@radix-ui/primitive@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" } }, "sha512-3e7rn8FDMin4CgeL7Z/49smCA3rFYY3Ha2rUQ7HRWFadS5iCRw08ZgVT1LaNTCNqgvrUiyczLflrVrF0SRQtNA=="],
"emblor/cmdk/@radix-ui/react-dialog/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA=="],
"emblor/cmdk/@radix-ui/react-dialog/@radix-ui/react-context": ["@radix-ui/react-context@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-1pVM9RfOQ+n/N5PJK33kRSKsr1glNxomxONs5c49MliinBY6Yw2Q995qfBUUo0/Mbg05B/sGA0gkgPI7kmSHBg=="],
"emblor/cmdk/@radix-ui/react-dialog/@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/primitive": "1.0.0", "@radix-ui/react-compose-refs": "1.0.0", "@radix-ui/react-primitive": "1.0.0", "@radix-ui/react-use-callback-ref": "1.0.0", "@radix-ui/react-use-escape-keydown": "1.0.0" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" } }, "sha512-n7kDRfx+LB1zLueRDvZ1Pd0bxdJWDUZNQ/GWoxDn2prnuJKRdxsjulejX/ePkOsLi2tTm6P24mDqlMSgQpsT6g=="],
"emblor/cmdk/@radix-ui/react-dialog/@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-UagjDk4ijOAnGu4WMUPj9ahi7/zJJqNZ9ZAiGPp7waUWJO0O1aWXi/udPphI0IUjvrhBsZJGSN66dR2dsueLWQ=="],
"emblor/cmdk/@radix-ui/react-dialog/@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-compose-refs": "1.0.0", "@radix-ui/react-primitive": "1.0.0", "@radix-ui/react-use-callback-ref": "1.0.0" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" } }, "sha512-C4SWtsULLGf/2L4oGeIHlvWQx7Rf+7cX/vKOAD2dXW0A1b5QXwi3wWeaEgW+wn+SEVrraMUk05vLU9fZZz5HbQ=="],
"emblor/cmdk/@radix-ui/react-dialog/@radix-ui/react-id": ["@radix-ui/react-id@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-use-layout-effect": "1.0.0" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-Q6iAB/U7Tq3NTolBBQbHTgclPmGWE3OlktGGqrClPozSw4vkQ1DfQAOtzgRPecKsMdJINE05iaoDUG8tRzCBjw=="],
"emblor/cmdk/@radix-ui/react-dialog/@radix-ui/react-portal": ["@radix-ui/react-portal@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-primitive": "1.0.0" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" } }, "sha512-a8qyFO/Xb99d8wQdu4o7qnigNjTPG123uADNecz0eX4usnQEj7o+cG4ZX4zkqq98NYekT7UoEQIjxBNWIFuqTA=="],
"emblor/cmdk/@radix-ui/react-dialog/@radix-ui/react-presence": ["@radix-ui/react-presence@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-compose-refs": "1.0.0", "@radix-ui/react-use-layout-effect": "1.0.0" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" } }, "sha512-A+6XEvN01NfVWiKu38ybawfHsBjWum42MRPnEuqPsBZ4eV7e/7K321B5VgYMPv3Xx5An6o1/l9ZuDBgmcmWK3w=="],
"emblor/cmdk/@radix-ui/react-dialog/@radix-ui/react-primitive": ["@radix-ui/react-primitive@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-slot": "1.0.0" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" } }, "sha512-EyXe6mnRlHZ8b6f4ilTDrXmkLShICIuOTTj0GX4w1rp+wSxf3+TD05u1UOITC8VsJ2a9nwHvdXtOXEOl0Cw/zQ=="],
"emblor/cmdk/@radix-ui/react-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-compose-refs": "1.0.0" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-3mrKauI/tWXo1Ll+gN5dHcxDPdm/Df1ufcDLCecn+pnCIVcdWE7CujXo8QaXOWRJyZyQWWbpB8eFwHzWXlv5mQ=="],
"emblor/cmdk/@radix-ui/react-dialog/@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-use-callback-ref": "1.0.0" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-FohDoZvk3mEXh9AWAVyRTYR4Sq7/gavuofglmiXB2g1aKyboUD4YtgWxKj8O5n+Uak52gXQ4wKz5IFST4vtJHg=="],
"emblor/cmdk/@radix-ui/react-dialog/react-remove-scroll": ["react-remove-scroll@2.5.4", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.3", "react-style-singleton": "^2.2.1", "tslib": "^2.1.0", "use-callback-ref": "^1.3.0", "use-sidecar": "^1.1.2" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-xGVKJJr0SJGQVirVFAUZ2k1QLyO6m+2fy0l8Qawbp5Jgrv3DeLalrfMNBFSlmz5kriGGzsVBtGVnf4pTKIhhWA=="],
"js-beautify/glob/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
"novel/cmdk/@radix-ui/react-id/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w=="],
@@ -3517,6 +3640,24 @@
"@zero/mail/eslint-config-next/@next/eslint-plugin-next/fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
"emblor/@radix-ui/react-dialog/@radix-ui/react-dismissable-layer/@radix-ui/react-use-callback-ref/react": ["react@19.0.0", "", {}, "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ=="],
"emblor/@radix-ui/react-dialog/@radix-ui/react-dismissable-layer/@radix-ui/react-use-escape-keydown/react": ["react@19.0.0", "", {}, "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ=="],
"emblor/@radix-ui/react-dialog/@radix-ui/react-focus-scope/@radix-ui/react-use-callback-ref/react": ["react@19.0.0", "", {}, "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ=="],
"emblor/cmdk/@radix-ui/react-dialog/@radix-ui/react-dismissable-layer/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-GZtyzoHz95Rhs6S63D2t/eqvdFCm7I+yHMLVQheKM7nBD8mbZIt+ct1jz4536MDnaOGKIxynJ8eHTkVGVVkoTg=="],
"emblor/cmdk/@radix-ui/react-dialog/@radix-ui/react-dismissable-layer/@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-use-callback-ref": "1.0.0" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-JwfBCUIfhXRxKExgIqGa4CQsiMemo1Xt0W/B4ei3fpzpvPENKpMKQ8mZSB6Acj3ebrAEgi2xiQvcI1PAAodvyg=="],
"emblor/cmdk/@radix-ui/react-dialog/@radix-ui/react-focus-scope/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-GZtyzoHz95Rhs6S63D2t/eqvdFCm7I+yHMLVQheKM7nBD8mbZIt+ct1jz4536MDnaOGKIxynJ8eHTkVGVVkoTg=="],
"emblor/cmdk/@radix-ui/react-dialog/@radix-ui/react-id/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-6Tpkq+R6LOlmQb1R5NNETLG0B4YP0wc+klfXafpUCj6JGyaUc8il7/kUZ7m59rGbXGczE9Bs+iz2qloqsZBduQ=="],
"emblor/cmdk/@radix-ui/react-dialog/@radix-ui/react-presence/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-6Tpkq+R6LOlmQb1R5NNETLG0B4YP0wc+klfXafpUCj6JGyaUc8il7/kUZ7m59rGbXGczE9Bs+iz2qloqsZBduQ=="],
"emblor/cmdk/@radix-ui/react-dialog/@radix-ui/react-use-controllable-state/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-GZtyzoHz95Rhs6S63D2t/eqvdFCm7I+yHMLVQheKM7nBD8mbZIt+ct1jz4536MDnaOGKIxynJ8eHTkVGVVkoTg=="],
"wrangler/sharp/color/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
"wrangler/sharp/color/color-string/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],