Initial commit: Outlook-competitor desktop mail client
Tauri v2 (Rust backend, React/TypeScript frontend). IMAP/POP3/SMTP core, Google OAuth (mail/calendar/contacts sync), CalDAV, local calendar with recurrence, rules engine, contacts, tasks, unified inbox, context menus, drag & drop, snooze, templates, and more.
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
import clsx from 'clsx'
|
||||
import { Popover } from './Popover'
|
||||
|
||||
export function ClassicTabBar({
|
||||
tabs,
|
||||
active,
|
||||
onChange,
|
||||
rightSlot,
|
||||
}: {
|
||||
tabs: { id: string; label: string }[]
|
||||
active: string
|
||||
onChange: (id: string) => void
|
||||
rightSlot?: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<div className="flex items-center gap-0.5 bg-[#1e5aa8] px-2 pt-1.5 dark:bg-zinc-900">
|
||||
{tabs.map((t) => (
|
||||
<button
|
||||
key={t.id}
|
||||
onClick={() => onChange(t.id)}
|
||||
className={clsx(
|
||||
'rounded-t-sm px-3 py-1.5 text-[13px] transition',
|
||||
active === t.id
|
||||
? 'bg-[#f3f2f1] font-semibold text-[#1e5aa8] dark:bg-zinc-800 dark:text-blue-300'
|
||||
: 'text-white/90 hover:bg-white/10',
|
||||
)}
|
||||
>
|
||||
{t.label}
|
||||
</button>
|
||||
))}
|
||||
{rightSlot && <div className="ml-auto flex items-center pb-1.5">{rightSlot}</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function ClassicRibbonGroup({
|
||||
caption,
|
||||
children,
|
||||
onLauncher,
|
||||
}: {
|
||||
caption: string
|
||||
children: React.ReactNode
|
||||
onLauncher?: () => void
|
||||
}) {
|
||||
return (
|
||||
<div className="flex h-[92px] shrink-0 flex-col justify-between border-r border-[#d1d1d1] px-1.5 last:border-r-0 dark:border-zinc-700">
|
||||
<div className="flex flex-1 items-start gap-0.5 pt-1">{children}</div>
|
||||
<div className="relative flex items-center justify-center pb-0.5">
|
||||
<span className="text-[10.5px] text-[#605e5c] dark:text-zinc-400">{caption}</span>
|
||||
{onLauncher && (
|
||||
<button
|
||||
onClick={onLauncher}
|
||||
title={`${caption}: weitere Optionen`}
|
||||
className="absolute right-0 text-[#605e5c] hover:text-[#1e5aa8] dark:text-zinc-400 dark:hover:text-blue-300"
|
||||
>
|
||||
<svg width="8" height="8" viewBox="0 0 9 9">
|
||||
<path d="M0 9L9 0M9 0H3M9 0V6" stroke="currentColor" strokeWidth="1.2" fill="none" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function ClassicButton({
|
||||
icon: Icon,
|
||||
label,
|
||||
onClick,
|
||||
disabled,
|
||||
active,
|
||||
size = 'large',
|
||||
title,
|
||||
}: {
|
||||
icon: React.ComponentType<{ size?: number }>
|
||||
label: string
|
||||
onClick?: () => void
|
||||
disabled?: boolean
|
||||
active?: boolean
|
||||
size?: 'large' | 'small'
|
||||
title?: string
|
||||
}) {
|
||||
if (size === 'small') {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
title={title ?? label}
|
||||
className={clsx(
|
||||
'flex items-center gap-1.5 rounded-sm px-1.5 py-0.5 text-[11.5px] text-[#1f1f1f] transition disabled:cursor-not-allowed disabled:opacity-40 dark:text-zinc-200',
|
||||
active ? 'bg-[#cce4f7] dark:bg-blue-500/20' : 'hover:bg-[#e6f2fb] dark:hover:bg-zinc-700/60',
|
||||
)}
|
||||
>
|
||||
<Icon size={14} />
|
||||
<span className="whitespace-nowrap">{label}</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
title={title ?? label}
|
||||
className={clsx(
|
||||
'flex w-[64px] shrink-0 flex-col items-center gap-1 rounded-sm px-1 py-1 text-center text-[11px] leading-tight text-[#1f1f1f] transition disabled:cursor-not-allowed disabled:opacity-40 dark:text-zinc-200',
|
||||
active ? 'bg-[#cce4f7] dark:bg-blue-500/20' : 'hover:bg-[#e6f2fb] dark:hover:bg-zinc-700/60',
|
||||
)}
|
||||
>
|
||||
<Icon size={24} />
|
||||
<span>{label}</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
export function ClassicDropdown({
|
||||
button,
|
||||
children,
|
||||
open,
|
||||
onOpenChange,
|
||||
align = 'left',
|
||||
widthClass = 'w-56',
|
||||
}: {
|
||||
button: React.ReactNode
|
||||
children: React.ReactNode
|
||||
open: boolean
|
||||
onOpenChange: (v: boolean) => void
|
||||
align?: 'left' | 'right'
|
||||
widthClass?: string
|
||||
}) {
|
||||
return (
|
||||
<Popover
|
||||
open={open}
|
||||
onOpenChange={onOpenChange}
|
||||
trigger={button}
|
||||
align={align === 'right' ? 'end' : 'start'}
|
||||
panelClassName={clsx(
|
||||
'animate-panel-in overflow-hidden rounded-md border border-zinc-300 bg-white py-1 shadow-xl dark:border-zinc-700 dark:bg-zinc-900',
|
||||
widthClass,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
export function ClassicMenuItem({
|
||||
children,
|
||||
onClick,
|
||||
disabled,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
onClick?: () => void
|
||||
disabled?: boolean
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
className="flex w-full items-center gap-2 px-3 py-1.5 text-left text-[12.5px] text-zinc-700 transition hover:bg-[#e6f2fb] disabled:cursor-not-allowed disabled:opacity-40 dark:text-zinc-200 dark:hover:bg-zinc-800"
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
export function ClassicRibbonBody({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="flex h-[100px] items-stretch overflow-x-auto bg-[#f3f2f1] px-1.5 dark:bg-zinc-800/60">{children}</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user