feat(i18n): Hebrew locale + full RTL support
This commit is contained in:
@@ -1053,7 +1053,7 @@ export function CalendarInvitationBanner({ email }: CalendarInvitationBannerProp
|
||||
setShowCalendarPicker(false);
|
||||
handleImport(cal.id);
|
||||
}}
|
||||
className="w-full px-3 py-1.5 text-sm text-left hover:bg-muted flex items-center gap-2"
|
||||
className="w-full px-3 py-1.5 text-sm text-start hover:bg-muted flex items-center gap-2"
|
||||
>
|
||||
<span
|
||||
className="w-3 h-3 rounded-full flex-shrink-0"
|
||||
@@ -1095,7 +1095,7 @@ export function CalendarInvitationBanner({ email }: CalendarInvitationBannerProp
|
||||
)}
|
||||
|
||||
{isProcessing && (
|
||||
<Loader2 className="w-4 h-4 animate-spin text-muted-foreground ml-auto" />
|
||||
<Loader2 className="w-4 h-4 animate-spin text-muted-foreground ms-auto" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1990,7 +1990,7 @@ export function EmailComposer({
|
||||
<div ref={composerRootRef} className={cn("flex h-full bg-background", className)}>
|
||||
<PluginSlot
|
||||
name="composer-sidebar"
|
||||
className="hidden md:flex shrink-0 h-full overflow-hidden border-r border-border"
|
||||
className="hidden md:flex shrink-0 h-full overflow-hidden border-e border-border"
|
||||
/>
|
||||
{/* Right-side composer sidebar slot is rendered after the main content div below. */}
|
||||
<div
|
||||
@@ -2047,7 +2047,7 @@ export function EmailComposer({
|
||||
size="sm"
|
||||
className="md:hidden h-9 px-4"
|
||||
>
|
||||
<Send className="w-4 h-4 mr-1.5" />
|
||||
<Send className="w-4 h-4 me-1.5" />
|
||||
{t('send')}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -2445,7 +2445,7 @@ export function EmailComposer({
|
||||
)}
|
||||
<button
|
||||
onClick={() => removeAttachment(index)}
|
||||
className="ml-1 hover:text-red-500 min-w-[20px] min-h-[20px] flex items-center justify-center"
|
||||
className="ms-1 hover:text-red-500 min-w-[20px] min-h-[20px] flex items-center justify-center"
|
||||
title={att.uploading ? t('upload_cancel') : undefined}
|
||||
>
|
||||
<X className="w-3 h-3" />
|
||||
@@ -2540,9 +2540,9 @@ export function EmailComposer({
|
||||
onClick={() => handleSend()}
|
||||
disabled={!canSend || isSending}
|
||||
title={getSendTooltip()}
|
||||
className="rounded-r-none border-r border-primary-foreground/20"
|
||||
className="rounded-e-none border-e border-primary-foreground/20"
|
||||
>
|
||||
<Send className="w-4 h-4 mr-2" />
|
||||
<Send className="w-4 h-4 me-2" />
|
||||
{t('send')}
|
||||
</Button>
|
||||
<Button
|
||||
@@ -2550,7 +2550,7 @@ export function EmailComposer({
|
||||
onClick={() => setShowSendMenu((open) => !open)}
|
||||
disabled={!canSend || isSending}
|
||||
title={t('schedule_send')}
|
||||
className="rounded-l-none px-2"
|
||||
className="rounded-s-none px-2"
|
||||
aria-haspopup="menu"
|
||||
aria-expanded={showSendMenu}
|
||||
>
|
||||
@@ -2565,7 +2565,7 @@ export function EmailComposer({
|
||||
type="button"
|
||||
role="menuitem"
|
||||
onClick={openScheduleDialog}
|
||||
className="flex w-full items-center gap-2 rounded-sm px-3 py-2 text-left text-sm hover:bg-accent hover:text-accent-foreground"
|
||||
className="flex w-full items-center gap-2 rounded-sm px-3 py-2 text-start text-sm hover:bg-accent hover:text-accent-foreground"
|
||||
>
|
||||
<CalendarClock className="w-4 h-4" />
|
||||
{t('schedule_send')}
|
||||
@@ -2580,7 +2580,7 @@ export function EmailComposer({
|
||||
title={getSendTooltip()}
|
||||
className="hidden md:inline-flex"
|
||||
>
|
||||
<Send className="w-4 h-4 mr-2" />
|
||||
<Send className="w-4 h-4 me-2" />
|
||||
{t('send')}
|
||||
</Button>
|
||||
)}
|
||||
@@ -2699,7 +2699,7 @@ export function EmailComposer({
|
||||
{t('discard')}
|
||||
</Button>
|
||||
<Button onClick={handleSaveDraftAndClose}>
|
||||
<Save className="w-4 h-4 mr-2" />
|
||||
<Save className="w-4 h-4 me-2" />
|
||||
{t('save_draft')}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -2718,7 +2718,7 @@ export function EmailComposer({
|
||||
</div>
|
||||
<PluginSlot
|
||||
name="composer-sidebar-right"
|
||||
className="hidden md:flex shrink-0 h-full overflow-hidden border-l border-border"
|
||||
className="hidden md:flex shrink-0 h-full overflow-hidden border-s border-border"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -2743,7 +2743,7 @@ const AutocompleteDropdown = React.forwardRef<HTMLDivElement, {
|
||||
role="option"
|
||||
aria-selected={i === selectedIndex}
|
||||
className={cn(
|
||||
"w-full px-3 py-2 text-left text-sm flex items-center gap-2",
|
||||
"w-full px-3 py-2 text-start text-sm flex items-center gap-2",
|
||||
i === selectedIndex ? "bg-accent text-accent-foreground" : "hover:bg-muted"
|
||||
)}
|
||||
onMouseDown={(e) => {
|
||||
|
||||
@@ -334,7 +334,7 @@ export function EmailContextMenu({
|
||||
</div>
|
||||
)}
|
||||
{node.children.length > 0 && (
|
||||
<div className="pl-4">
|
||||
<div className="ps-4">
|
||||
{renderNodes(node.children)}
|
||||
</div>
|
||||
)}
|
||||
@@ -377,7 +377,7 @@ export function EmailContextMenu({
|
||||
role="menuitem"
|
||||
onClick={() => handleAction(() => onSetColorTag?.(option.value))}
|
||||
className={cn(
|
||||
"w-full px-3 py-1.5 text-sm text-left flex items-center gap-2 hover:bg-muted cursor-pointer",
|
||||
"w-full px-3 py-1.5 text-sm text-start flex items-center gap-2 hover:bg-muted cursor-pointer",
|
||||
isActive && "bg-accent font-medium"
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -181,16 +181,17 @@ export function EmailHoverActions({
|
||||
|
||||
return (
|
||||
<div
|
||||
className="absolute right-0 top-0 bottom-0 z-10 hidden group-hover:flex items-center"
|
||||
className="absolute end-0 top-0 bottom-0 z-10 hidden group-hover:flex items-center"
|
||||
>
|
||||
<div
|
||||
className={cn("w-8 h-full", hoverBackgroundClassName)}
|
||||
style={{
|
||||
WebkitMaskImage: "linear-gradient(to right, transparent, black)",
|
||||
maskImage: "linear-gradient(to right, transparent, black)",
|
||||
}}
|
||||
className={cn(
|
||||
"w-8 h-full",
|
||||
"[mask-image:linear-gradient(to_right,transparent,black)] [-webkit-mask-image:linear-gradient(to_right,transparent,black)]",
|
||||
"rtl:[mask-image:linear-gradient(to_left,transparent,black)] rtl:[-webkit-mask-image:linear-gradient(to_left,transparent,black)]",
|
||||
hoverBackgroundClassName,
|
||||
)}
|
||||
/>
|
||||
<div className={cn("flex items-center gap-0.5 h-full pr-3 pl-0.5", hoverBackgroundClassName)}>
|
||||
<div className={cn("flex items-center gap-0.5 h-full pe-3 ps-0.5", hoverBackgroundClassName)}>
|
||||
{actionButtons}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -176,7 +176,7 @@ export function EmailListItem({ email, selected, onClick, onDoubleClick, onConte
|
||||
|
||||
{/* Unread indicator */}
|
||||
{isUnread && (
|
||||
<div className="absolute left-0.5 top-1/2 -translate-y-1/2">
|
||||
<div className="absolute start-0.5 top-1/2 -translate-y-1/2">
|
||||
<Circle className="w-2 h-2 fill-unread text-unread" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -417,9 +417,9 @@ export function EmailList({
|
||||
className="text-destructive border-destructive/30 hover:bg-destructive/10 text-xs"
|
||||
>
|
||||
{isProcessing ? (
|
||||
<Loader2 className="w-3 h-3 animate-spin mr-1" />
|
||||
<Loader2 className="w-3 h-3 animate-spin me-1" />
|
||||
) : (
|
||||
<Trash2 className="w-3 h-3 mr-1" />
|
||||
<Trash2 className="w-3 h-3 me-1" />
|
||||
)}
|
||||
{t('empty_folder.button')}
|
||||
</Button>
|
||||
|
||||
@@ -333,7 +333,7 @@ function renderClickableRecipients(
|
||||
|
||||
return (
|
||||
<span key={r.email + index} className="inline-flex items-center">
|
||||
{index > 0 && <span className="text-muted-foreground mr-1">,</span>}
|
||||
{index > 0 && <span className="text-muted-foreground me-1">,</span>}
|
||||
<RecipientPopover
|
||||
name={r.name}
|
||||
email={r.email}
|
||||
@@ -382,7 +382,7 @@ export function ContactSidebarPanel({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-[320px] shrink-0 border-l border-border bg-background flex flex-col h-full animate-in slide-in-from-right-5 duration-200">
|
||||
<div className="w-[320px] shrink-0 border-s border-border bg-background flex flex-col h-full animate-in slide-in-from-right-5 duration-200">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-4 py-3 border-b border-border">
|
||||
<h3 className="text-sm font-semibold text-foreground truncate">{t('contact_sidebar.title')}</h3>
|
||||
@@ -607,7 +607,7 @@ function SidebarSection({ icon: Icon, title, children }: { icon: React.Component
|
||||
<Icon className="w-3.5 h-3.5 text-muted-foreground" />
|
||||
<h4 className="text-xs font-medium text-muted-foreground uppercase tracking-wider">{title}</h4>
|
||||
</div>
|
||||
<div className="space-y-1 pl-5.5">{children}</div>
|
||||
<div className="space-y-1 ps-5.5">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -2637,7 +2637,7 @@ export function EmailViewer({
|
||||
<h3 className="text-xl font-semibold text-foreground mb-3">{tDemoWelcome('title')}</h3>
|
||||
<p className="text-sm text-muted-foreground mb-6 leading-relaxed">{tDemoWelcome('description')}</p>
|
||||
<div className="flex flex-col gap-3 items-center">
|
||||
<div className="grid grid-cols-2 gap-3 text-left text-sm text-muted-foreground w-full">
|
||||
<div className="grid grid-cols-2 gap-3 text-start text-sm text-muted-foreground w-full">
|
||||
<div className="flex items-center gap-2">
|
||||
<Mail className="w-4 h-4 text-primary shrink-0" />
|
||||
<span>{tDemoWelcome('feature_email')}</span>
|
||||
@@ -2679,7 +2679,7 @@ export function EmailViewer({
|
||||
<p className="text-muted-foreground">{t('no_conversation_description')}</p>
|
||||
{onCompose && (
|
||||
<Button onClick={onCompose} className="mt-6" title={t('compose_hint')}>
|
||||
<PenSquare className="w-4 h-4 mr-2" />
|
||||
<PenSquare className="w-4 h-4 me-2" />
|
||||
{t('compose')}
|
||||
</Button>
|
||||
)}
|
||||
@@ -2704,7 +2704,7 @@ export function EmailViewer({
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={onBack}
|
||||
className="h-9 w-9 flex-shrink-0 -ml-1"
|
||||
className="h-9 w-9 flex-shrink-0 -ms-1"
|
||||
aria-label={t('back_to_list')}
|
||||
>
|
||||
<ChevronLeft className="w-5 h-5" />
|
||||
@@ -2839,7 +2839,7 @@ export function EmailViewer({
|
||||
{showToolbarLabels && <span className="text-[10px] leading-tight sm:text-sm">{t('move')}</span>}
|
||||
</Button>
|
||||
{moveMenuOpen && (
|
||||
<div className="absolute right-0 top-full mt-1 py-1 w-48 max-h-72 overflow-y-auto bg-background rounded-lg shadow-lg border border-border z-10">
|
||||
<div className="absolute end-0 top-full mt-1 py-1 w-48 max-h-72 overflow-y-auto bg-background rounded-lg shadow-lg border border-border z-10">
|
||||
{(() => {
|
||||
const renderNodes = (nodes: MailboxNode[], depth = 0) => {
|
||||
return nodes.map((node) => {
|
||||
@@ -2850,7 +2850,7 @@ export function EmailViewer({
|
||||
{isTarget ? (
|
||||
<button
|
||||
onClick={() => { onMoveToMailbox(node.id); setMoveMenuOpen(false); }}
|
||||
className="w-full px-3 py-1.5 text-sm text-left hover:bg-muted flex items-center gap-2"
|
||||
className="w-full px-3 py-1.5 text-sm text-start hover:bg-muted flex items-center gap-2"
|
||||
style={{ paddingLeft: `${0.75 + depth * 1}rem` }}
|
||||
>
|
||||
<Icon className="w-4 h-4 flex-shrink-0" />
|
||||
@@ -2910,7 +2910,7 @@ export function EmailViewer({
|
||||
)}
|
||||
</button>
|
||||
{tagMenuOpen && (
|
||||
<div className="absolute right-0 top-full mt-1 py-1 w-40 bg-background rounded-lg shadow-lg border border-border z-10">
|
||||
<div className="absolute end-0 top-full mt-1 py-1 w-40 bg-background rounded-lg shadow-lg border border-border z-10">
|
||||
{colorOptions.map((option) => {
|
||||
const isActive = currentColors.includes(option.value);
|
||||
return (
|
||||
@@ -2918,13 +2918,13 @@ export function EmailViewer({
|
||||
key={option.value}
|
||||
onClick={() => { if (email) onSetColorTag?.(email.id, option.value); setTagMenuOpen(false); }}
|
||||
className={cn(
|
||||
"w-full px-3 py-1.5 text-sm text-left hover:bg-muted flex items-center gap-2",
|
||||
"w-full px-3 py-1.5 text-sm text-start hover:bg-muted flex items-center gap-2",
|
||||
isActive && "bg-accent font-medium"
|
||||
)}
|
||||
>
|
||||
<span className={cn("w-3 h-3 rounded-full flex-shrink-0", option.color)} />
|
||||
<span className="truncate">{option.name}</span>
|
||||
{isActive && <Check className="w-3 h-3 ml-auto flex-shrink-0 text-foreground" />}
|
||||
{isActive && <Check className="w-3 h-3 ms-auto flex-shrink-0 text-foreground" />}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
@@ -2933,7 +2933,7 @@ export function EmailViewer({
|
||||
<div className="h-px bg-border my-1" />
|
||||
<button
|
||||
onClick={() => { if (email) onSetColorTag?.(email.id, null); setTagMenuOpen(false); }}
|
||||
className="w-full px-3 py-1.5 text-sm text-left hover:bg-muted flex items-center gap-2 text-muted-foreground"
|
||||
className="w-full px-3 py-1.5 text-sm text-start hover:bg-muted flex items-center gap-2 text-muted-foreground"
|
||||
>
|
||||
<X className="w-3 h-3 flex-shrink-0" />
|
||||
<span>{t('remove_color')}</span>
|
||||
@@ -3037,11 +3037,11 @@ export function EmailViewer({
|
||||
<span className="text-[10px] leading-tight sm:hidden">{t('more_actions')}</span>
|
||||
</Button>
|
||||
{moreMenuOpen && !isMobile && (
|
||||
<div className="absolute right-0 top-full mt-1 w-48 bg-background rounded-md shadow-lg border border-border z-10 py-1">
|
||||
<div className="absolute end-0 top-full mt-1 w-48 bg-background rounded-md shadow-lg border border-border z-10 py-1">
|
||||
{/* Star toggle */}
|
||||
<button
|
||||
onClick={() => { onToggleStar?.(); setMoreMenuOpen(false); setMoreMenuSub(null); }}
|
||||
className="w-full px-3 py-1.5 text-sm text-left hover:bg-muted text-foreground flex items-center gap-2"
|
||||
className="w-full px-3 py-1.5 text-sm text-start hover:bg-muted text-foreground flex items-center gap-2"
|
||||
>
|
||||
<Star className={cn("w-4 h-4", isStarred && "fill-yellow-400 text-yellow-400")} />
|
||||
{isStarred ? t('tooltips.unstar') : t('tooltips.star')}
|
||||
@@ -3049,7 +3049,7 @@ export function EmailViewer({
|
||||
{/* Overflow: reply */}
|
||||
<button
|
||||
onClick={() => { onReply?.(); setMoreMenuOpen(false); setMoreMenuSub(null); }}
|
||||
className={cn("w-full px-3 py-1.5 text-sm text-left hover:bg-muted text-foreground flex items-center gap-2", hiddenPriorities.has(1) ? "" : "sm:hidden")}
|
||||
className={cn("w-full px-3 py-1.5 text-sm text-start hover:bg-muted text-foreground flex items-center gap-2", hiddenPriorities.has(1) ? "" : "sm:hidden")}
|
||||
>
|
||||
<Reply className="w-4 h-4" />
|
||||
{t('reply')}
|
||||
@@ -3057,7 +3057,7 @@ export function EmailViewer({
|
||||
{/* Overflow: reply all */}
|
||||
<button
|
||||
onClick={() => { onReplyAll?.(); setMoreMenuOpen(false); setMoreMenuSub(null); }}
|
||||
className={cn("w-full px-3 py-1.5 text-sm text-left hover:bg-muted text-foreground flex items-center gap-2", hiddenPriorities.has(2) ? "" : "sm:hidden")}
|
||||
className={cn("w-full px-3 py-1.5 text-sm text-start hover:bg-muted text-foreground flex items-center gap-2", hiddenPriorities.has(2) ? "" : "sm:hidden")}
|
||||
>
|
||||
<ReplyAll className="w-4 h-4" />
|
||||
{t('reply_all')}
|
||||
@@ -3065,7 +3065,7 @@ export function EmailViewer({
|
||||
{/* Overflow: forward */}
|
||||
<button
|
||||
onClick={() => { onForward?.(); setMoreMenuOpen(false); setMoreMenuSub(null); }}
|
||||
className={cn("w-full px-3 py-1.5 text-sm text-left hover:bg-muted text-foreground flex items-center gap-2", hiddenPriorities.has(3) ? "" : "sm:hidden")}
|
||||
className={cn("w-full px-3 py-1.5 text-sm text-start hover:bg-muted text-foreground flex items-center gap-2", hiddenPriorities.has(3) ? "" : "sm:hidden")}
|
||||
>
|
||||
<Forward className="w-4 h-4" />
|
||||
{t('forward')}
|
||||
@@ -3073,7 +3073,7 @@ export function EmailViewer({
|
||||
{/* Overflow: archive */}
|
||||
<button
|
||||
onClick={() => { onArchive?.(); setMoreMenuOpen(false); setMoreMenuSub(null); }}
|
||||
className={cn("w-full px-3 py-1.5 text-sm text-left hover:bg-muted text-foreground flex items-center gap-2", hiddenPriorities.has(4) ? "" : "sm:hidden")}
|
||||
className={cn("w-full px-3 py-1.5 text-sm text-start hover:bg-muted text-foreground flex items-center gap-2", hiddenPriorities.has(4) ? "" : "sm:hidden")}
|
||||
>
|
||||
<Archive className="w-4 h-4" />
|
||||
{t('archive')}
|
||||
@@ -3086,14 +3086,14 @@ export function EmailViewer({
|
||||
>
|
||||
<button
|
||||
onClick={() => setMoreMenuSub(moreMenuSub === 'move' ? null : 'move')}
|
||||
className="w-full px-3 py-1.5 text-sm text-left hover:bg-muted text-foreground flex items-center gap-2"
|
||||
className="w-full px-3 py-1.5 text-sm text-start hover:bg-muted text-foreground flex items-center gap-2"
|
||||
>
|
||||
<FolderInput className="w-4 h-4" />
|
||||
<span className="flex-1">{t('move_to')}</span>
|
||||
<ChevronRight className="w-3 h-3 text-muted-foreground" />
|
||||
</button>
|
||||
{moreMenuSub === 'move' && (
|
||||
<div className="absolute right-full top-0 mr-1 py-1 w-48 max-h-72 overflow-y-auto bg-background rounded-md shadow-lg border border-border z-10">
|
||||
<div className="absolute end-full top-0 me-1 py-1 w-48 max-h-72 overflow-y-auto bg-background rounded-md shadow-lg border border-border z-10">
|
||||
{(() => {
|
||||
const renderMobileNodes = (nodes: MailboxNode[], depth = 0) => {
|
||||
return nodes.map((node) => {
|
||||
@@ -3104,7 +3104,7 @@ export function EmailViewer({
|
||||
{isTarget ? (
|
||||
<button
|
||||
onClick={() => { onMoveToMailbox(node.id); setMoreMenuOpen(false); setMoreMenuSub(null); }}
|
||||
className="w-full px-3 py-1.5 text-sm text-left hover:bg-muted flex items-center gap-2"
|
||||
className="w-full px-3 py-1.5 text-sm text-start hover:bg-muted flex items-center gap-2"
|
||||
style={{ paddingLeft: `${0.75 + depth * 1}rem` }}
|
||||
>
|
||||
<Icon className="w-4 h-4 flex-shrink-0" />
|
||||
@@ -3138,14 +3138,14 @@ export function EmailViewer({
|
||||
>
|
||||
<button
|
||||
onClick={() => setMoreMenuSub(moreMenuSub === 'tag' ? null : 'tag')}
|
||||
className="w-full px-3 py-1.5 text-sm text-left hover:bg-muted text-foreground flex items-center gap-2"
|
||||
className="w-full px-3 py-1.5 text-sm text-start hover:bg-muted text-foreground flex items-center gap-2"
|
||||
>
|
||||
<Tag className="w-4 h-4" />
|
||||
<span className="flex-1">{t('tag')}</span>
|
||||
<ChevronRight className="w-3 h-3 text-muted-foreground" />
|
||||
</button>
|
||||
{moreMenuSub === 'tag' && (
|
||||
<div className="absolute right-full top-0 mr-1 py-1 w-40 bg-background rounded-md shadow-lg border border-border z-10">
|
||||
<div className="absolute end-full top-0 me-1 py-1 w-40 bg-background rounded-md shadow-lg border border-border z-10">
|
||||
{colorOptions.map((option) => {
|
||||
const isActive = currentColors.includes(option.value);
|
||||
return (
|
||||
@@ -3153,13 +3153,13 @@ export function EmailViewer({
|
||||
key={option.value}
|
||||
onClick={() => { if (email) onSetColorTag?.(email.id, option.value); setMoreMenuOpen(false); setMoreMenuSub(null); }}
|
||||
className={cn(
|
||||
"w-full px-3 py-1.5 text-sm text-left hover:bg-muted flex items-center gap-2",
|
||||
"w-full px-3 py-1.5 text-sm text-start hover:bg-muted flex items-center gap-2",
|
||||
isActive && "bg-accent font-medium"
|
||||
)}
|
||||
>
|
||||
<span className={cn("w-3 h-3 rounded-full flex-shrink-0", option.color)} />
|
||||
<span className="truncate">{option.name}</span>
|
||||
{isActive && <Check className="w-3 h-3 ml-auto flex-shrink-0 text-foreground" />}
|
||||
{isActive && <Check className="w-3 h-3 ms-auto flex-shrink-0 text-foreground" />}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
@@ -3168,7 +3168,7 @@ export function EmailViewer({
|
||||
<div className="h-px bg-border my-1" />
|
||||
<button
|
||||
onClick={() => { if (email) onSetColorTag?.(email.id, null); setMoreMenuOpen(false); setMoreMenuSub(null); }}
|
||||
className="w-full px-3 py-1.5 text-sm text-left hover:bg-muted flex items-center gap-2 text-muted-foreground"
|
||||
className="w-full px-3 py-1.5 text-sm text-start hover:bg-muted flex items-center gap-2 text-muted-foreground"
|
||||
>
|
||||
<X className="w-3 h-3 flex-shrink-0" />
|
||||
<span>{t('remove_color')}</span>
|
||||
@@ -3183,7 +3183,7 @@ export function EmailViewer({
|
||||
{spamApplicable && (onMarkAsSpam || onUndoSpam) && (
|
||||
<button
|
||||
onClick={() => { (isInJunkFolder ? onUndoSpam : onMarkAsSpam)?.(); setMoreMenuOpen(false); setMoreMenuSub(null); }}
|
||||
className={cn("w-full px-3 py-1.5 text-sm text-left hover:bg-muted text-foreground flex items-center gap-2", hiddenPriorities.has(7) ? "" : "sm:hidden")}
|
||||
className={cn("w-full px-3 py-1.5 text-sm text-start hover:bg-muted text-foreground flex items-center gap-2", hiddenPriorities.has(7) ? "" : "sm:hidden")}
|
||||
>
|
||||
{isInJunkFolder ? (
|
||||
<ShieldCheck className="h-4 w-4 text-green-600 dark:text-green-400" />
|
||||
@@ -3196,7 +3196,7 @@ export function EmailViewer({
|
||||
{/* Overflow: toggle read */}
|
||||
<button
|
||||
onClick={() => { onMarkAsRead?.(email.id, isUnread); setMoreMenuOpen(false); setMoreMenuSub(null); }}
|
||||
className={cn("w-full px-3 py-1.5 text-sm text-left hover:bg-muted text-foreground flex items-center gap-2", hiddenPriorities.has(8) ? "" : "sm:hidden")}
|
||||
className={cn("w-full px-3 py-1.5 text-sm text-start hover:bg-muted text-foreground flex items-center gap-2", hiddenPriorities.has(8) ? "" : "sm:hidden")}
|
||||
>
|
||||
{isUnread ? <MailOpen className="w-4 h-4" /> : <Mail className="w-4 h-4" />}
|
||||
{isUnread ? t('mark_read') : t('mark_unread')}
|
||||
@@ -3204,7 +3204,7 @@ export function EmailViewer({
|
||||
{/* Overflow: print */}
|
||||
<button
|
||||
onClick={() => { handlePrint(); setMoreMenuOpen(false); setMoreMenuSub(null); }}
|
||||
className={cn("w-full px-3 py-1.5 text-sm text-left hover:bg-muted text-foreground flex items-center gap-2", hiddenPriorities.has(9) ? "" : "sm:hidden")}
|
||||
className={cn("w-full px-3 py-1.5 text-sm text-start hover:bg-muted text-foreground flex items-center gap-2", hiddenPriorities.has(9) ? "" : "sm:hidden")}
|
||||
>
|
||||
<Printer className="w-4 h-4" />
|
||||
{t('print')}
|
||||
@@ -3212,7 +3212,7 @@ export function EmailViewer({
|
||||
{/* Overflow: view source */}
|
||||
<button
|
||||
onClick={() => { setShowSourceModal(true); setMoreMenuOpen(false); setMoreMenuSub(null); }}
|
||||
className={cn("w-full px-3 py-1.5 text-sm text-left hover:bg-muted text-foreground flex items-center gap-2", hiddenPriorities.has(10) ? "" : "sm:hidden")}
|
||||
className={cn("w-full px-3 py-1.5 text-sm text-start hover:bg-muted text-foreground flex items-center gap-2", hiddenPriorities.has(10) ? "" : "sm:hidden")}
|
||||
>
|
||||
<Code className="w-4 h-4" />
|
||||
{t('view_source')}
|
||||
@@ -3221,7 +3221,7 @@ export function EmailViewer({
|
||||
{effectiveEmailContent.isHtml && (
|
||||
<button
|
||||
onClick={() => { setEmailViewDarkOverride(prev => prev === null ? !(resolvedTheme === 'dark') : !prev); setMoreMenuOpen(false); setMoreMenuSub(null); }}
|
||||
className={cn("w-full px-3 py-1.5 text-sm text-left hover:bg-muted text-foreground flex items-center gap-2", hiddenPriorities.has(11) ? "" : "sm:hidden")}
|
||||
className={cn("w-full px-3 py-1.5 text-sm text-start hover:bg-muted text-foreground flex items-center gap-2", hiddenPriorities.has(11) ? "" : "sm:hidden")}
|
||||
>
|
||||
{isDark ? <Sun className="w-4 h-4" /> : <Moon className="w-4 h-4" />}
|
||||
{isDark ? 'View in light mode' : 'View in dark mode'}
|
||||
@@ -3231,7 +3231,7 @@ export function EmailViewer({
|
||||
{/* Export email */}
|
||||
<button
|
||||
onClick={() => { handleExportEmail(); setMoreMenuOpen(false); setMoreMenuSub(null); }}
|
||||
className="w-full px-3 py-1.5 text-sm text-left hover:bg-muted text-foreground flex items-center gap-2"
|
||||
className="w-full px-3 py-1.5 text-sm text-start hover:bg-muted text-foreground flex items-center gap-2"
|
||||
>
|
||||
<Download className="w-4 h-4" />
|
||||
{t('export_email')}
|
||||
@@ -3239,7 +3239,7 @@ export function EmailViewer({
|
||||
{/* Import email */}
|
||||
<button
|
||||
onClick={() => { handleImportEmail(); setMoreMenuOpen(false); setMoreMenuSub(null); }}
|
||||
className="w-full px-3 py-1.5 text-sm text-left hover:bg-muted text-foreground flex items-center gap-2"
|
||||
className="w-full px-3 py-1.5 text-sm text-start hover:bg-muted text-foreground flex items-center gap-2"
|
||||
>
|
||||
<Upload className="w-4 h-4" />
|
||||
{t('import_email')}
|
||||
@@ -3247,7 +3247,7 @@ export function EmailViewer({
|
||||
{onShowShortcuts && (
|
||||
<button
|
||||
onClick={() => { onShowShortcuts(); setMoreMenuOpen(false); setMoreMenuSub(null); }}
|
||||
className="w-full px-3 py-1.5 text-sm text-left hover:bg-muted text-foreground flex items-center gap-2"
|
||||
className="w-full px-3 py-1.5 text-sm text-start hover:bg-muted text-foreground flex items-center gap-2"
|
||||
>
|
||||
<Keyboard className="w-4 h-4" />
|
||||
{t('keyboard_shortcuts')}
|
||||
@@ -3275,7 +3275,7 @@ export function EmailViewer({
|
||||
)}
|
||||
{!isScheduled && isMobile && (
|
||||
<div className={cn(
|
||||
"fixed inset-y-0 right-0 w-72 bg-background border-l border-border z-[70] sm:hidden",
|
||||
"fixed inset-y-0 right-0 w-72 bg-background border-s border-border z-[70] sm:hidden",
|
||||
"transform transition-transform duration-300 ease-in-out",
|
||||
"flex flex-col",
|
||||
moreMenuOpen ? "translate-x-0" : "translate-x-full"
|
||||
@@ -3284,7 +3284,7 @@ export function EmailViewer({
|
||||
{moreMenuSub ? (
|
||||
<button
|
||||
onClick={() => setMoreMenuSub(null)}
|
||||
className="flex items-center gap-1 -ml-2 px-2 py-1 rounded hover:bg-muted text-sm font-semibold text-foreground"
|
||||
className="flex items-center gap-1 -ms-2 px-2 py-1 rounded hover:bg-muted text-sm font-semibold text-foreground"
|
||||
>
|
||||
<ChevronLeft className="w-5 h-5" />
|
||||
{moreMenuSub === 'move' ? t('move_to') : t('tag')}
|
||||
@@ -3302,7 +3302,7 @@ export function EmailViewer({
|
||||
{/* Star toggle */}
|
||||
<button
|
||||
onClick={() => { onToggleStar?.(); setMoreMenuOpen(false); }}
|
||||
className="w-full px-4 py-3 min-h-[44px] text-sm text-left hover:bg-muted text-foreground flex items-center gap-3"
|
||||
className="w-full px-4 py-3 min-h-[44px] text-sm text-start hover:bg-muted text-foreground flex items-center gap-3"
|
||||
>
|
||||
<Star className={cn("w-5 h-5", isStarred && "fill-yellow-400 text-yellow-400")} />
|
||||
{isStarred ? t('tooltips.unstar') : t('tooltips.star')}
|
||||
@@ -3311,12 +3311,12 @@ export function EmailViewer({
|
||||
{colorOptions.length > 0 && (
|
||||
<button
|
||||
onClick={() => setMoreMenuSub('tag')}
|
||||
className="w-full px-4 py-3 min-h-[44px] text-sm text-left hover:bg-muted text-foreground flex items-center gap-3"
|
||||
className="w-full px-4 py-3 min-h-[44px] text-sm text-start hover:bg-muted text-foreground flex items-center gap-3"
|
||||
>
|
||||
<Tag className="w-5 h-5" />
|
||||
<span className="flex-1">{t('tag')}</span>
|
||||
{currentColors.length > 0 && (
|
||||
<div className="flex -space-x-1 mr-1">
|
||||
<div className="flex -space-x-1 me-1">
|
||||
{currentColors.slice(0, 3).map((c) => {
|
||||
const opt = colorOptions.find((o) => o.value === c);
|
||||
return opt ? <span key={c} className={cn("w-3 h-3 rounded-full border border-background", opt.color)} /> : null;
|
||||
@@ -3328,14 +3328,14 @@ export function EmailViewer({
|
||||
)}
|
||||
<button
|
||||
onClick={() => { handlePrint(); setMoreMenuOpen(false); }}
|
||||
className="w-full px-4 py-3 min-h-[44px] text-sm text-left hover:bg-muted text-foreground flex items-center gap-3"
|
||||
className="w-full px-4 py-3 min-h-[44px] text-sm text-start hover:bg-muted text-foreground flex items-center gap-3"
|
||||
>
|
||||
<Printer className="w-5 h-5" />
|
||||
{t('print')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { setShowSourceModal(true); setMoreMenuOpen(false); }}
|
||||
className="w-full px-4 py-3 min-h-[44px] text-sm text-left hover:bg-muted text-foreground flex items-center gap-3"
|
||||
className="w-full px-4 py-3 min-h-[44px] text-sm text-start hover:bg-muted text-foreground flex items-center gap-3"
|
||||
>
|
||||
<Code className="w-5 h-5" />
|
||||
{t('view_source')}
|
||||
@@ -3343,7 +3343,7 @@ export function EmailViewer({
|
||||
{effectiveEmailContent.isHtml && (
|
||||
<button
|
||||
onClick={() => { setEmailViewDarkOverride(prev => prev === null ? !(resolvedTheme === 'dark') : !prev); setMoreMenuOpen(false); }}
|
||||
className="w-full px-4 py-3 min-h-[44px] text-sm text-left hover:bg-muted text-foreground flex items-center gap-3"
|
||||
className="w-full px-4 py-3 min-h-[44px] text-sm text-start hover:bg-muted text-foreground flex items-center gap-3"
|
||||
>
|
||||
{isDark ? <Sun className="w-5 h-5" /> : <Moon className="w-5 h-5" />}
|
||||
{isDark ? 'View in light mode' : 'View in dark mode'}
|
||||
@@ -3352,14 +3352,14 @@ export function EmailViewer({
|
||||
<div className="h-px bg-border my-1" />
|
||||
<button
|
||||
onClick={() => { handleExportEmail(); setMoreMenuOpen(false); }}
|
||||
className="w-full px-4 py-3 min-h-[44px] text-sm text-left hover:bg-muted text-foreground flex items-center gap-3"
|
||||
className="w-full px-4 py-3 min-h-[44px] text-sm text-start hover:bg-muted text-foreground flex items-center gap-3"
|
||||
>
|
||||
<Download className="w-5 h-5" />
|
||||
{t('export_email')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { handleImportEmail(); setMoreMenuOpen(false); }}
|
||||
className="w-full px-4 py-3 min-h-[44px] text-sm text-left hover:bg-muted text-foreground flex items-center gap-3"
|
||||
className="w-full px-4 py-3 min-h-[44px] text-sm text-start hover:bg-muted text-foreground flex items-center gap-3"
|
||||
>
|
||||
<Upload className="w-5 h-5" />
|
||||
{t('import_email')}
|
||||
@@ -3367,7 +3367,7 @@ export function EmailViewer({
|
||||
{onShowShortcuts && (
|
||||
<button
|
||||
onClick={() => { onShowShortcuts(); setMoreMenuOpen(false); }}
|
||||
className="w-full px-4 py-3 min-h-[44px] text-sm text-left hover:bg-muted text-foreground flex items-center gap-3"
|
||||
className="w-full px-4 py-3 min-h-[44px] text-sm text-start hover:bg-muted text-foreground flex items-center gap-3"
|
||||
>
|
||||
<Keyboard className="w-5 h-5" />
|
||||
{t('keyboard_shortcuts')}
|
||||
@@ -3385,7 +3385,7 @@ export function EmailViewer({
|
||||
{isTarget ? (
|
||||
<button
|
||||
onClick={() => { onMoveToMailbox(node.id); setMoreMenuOpen(false); setMoreMenuSub(null); }}
|
||||
className="w-full px-4 py-2.5 min-h-[44px] text-sm text-left hover:bg-muted flex items-center gap-3"
|
||||
className="w-full px-4 py-2.5 min-h-[44px] text-sm text-start hover:bg-muted flex items-center gap-3"
|
||||
style={{ paddingLeft: `${1 + depth * 1}rem` }}
|
||||
>
|
||||
<Icon className="w-5 h-5 flex-shrink-0" />
|
||||
@@ -3416,20 +3416,20 @@ export function EmailViewer({
|
||||
key={option.value}
|
||||
onClick={() => { if (email) onSetColorTag?.(email.id, option.value); setMoreMenuOpen(false); setMoreMenuSub(null); }}
|
||||
className={cn(
|
||||
"w-full px-4 py-2.5 min-h-[44px] text-sm text-left hover:bg-muted flex items-center gap-3",
|
||||
"w-full px-4 py-2.5 min-h-[44px] text-sm text-start hover:bg-muted flex items-center gap-3",
|
||||
isActive && "bg-accent font-medium"
|
||||
)}
|
||||
>
|
||||
<span className={cn("w-3.5 h-3.5 rounded-full flex-shrink-0", option.color)} />
|
||||
<span className="truncate">{option.name}</span>
|
||||
{isActive && <Check className="w-4 h-4 ml-auto flex-shrink-0 text-foreground" />}
|
||||
{isActive && <Check className="w-4 h-4 ms-auto flex-shrink-0 text-foreground" />}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
{currentColors.length > 0 && (
|
||||
<button
|
||||
onClick={() => { if (email) onSetColorTag?.(email.id, null); setMoreMenuOpen(false); setMoreMenuSub(null); }}
|
||||
className="w-full px-4 py-2.5 min-h-[44px] text-sm text-left hover:bg-muted flex items-center gap-3 text-muted-foreground"
|
||||
className="w-full px-4 py-2.5 min-h-[44px] text-sm text-start hover:bg-muted flex items-center gap-3 text-muted-foreground"
|
||||
>
|
||||
<X className="w-4 h-4 flex-shrink-0" />
|
||||
<span>{t('remove_color')}</span>
|
||||
@@ -3469,7 +3469,7 @@ export function EmailViewer({
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={onBack}
|
||||
className="h-11 w-11 lg:h-10 lg:w-10 flex-shrink-0 -ml-2"
|
||||
className="h-11 w-11 lg:h-10 lg:w-10 flex-shrink-0 -ms-2"
|
||||
aria-label={t('back_to_list')}
|
||||
>
|
||||
<ChevronLeft className="w-5 h-5" />
|
||||
@@ -3513,7 +3513,7 @@ export function EmailViewer({
|
||||
</div>
|
||||
</div>
|
||||
{/* Date/time on the right of subject row - hidden on mobile, shown next to sender */}
|
||||
<div className="hidden sm:block flex-shrink-0 text-right">
|
||||
<div className="hidden sm:block flex-shrink-0 text-end">
|
||||
<span className="text-xs lg:text-sm text-muted-foreground whitespace-nowrap">
|
||||
{formatDateTime(email.receivedAt, timeFormat, { weekday: 'short', year: 'numeric', month: 'short', day: 'numeric' })}
|
||||
</span>
|
||||
@@ -3569,7 +3569,7 @@ export function EmailViewer({
|
||||
name={sender?.name}
|
||||
email={sender.email}
|
||||
onViewContact={handleViewContactSidebar}
|
||||
className="font-semibold text-left"
|
||||
className="font-semibold text-start"
|
||||
/>
|
||||
) : (
|
||||
<span className="font-semibold text-foreground">{t('unknown_sender')}</span>
|
||||
@@ -3633,7 +3633,7 @@ export function EmailViewer({
|
||||
)}
|
||||
<button
|
||||
onClick={() => setShowFullHeaders(!showFullHeaders)}
|
||||
className="text-xs text-muted-foreground hover:text-foreground flex items-center gap-0.5 transition-colors ml-1"
|
||||
className="text-xs text-muted-foreground hover:text-foreground flex items-center gap-0.5 transition-colors ms-1"
|
||||
>
|
||||
{showFullHeaders ? (
|
||||
<>
|
||||
@@ -3756,7 +3756,7 @@ export function EmailViewer({
|
||||
<span className="text-xs text-foreground truncate max-w-[180px]">
|
||||
{getAttachmentDisplayName(attachment.name, attachment.type)}
|
||||
</span>
|
||||
<span className="text-[10px] text-muted-foreground ml-auto flex-shrink-0">
|
||||
<span className="text-[10px] text-muted-foreground ms-auto flex-shrink-0">
|
||||
{formatFileSize(attachment.size)}
|
||||
</span>
|
||||
<div className="absolute inset-y-0 right-0 rounded-r-md bg-background/95 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center gap-1 px-1.5">
|
||||
@@ -3814,7 +3814,7 @@ export function EmailViewer({
|
||||
name={sender?.name}
|
||||
email={sender.email}
|
||||
onViewContact={handleViewContactSidebar}
|
||||
className="text-sm font-semibold text-left"
|
||||
className="text-sm font-semibold text-start"
|
||||
/>
|
||||
) : (
|
||||
<span className="text-sm font-semibold text-foreground">{t('unknown_sender')}</span>
|
||||
@@ -3857,7 +3857,7 @@ export function EmailViewer({
|
||||
)}
|
||||
<button
|
||||
onClick={() => setShowFullHeaders(!showFullHeaders)}
|
||||
className="text-xs text-muted-foreground hover:text-foreground flex items-center gap-0.5 transition-colors ml-1"
|
||||
className="text-xs text-muted-foreground hover:text-foreground flex items-center gap-0.5 transition-colors ms-1"
|
||||
>
|
||||
{showFullHeaders ? (
|
||||
<>
|
||||
@@ -3874,7 +3874,7 @@ export function EmailViewer({
|
||||
</div>
|
||||
</div>
|
||||
{/* Date/time + size on the right (mobile) */}
|
||||
<div className="sm:hidden flex-shrink-0 text-right ml-2">
|
||||
<div className="sm:hidden flex-shrink-0 text-end ms-2">
|
||||
<span className="text-xs text-muted-foreground whitespace-nowrap">
|
||||
{formatDateTime(email.receivedAt, timeFormat, { weekday: 'short', year: 'numeric', month: 'short', day: 'numeric' })}
|
||||
</span>
|
||||
@@ -4006,7 +4006,7 @@ export function EmailViewer({
|
||||
email={sender?.email || ''}
|
||||
displayLabel={sender?.name && sender?.email ? `${sender.name} <${sender.email}>` : undefined}
|
||||
onViewContact={handleViewContactSidebar}
|
||||
className="text-sm text-left"
|
||||
className="text-sm text-start"
|
||||
/>
|
||||
</div>
|
||||
</Row>
|
||||
@@ -4534,7 +4534,7 @@ export function EmailViewer({
|
||||
<span className="text-sm text-foreground truncate max-w-[220px]">
|
||||
{getAttachmentDisplayName(attachment.name, attachment.type)}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground ml-auto flex-shrink-0">
|
||||
<span className="text-xs text-muted-foreground ms-auto flex-shrink-0">
|
||||
{formatFileSize(attachment.size)}
|
||||
</span>
|
||||
<div className="absolute inset-y-0 right-0 rounded-r-md bg-background/95 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center gap-1 px-1.5">
|
||||
@@ -4672,7 +4672,7 @@ export function EmailViewer({
|
||||
<span className="text-xs text-foreground truncate max-w-[180px]">
|
||||
{getAttachmentDisplayName(attachment.name, attachment.type)}
|
||||
</span>
|
||||
<span className="text-[10px] text-muted-foreground ml-auto flex-shrink-0">
|
||||
<span className="text-[10px] text-muted-foreground ms-auto flex-shrink-0">
|
||||
{formatFileSize(attachment.size)}
|
||||
</span>
|
||||
<div className="absolute inset-y-0 right-0 rounded-r-md bg-background/95 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center gap-1 px-1.5">
|
||||
@@ -4815,7 +4815,7 @@ export function EmailViewer({
|
||||
disabled={isSendingQuickReply}
|
||||
className="text-muted-foreground"
|
||||
>
|
||||
<MoreVertical className="w-4 h-4 mr-1" />
|
||||
<MoreVertical className="w-4 h-4 me-1" />
|
||||
{t('more_options')}
|
||||
</Button>
|
||||
<Button
|
||||
@@ -4825,12 +4825,12 @@ export function EmailViewer({
|
||||
>
|
||||
{isSendingQuickReply ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-1 animate-spin" />
|
||||
<Loader2 className="w-4 h-4 me-1 animate-spin" />
|
||||
{t('sending')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Reply className="w-4 h-4 mr-1" />
|
||||
<Reply className="w-4 h-4 me-1" />
|
||||
{t('send')}
|
||||
</>
|
||||
)}
|
||||
@@ -4966,7 +4966,7 @@ export function EmailViewer({
|
||||
<>
|
||||
{/* Collapse toggle when sidebar is collapsed */}
|
||||
{detailSidebarCollapsed && (
|
||||
<div className="flex flex-col items-center border-l border-border bg-background">
|
||||
<div className="flex flex-col items-center border-s border-border bg-background">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
@@ -4990,7 +4990,7 @@ export function EmailViewer({
|
||||
onDoubleClick={() => setDetailSidebarWidth(280)}
|
||||
/>
|
||||
<div
|
||||
className="flex flex-col h-full border-l border-border bg-background overflow-hidden"
|
||||
className="flex flex-col h-full border-s border-border bg-background overflow-hidden"
|
||||
style={{ width: detailSidebarWidth, minWidth: detailSidebarWidth }}
|
||||
>
|
||||
<div className="flex items-center justify-end px-1 py-1 border-b border-border shrink-0">
|
||||
|
||||
@@ -31,7 +31,7 @@ export function ReadReceiptBanner({ requestedBy, onSend, onIgnore }: ReadReceipt
|
||||
<MailCheck className="w-4 h-4 shrink-0 text-amber-600 dark:text-amber-400" />
|
||||
<span className="text-foreground">{t('prompt')}</span>
|
||||
<span className="break-all text-muted-foreground">{requestedBy}</span>
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
<div className="ms-auto flex items-center gap-2">
|
||||
<button
|
||||
onClick={async () => {
|
||||
setState('sending');
|
||||
|
||||
@@ -221,7 +221,7 @@ export function RecipientPopover({ name, email, displayLabel, onViewContact, cla
|
||||
{onViewContact && (
|
||||
<button
|
||||
onClick={handleViewContact}
|
||||
className="flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground px-2 py-1.5 rounded hover:bg-muted transition-colors ml-auto"
|
||||
className="flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground px-2 py-1.5 rounded hover:bg-muted transition-colors ms-auto"
|
||||
title={contact ? "View contact" : "View details"}
|
||||
>
|
||||
{contact ? <ExternalLink className="w-3.5 h-3.5" /> : <UserPlus className="w-3.5 h-3.5" />}
|
||||
|
||||
@@ -8,6 +8,7 @@ import Heading from "@tiptap/extension-heading";
|
||||
import Underline from "@tiptap/extension-underline";
|
||||
import Link from "@tiptap/extension-link";
|
||||
import TextAlign from "@tiptap/extension-text-align";
|
||||
import { TextDirection } from "@/components/email/text-direction";
|
||||
import { TextStyle } from "@tiptap/extension-text-style";
|
||||
import Color from "@tiptap/extension-color";
|
||||
import { ResizableImage } from "@/components/email/resizable-image";
|
||||
@@ -19,6 +20,7 @@ import { TableCell } from "@tiptap/extension-table-cell";
|
||||
import { QuotedHtml, serializeEditorContent } from "@/components/email/quoted-html";
|
||||
import { SignatureBlock } from "@/components/email/signature-block";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useSettingsStore } from "@/stores/settings-store";
|
||||
import {
|
||||
Bold,
|
||||
Italic,
|
||||
@@ -29,6 +31,7 @@ import {
|
||||
AlignLeft,
|
||||
AlignCenter,
|
||||
AlignRight,
|
||||
ArrowLeftRight,
|
||||
Link as LinkIcon,
|
||||
Undo,
|
||||
Redo,
|
||||
@@ -183,6 +186,7 @@ export function RichTextEditor({
|
||||
hasError,
|
||||
onEditorReady,
|
||||
}: RichTextEditorProps) {
|
||||
const rtlEditingSupport = useSettingsStore((st) => st.rtlEditingSupport);
|
||||
const onImageUploadRef = React.useRef(onImageUpload);
|
||||
onImageUploadRef.current = onImageUpload;
|
||||
const onEditorReadyRef = React.useRef(onEditorReady);
|
||||
@@ -240,6 +244,7 @@ export function RichTextEditor({
|
||||
// rich/branded signatures keep their inline styling in the editor and
|
||||
// in the sent mail (see signature-block.ts).
|
||||
SignatureBlock,
|
||||
TextDirection,
|
||||
],
|
||||
content,
|
||||
editorProps: {
|
||||
@@ -454,6 +459,22 @@ export function RichTextEditor({
|
||||
<AlignRight className="w-4 h-4" />
|
||||
</ToolbarButton>
|
||||
|
||||
{rtlEditingSupport && (
|
||||
<ToolbarButton
|
||||
active={
|
||||
(editor.getAttributes("paragraph").dir || editor.getAttributes("heading").dir) === "rtl"
|
||||
}
|
||||
onClick={() => {
|
||||
const cur =
|
||||
editor.getAttributes("paragraph").dir || editor.getAttributes("heading").dir;
|
||||
editor.chain().focus().setTextDirection(cur === "rtl" ? "ltr" : "rtl").run();
|
||||
}}
|
||||
title="Text direction (RTL/LTR)"
|
||||
>
|
||||
<ArrowLeftRight className="w-4 h-4" />
|
||||
</ToolbarButton>
|
||||
)}
|
||||
|
||||
<ToolbarSeparator />
|
||||
|
||||
<ToolbarButton
|
||||
@@ -478,28 +499,28 @@ export function RichTextEditor({
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-2 px-2 py-1.5 text-sm rounded hover:bg-accent text-left"
|
||||
className="flex items-center gap-2 px-2 py-1.5 text-sm rounded hover:bg-accent text-start"
|
||||
onClick={() => { editor.chain().focus().addRowBefore().run(); setTableMenuOpen(false); }}
|
||||
>
|
||||
<Rows3 className="w-4 h-4" /> Add row above
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-2 px-2 py-1.5 text-sm rounded hover:bg-accent text-left"
|
||||
className="flex items-center gap-2 px-2 py-1.5 text-sm rounded hover:bg-accent text-start"
|
||||
onClick={() => { editor.chain().focus().addRowAfter().run(); setTableMenuOpen(false); }}
|
||||
>
|
||||
<Rows3 className="w-4 h-4" /> Add row below
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-2 px-2 py-1.5 text-sm rounded hover:bg-accent text-left"
|
||||
className="flex items-center gap-2 px-2 py-1.5 text-sm rounded hover:bg-accent text-start"
|
||||
onClick={() => { editor.chain().focus().addColumnBefore().run(); setTableMenuOpen(false); }}
|
||||
>
|
||||
<Columns3 className="w-4 h-4" /> Add column before
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-2 px-2 py-1.5 text-sm rounded hover:bg-accent text-left"
|
||||
className="flex items-center gap-2 px-2 py-1.5 text-sm rounded hover:bg-accent text-start"
|
||||
onClick={() => { editor.chain().focus().addColumnAfter().run(); setTableMenuOpen(false); }}
|
||||
>
|
||||
<Columns3 className="w-4 h-4" /> Add column after
|
||||
@@ -507,21 +528,21 @@ export function RichTextEditor({
|
||||
<div className="h-px bg-border my-1" />
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-2 px-2 py-1.5 text-sm rounded hover:bg-accent text-left"
|
||||
className="flex items-center gap-2 px-2 py-1.5 text-sm rounded hover:bg-accent text-start"
|
||||
onClick={() => { editor.chain().focus().deleteRow().run(); setTableMenuOpen(false); }}
|
||||
>
|
||||
<Trash2 className="w-4 h-4" /> Delete row
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-2 px-2 py-1.5 text-sm rounded hover:bg-accent text-left"
|
||||
className="flex items-center gap-2 px-2 py-1.5 text-sm rounded hover:bg-accent text-start"
|
||||
onClick={() => { editor.chain().focus().deleteColumn().run(); setTableMenuOpen(false); }}
|
||||
>
|
||||
<Trash2 className="w-4 h-4" /> Delete column
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-2 px-2 py-1.5 text-sm rounded hover:bg-accent text-left"
|
||||
className="flex items-center gap-2 px-2 py-1.5 text-sm rounded hover:bg-accent text-start"
|
||||
onClick={() => { editor.chain().focus().toggleHeaderRow().run(); setTableMenuOpen(false); }}
|
||||
>
|
||||
<Rows3 className="w-4 h-4" /> Toggle header row
|
||||
@@ -529,7 +550,7 @@ export function RichTextEditor({
|
||||
<div className="h-px bg-border my-1" />
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-2 px-2 py-1.5 text-sm rounded hover:bg-accent text-left text-red-600 dark:text-red-400"
|
||||
className="flex items-center gap-2 px-2 py-1.5 text-sm rounded hover:bg-accent text-start text-red-600 dark:text-red-400"
|
||||
onClick={() => { editor.chain().focus().deleteTable().run(); setTableMenuOpen(false); }}
|
||||
>
|
||||
<Trash2 className="w-4 h-4" /> Delete table
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import { Extension } from "@tiptap/core";
|
||||
|
||||
export type TextDir = "ltr" | "rtl";
|
||||
|
||||
declare module "@tiptap/core" {
|
||||
interface Commands<ReturnType> {
|
||||
textDirection: {
|
||||
setTextDirection: (dir: TextDir) => ReturnType;
|
||||
unsetTextDirection: () => ReturnType;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a `dir` attribute to block nodes so the composer can mark individual
|
||||
* paragraphs/headings as LTR or RTL (Gmail-style right-to-left editing). The
|
||||
* attribute round-trips to HTML so the direction is preserved in the sent mail.
|
||||
*/
|
||||
export const TextDirection = Extension.create({
|
||||
name: "textDirection",
|
||||
|
||||
addOptions() {
|
||||
return { types: ["paragraph", "heading", "blockquote", "listItem"] };
|
||||
},
|
||||
|
||||
addGlobalAttributes() {
|
||||
return [
|
||||
{
|
||||
types: this.options.types,
|
||||
attributes: {
|
||||
dir: {
|
||||
default: null,
|
||||
parseHTML: (element) => element.getAttribute("dir") || null,
|
||||
renderHTML: (attributes) =>
|
||||
attributes.dir ? { dir: attributes.dir } : {},
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
return {
|
||||
setTextDirection:
|
||||
(dir) =>
|
||||
({ commands }) =>
|
||||
this.options.types.every((type: string) =>
|
||||
commands.updateAttributes(type, { dir }),
|
||||
),
|
||||
unsetTextDirection:
|
||||
() =>
|
||||
({ commands }) =>
|
||||
this.options.types.every((type: string) =>
|
||||
commands.resetAttributes(type, "dir"),
|
||||
),
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -150,7 +150,7 @@ export function ThreadConversationView({
|
||||
<div className="flex items-center px-4 border-b border-border bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/80 sticky top-0 z-10" style={{ gap: 'var(--density-item-gap)', paddingBlock: 'var(--density-header-py)' }}>
|
||||
<button
|
||||
onClick={onBack}
|
||||
className="p-2 -ml-2 rounded-full hover:bg-muted transition-colors"
|
||||
className="p-2 -ms-2 rounded-full hover:bg-muted transition-colors"
|
||||
>
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
</button>
|
||||
@@ -488,13 +488,13 @@ function EmailCard({
|
||||
<div className={cn(
|
||||
"rounded-lg border border-border overflow-hidden transition-all duration-200",
|
||||
isExpanded ? "bg-background shadow-sm" : "bg-muted/30",
|
||||
isUnread && !isExpanded && "border-l-2 border-l-primary"
|
||||
isUnread && !isExpanded && "border-s-2 border-l-primary"
|
||||
)}>
|
||||
{/* Card Header - Always visible */}
|
||||
<button
|
||||
onClick={onToggleExpanded}
|
||||
className={cn(
|
||||
"w-full flex items-start text-left transition-colors",
|
||||
"w-full flex items-start text-start transition-colors",
|
||||
!isExpanded && "hover:bg-muted/50"
|
||||
)}
|
||||
style={{ gap: 'var(--density-item-gap)', padding: 'var(--density-card-p)' }}
|
||||
@@ -657,7 +657,7 @@ function EmailCard({
|
||||
}}
|
||||
className="flex-1"
|
||||
>
|
||||
<Reply className="w-4 h-4 mr-2" />
|
||||
<Reply className="w-4 h-4 me-2" />
|
||||
{t("email_viewer.reply")}
|
||||
</Button>
|
||||
)}
|
||||
@@ -671,7 +671,7 @@ function EmailCard({
|
||||
}}
|
||||
className="flex-1"
|
||||
>
|
||||
<ReplyAll className="w-4 h-4 mr-2" />
|
||||
<ReplyAll className="w-4 h-4 me-2" />
|
||||
{t("email_viewer.reply_all")}
|
||||
</Button>
|
||||
)}
|
||||
@@ -685,7 +685,7 @@ function EmailCard({
|
||||
}}
|
||||
className="flex-1"
|
||||
>
|
||||
<Forward className="w-4 h-4 mr-2" />
|
||||
<Forward className="w-4 h-4 me-2" />
|
||||
{t("email_viewer.forward")}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -86,8 +86,8 @@ export function ThreadEmailItem({
|
||||
{...longPressHandlers}
|
||||
className={cn(
|
||||
"relative cursor-pointer select-none transition-all duration-150",
|
||||
"pl-12 pr-4",
|
||||
"border-l-2 border-l-transparent",
|
||||
"ps-12 pe-4",
|
||||
"border-s-2 border-l-transparent",
|
||||
selected
|
||||
? "bg-selection border-l-primary"
|
||||
: "hover:bg-muted/50",
|
||||
@@ -130,7 +130,7 @@ export function ThreadEmailItem({
|
||||
|
||||
{/* Unread indicator */}
|
||||
{isUnread && (
|
||||
<div className="absolute left-7 top-1/2 -translate-y-1/2">
|
||||
<div className="absolute start-7 top-1/2 -translate-y-1/2">
|
||||
<Circle className="w-1.5 h-1.5 fill-unread text-unread" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -218,7 +218,7 @@ const SingleEmailItem = React.forwardRef<HTMLDivElement, SingleEmailItemProps>(
|
||||
)}
|
||||
|
||||
{isUnread && (
|
||||
<div className="absolute left-0.5 top-1/2 -translate-y-1/2">
|
||||
<div className="absolute start-0.5 top-1/2 -translate-y-1/2">
|
||||
<Circle className="w-2 h-2 fill-unread text-unread" />
|
||||
</div>
|
||||
)}
|
||||
@@ -643,7 +643,7 @@ export const ThreadListItem = React.forwardRef<HTMLDivElement, ThreadListItemPro
|
||||
)}
|
||||
|
||||
{hasUnread && (
|
||||
<div className="absolute left-0.5 top-1/2 -translate-y-1/2">
|
||||
<div className="absolute start-0.5 top-1/2 -translate-y-1/2">
|
||||
<Circle className="w-2 h-2 fill-unread text-unread" />
|
||||
</div>
|
||||
)}
|
||||
@@ -896,7 +896,7 @@ export const ThreadListItem = React.forwardRef<HTMLDivElement, ThreadListItemPro
|
||||
<div className="bg-muted/20 animate-in slide-in-from-top-2 duration-200">
|
||||
{isLoading ? (
|
||||
<div className="py-4 flex items-center justify-center text-sm text-muted-foreground">
|
||||
<Loader2 className="w-4 h-4 animate-spin mr-2" />
|
||||
<Loader2 className="w-4 h-4 animate-spin me-2" />
|
||||
{t('loading')}
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -96,7 +96,7 @@ export function UnsubscribeBanner({
|
||||
|
||||
if (success) {
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1 ml-1">
|
||||
<span className="inline-flex items-center gap-1 ms-1">
|
||||
<CheckCircle className="w-3 h-3 text-green-600 dark:text-green-400" />
|
||||
<span className="text-xs text-green-600 dark:text-green-400">
|
||||
{t(unsubMethod === 'http'
|
||||
@@ -110,7 +110,7 @@ export function UnsubscribeBanner({
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1 ml-1">
|
||||
<span className="inline-flex items-center gap-1 ms-1">
|
||||
<AlertCircle className="w-3 h-3 text-red-500 dark:text-red-400" />
|
||||
<button
|
||||
onClick={onDismiss}
|
||||
|
||||
Reference in New Issue
Block a user