feat(i18n): Hebrew locale + full RTL support

This commit is contained in:
Shuki Vaknin
2026-07-08 00:07:34 +02:00
committed by Linus Rath
parent 8904d724bb
commit 22418c17cf
113 changed files with 3958 additions and 494 deletions
+5 -5
View File
@@ -1149,13 +1149,13 @@ export default function CalendarPage() {
</div> </div>
<div className="px-6 py-4 space-y-3"> <div className="px-6 py-4 space-y-3">
<Button variant="outline" className="w-full justify-start h-auto py-3" onClick={handleImportWebcal}> <Button variant="outline" className="w-full justify-start h-auto py-3" onClick={handleImportWebcal}>
<span className="text-left"> <span className="text-start">
<span className="block font-medium">{tWebcalAction("import_title")}</span> <span className="block font-medium">{tWebcalAction("import_title")}</span>
<span className="block text-xs text-muted-foreground mt-0.5">{tWebcalAction("import_description")}</span> <span className="block text-xs text-muted-foreground mt-0.5">{tWebcalAction("import_description")}</span>
</span> </span>
</Button> </Button>
<Button variant="outline" className="w-full justify-start h-auto py-3" onClick={handleSubscribeWebcal}> <Button variant="outline" className="w-full justify-start h-auto py-3" onClick={handleSubscribeWebcal}>
<span className="text-left"> <span className="text-start">
<span className="block font-medium">{tWebcalAction("subscribe_title")}</span> <span className="block font-medium">{tWebcalAction("subscribe_title")}</span>
<span className="block text-xs text-muted-foreground mt-0.5">{tWebcalAction("subscribe_description")}</span> <span className="block text-xs text-muted-foreground mt-0.5">{tWebcalAction("subscribe_description")}</span>
</span> </span>
@@ -1336,7 +1336,7 @@ export default function CalendarPage() {
<> <>
<div <div
className={cn( className={cn(
"border-r border-border bg-secondary overflow-y-auto flex-shrink-0 p-3", "border-e border-border bg-secondary overflow-y-auto flex-shrink-0 p-3",
!isResizing && "transition-[width] duration-300", !isResizing && "transition-[width] duration-300",
isNarrow && cn( isNarrow && cn(
"absolute inset-y-0 left-0 z-50 w-72 pt-[env(safe-area-inset-top)]", "absolute inset-y-0 left-0 z-50 w-72 pt-[env(safe-area-inset-top)]",
@@ -1475,7 +1475,7 @@ export default function CalendarPage() {
{/* Desktop event panel */} {/* Desktop event panel */}
{!isMobile && showEventModal && ( {!isMobile && showEventModal && (
<div className="w-[400px] border-l border-border flex-shrink-0 overflow-hidden"> <div className="w-[400px] border-s border-border flex-shrink-0 overflow-hidden">
<EventModal <EventModal
key={editEvent?.id ?? 'new'} key={editEvent?.id ?? 'new'}
event={editEvent} event={editEvent}
@@ -1498,7 +1498,7 @@ export default function CalendarPage() {
{/* Desktop task panel */} {/* Desktop task panel */}
{!isMobile && showTaskModal && ( {!isMobile && showTaskModal && (
<div className="w-[400px] border-l border-border flex-shrink-0 overflow-hidden"> <div className="w-[400px] border-s border-border flex-shrink-0 overflow-hidden">
<TaskModal <TaskModal
key={editTask?.id ?? 'new-task'} key={editTask?.id ?? 'new-task'}
task={editTask} task={editTask}
+4 -4
View File
@@ -751,7 +751,7 @@ export default function ContactsPage() {
<button <button
key={group.id} key={group.id}
onClick={() => handleBulkAddToGroupConfirm(group.id)} onClick={() => handleBulkAddToGroupConfirm(group.id)}
className="w-full flex items-center gap-3 px-6 py-3 text-left hover:bg-muted transition-colors" className="w-full flex items-center gap-3 px-6 py-3 text-start hover:bg-muted transition-colors"
> >
<div className="w-9 h-9 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0"> <div className="w-9 h-9 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0">
<Users className="w-4 h-4 text-primary" /> <Users className="w-4 h-4 text-primary" />
@@ -869,7 +869,7 @@ export default function ContactsPage() {
<> <>
<div <div
className={cn( className={cn(
"border-r border-border flex flex-col flex-shrink-0 bg-background", "border-e border-border flex flex-col flex-shrink-0 bg-background",
!isSidebarResizing && "transition-[width] duration-300", !isSidebarResizing && "transition-[width] duration-300",
isNarrow && cn( isNarrow && cn(
"absolute inset-y-0 left-0 z-50 w-72 pt-[env(safe-area-inset-top)]", "absolute inset-y-0 left-0 z-50 w-72 pt-[env(safe-area-inset-top)]",
@@ -937,7 +937,7 @@ export default function ContactsPage() {
<div <div
data-tour="contacts-list" data-tour="contacts-list"
className={cn( className={cn(
"border-r border-border bg-background flex flex-col flex-shrink-0", "border-e border-border bg-background flex flex-col flex-shrink-0",
isMobile ? "w-full" : "", isMobile ? "w-full" : "",
!isListResizing && !isMobile && "transition-[width] duration-300" !isListResizing && !isMobile && "transition-[width] duration-300"
)} )}
@@ -991,7 +991,7 @@ export default function ContactsPage() {
onClick={mobileBackToList} onClick={mobileBackToList}
className="touch-manipulation" className="touch-manipulation"
> >
<ArrowLeft className="w-4 h-4 mr-2" /> <ArrowLeft className="w-4 h-4 me-2" />
{returnToEmail ? t("back_to_email") : t("back_to_contacts")} {returnToEmail ? t("back_to_email") : t("back_to_contacts")}
</Button> </Button>
</div> </div>
+2 -2
View File
@@ -38,11 +38,11 @@ export default function LocaleError({
</p> </p>
<div className="flex gap-3 justify-center"> <div className="flex gap-3 justify-center">
<Button variant="outline" onClick={() => router.push('/')}> <Button variant="outline" onClick={() => router.push('/')}>
<Home className="w-4 h-4 mr-2" /> <Home className="w-4 h-4 me-2" />
{t("go_home")} {t("go_home")}
</Button> </Button>
<Button onClick={reset}> <Button onClick={reset}>
<RefreshCw className="w-4 h-4 mr-2" /> <RefreshCw className="w-4 h-4 me-2" />
{t("try_again")} {t("try_again")}
</Button> </Button>
</div> </div>
+2 -2
View File
@@ -477,7 +477,7 @@ export default function FilesPage() {
onClick={() => router.push("/")} onClick={() => router.push("/")}
className="justify-start" className="justify-start"
> >
<ArrowLeft className="w-4 h-4 mr-2" /> <ArrowLeft className="w-4 h-4 me-2" />
{t("title")} {t("title")}
</Button> </Button>
</div> </div>
@@ -611,7 +611,7 @@ export default function FilesPage() {
: '0%' }} : '0%' }}
/> />
</div> </div>
<p className="mt-2 text-xs text-muted-foreground tabular-nums text-right"> <p className="mt-2 text-xs text-muted-foreground tabular-nums text-end">
{migrationProgress.current} / {migrationProgress.total} {migrationProgress.current} / {migrationProgress.total}
</p> </p>
</div> </div>
+5 -5
View File
@@ -725,7 +725,7 @@ export default function LoginPage() {
)} )}
> >
<Icon className="w-4 h-4" /> <Icon className="w-4 h-4" />
<span className="flex-1 text-left">{option.label}</span> <span className="flex-1 text-start">{option.label}</span>
{isActive && <Check className="w-3.5 h-3.5 text-primary" />} {isActive && <Check className="w-3.5 h-3.5 text-primary" />}
</button> </button>
); );
@@ -874,7 +874,7 @@ export default function LoginPage() {
)} )}
> >
<Icon className="w-4 h-4" /> <Icon className="w-4 h-4" />
<span className="flex-1 text-left">{option.label}</span> <span className="flex-1 text-start">{option.label}</span>
{isActive && <Check className="w-3.5 h-3.5 text-primary" />} {isActive && <Check className="w-3.5 h-3.5 text-primary" />}
</button> </button>
); );
@@ -1133,7 +1133,7 @@ export default function LoginPage() {
type={showPassword ? "text" : "password"} type={showPassword ? "text" : "password"}
value={formData.password} value={formData.password}
onChange={(e) => setFormData({ ...formData, password: e.target.value })} onChange={(e) => setFormData({ ...formData, password: e.target.value })}
className="h-11 px-3.5 pr-11 bg-muted/40 border-border/60 rounded-xl focus:bg-background focus:border-primary/50 transition-all duration-200" className="h-11 px-3.5 pe-11 bg-muted/40 border-border/60 rounded-xl focus:bg-background focus:border-primary/50 transition-all duration-200"
placeholder={t("password_placeholder")} placeholder={t("password_placeholder")}
required required
autoComplete="current-password" autoComplete="current-password"
@@ -1253,9 +1253,9 @@ export default function LoginPage() {
disabled={oauthLoading || isLoading} disabled={oauthLoading || isLoading}
> >
{oauthLoading ? ( {oauthLoading ? (
<Loader2 className="w-4 h-4 animate-spin mr-2" /> <Loader2 className="w-4 h-4 animate-spin me-2" />
) : ( ) : (
<LogIn className="w-4 h-4 mr-2" /> <LogIn className="w-4 h-4 me-2" />
)} )}
{t("sign_in_sso")} {t("sign_in_sso")}
</Button> </Button>
+13 -13
View File
@@ -75,7 +75,7 @@ import { appLifecycleHooks, uiHooks, routerHooks, toastHooks, emailHooks } from
import { emailToReadView } from "@/lib/plugin-projection"; import { emailToReadView } from "@/lib/plugin-projection";
import { buildQuoteHeader } from "@/lib/quote-header"; import { buildQuoteHeader } from "@/lib/quote-header";
import { buildReplySubject, buildForwardSubject } from "@/lib/subject-prefix"; import { buildReplySubject, buildForwardSubject } from "@/lib/subject-prefix";
import { useLocaleStore } from "@/stores/locale-store"; import { getEffectiveLocale } from '@/i18n/detect-locale';
import type { QuoteHeader } from "@/lib/plugin-types"; import type { QuoteHeader } from "@/lib/plugin-types";
const SCHEDULED_MAILBOX_ID = '__scheduled__'; const SCHEDULED_MAILBOX_ID = '__scheduled__';
@@ -1280,7 +1280,7 @@ export default function Home() {
}, },
newTo, newTo,
newCc, newCc,
locale: useLocaleStore.getState().locale, locale: getEffectiveLocale(),
timeFormat: useSettingsStore.getState().timeFormat, timeFormat: useSettingsStore.getState().timeFormat,
unknownLabel: tCommon('unknown'), unknownLabel: tCommon('unknown'),
labels: { labels: {
@@ -2761,17 +2761,17 @@ export default function Home() {
"relative flex flex-col bg-background", "relative flex flex-col bg-background",
isHorizontalMailLayout isHorizontalMailLayout
? (shouldHideHorizontalViewerPane ? "md:w-full md:min-h-0" : "md:w-full md:h-auto") ? (shouldHideHorizontalViewerPane ? "md:w-full md:min-h-0" : "md:w-full md:h-auto")
: "h-full border-r border-border", : "h-full border-e border-border",
// Mobile: full width, hidden when viewing email // Mobile: full width, hidden when viewing email
"max-md:flex-1 max-md:border-r-0 max-md:border-b-0", "max-md:flex-1 max-md:border-e-0 max-md:border-b-0",
isMobile && activeView !== "list" && "max-md:hidden", isMobile && activeView !== "list" && "max-md:hidden",
// Tablet/Desktop: fixed width with collapse animation // Tablet/Desktop: fixed width with collapse animation
!isHorizontalMailLayout && (shouldHideViewerPane ? "md:flex-1 md:border-r-0" : "md:flex-shrink-0"), !isHorizontalMailLayout && (shouldHideViewerPane ? "md:flex-1 md:border-e-0" : "md:flex-shrink-0"),
isHorizontalMailLayout && (shouldHideHorizontalViewerPane ? "md:flex-1" : "md:flex-shrink-0"), isHorizontalMailLayout && (shouldHideHorizontalViewerPane ? "md:flex-1" : "md:flex-shrink-0"),
isHorizontalMailLayout && !shouldHideHorizontalViewerPane && "md:shadow-[0_8px_12px_-6px_rgba(0,0,0,0.18)] dark:md:shadow-[0_8px_14px_-6px_rgba(0,0,0,0.55)]", isHorizontalMailLayout && !shouldHideHorizontalViewerPane && "md:shadow-[0_8px_12px_-6px_rgba(0,0,0,0.18)] dark:md:shadow-[0_8px_14px_-6px_rgba(0,0,0,0.55)]",
!isHorizontalMailLayout && "md:shadow-sm", !isHorizontalMailLayout && "md:shadow-sm",
!isResizing && "transition-all duration-200 ease-out", !isResizing && "transition-all duration-200 ease-out",
shouldCollapseListPane && "md:w-0 md:opacity-0 md:overflow-hidden md:border-r-0" shouldCollapseListPane && "md:w-0 md:opacity-0 md:overflow-hidden md:border-e-0"
)} )}
style={ style={
isMobile isMobile
@@ -2824,13 +2824,13 @@ export default function Home() {
)} )}
</button> </button>
<form onSubmit={(e) => { e.preventDefault(); if (searchQuery.trim()) handleSearch(searchQuery); }} className="relative flex-1"> <form onSubmit={(e) => { e.preventDefault(); if (searchQuery.trim()) handleSearch(searchQuery); }} className="relative flex-1">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground" /> <Search className="absolute start-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<Input <Input
type="text" type="text"
placeholder={t("sidebar.search_placeholder_hint")} placeholder={t("sidebar.search_placeholder_hint")}
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
className={cn("pl-9 h-9", searchQuery && "pr-8")} className={cn("ps-9 h-9", searchQuery && "pe-8")}
data-search-input data-search-input
data-tour="search-input" data-tour="search-input"
disabled={isUnifiedView || isScheduledView} disabled={isUnifiedView || isScheduledView}
@@ -2840,7 +2840,7 @@ export default function Home() {
<button <button
type="button" type="button"
onClick={handleClearSearch} onClick={handleClearSearch}
className="absolute right-2 top-1/2 transform -translate-y-1/2 p-1 rounded-full hover:bg-muted text-muted-foreground hover:text-foreground transition-colors" className="absolute end-2 top-1/2 transform -translate-y-1/2 p-1 rounded-full hover:bg-muted text-muted-foreground hover:text-foreground transition-colors"
aria-label={t("sidebar.clear_search")} aria-label={t("sidebar.clear_search")}
> >
<X className="w-4 h-4" /> <X className="w-4 h-4" />
@@ -2897,7 +2897,7 @@ export default function Home() {
</div> </div>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<Button variant="ghost" size="sm" onClick={() => { clearSearchFilters(); setShowAdvancedFields(false); if (client) advancedSearch(client); }} className="h-7 px-2 text-xs text-muted-foreground"> <Button variant="ghost" size="sm" onClick={() => { clearSearchFilters(); setShowAdvancedFields(false); if (client) advancedSearch(client); }} className="h-7 px-2 text-xs text-muted-foreground">
<RotateCcw className="w-3 h-3 mr-1" /> <RotateCcw className="w-3 h-3 me-1" />
{t("advanced_search.clear")} {t("advanced_search.clear")}
</Button> </Button>
</div> </div>
@@ -3122,7 +3122,7 @@ export default function Home() {
}} }}
className={cn( className={cn(
"absolute z-40 rounded-full shadow-lg", "absolute z-40 rounded-full shadow-lg",
isMobile ? "bottom-4 right-4 h-14 w-14" : "bottom-4 right-4 h-12 w-12" isMobile ? "bottom-4 end-4 h-14 w-14" : "bottom-4 end-4 h-12 w-12"
)} )}
aria-label={t('sidebar.compose')} aria-label={t('sidebar.compose')}
title={t('sidebar.compose_hint')} title={t('sidebar.compose_hint')}
@@ -3250,13 +3250,13 @@ export default function Home() {
setShowComposer(true); setShowComposer(true);
if (isMobile) setActiveView('viewer'); if (isMobile) setActiveView('viewer');
}} }}
className="flex items-center gap-3 px-4 py-2.5 bg-primary/10 border-b border-primary/20 hover:bg-primary/15 transition-colors cursor-pointer w-full text-left" className="flex items-center gap-3 px-4 py-2.5 bg-primary/10 border-b border-primary/20 hover:bg-primary/15 transition-colors cursor-pointer w-full text-start"
> >
<PenLine className="w-4 h-4 text-primary shrink-0" /> <PenLine className="w-4 h-4 text-primary shrink-0" />
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<span className="text-sm font-medium text-primary">{t('email_composer.continue_draft')}</span> <span className="text-sm font-medium text-primary">{t('email_composer.continue_draft')}</span>
{pendingDraft.subject && ( {pendingDraft.subject && (
<span className="text-xs text-muted-foreground ml-2 truncate">{pendingDraft.subject}</span> <span className="text-xs text-muted-foreground ms-2 truncate">{pendingDraft.subject}</span>
)} )}
</div> </div>
<X <X
+9 -9
View File
@@ -712,11 +712,11 @@ export default function SettingsPage() {
clearManagedAccount(); clearManagedAccount();
handleTabSelect('account'); handleTabSelect('account');
}} }}
className="flex items-center gap-2 w-full mb-4 px-3 py-2 rounded-md border border-border bg-muted/40 hover:bg-muted text-left transition-colors" className="flex items-center gap-2 w-full mb-4 px-3 py-2 rounded-md border border-border bg-muted/40 hover:bg-muted text-start transition-colors"
> >
<ArrowLeft className="w-4 h-4 text-muted-foreground flex-shrink-0" /> <ArrowLeft className="w-4 h-4 text-muted-foreground flex-shrink-0" />
<span className="text-sm text-muted-foreground">{t('scoped.back')}</span> <span className="text-sm text-muted-foreground">{t('scoped.back')}</span>
<span className="ml-auto text-sm font-medium truncate"> <span className="ms-auto text-sm font-medium truncate">
{t('scoped.managing', { name: managedAccount.name })} {t('scoped.managing', { name: managedAccount.name })}
</span> </span>
</button> </button>
@@ -820,7 +820,7 @@ export default function SettingsPage() {
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
placeholder={t('search_placeholder')} placeholder={t('search_placeholder')}
className="pl-9 pr-9 h-10" className="ps-9 pe-9 h-10"
aria-label={t('search_placeholder')} aria-label={t('search_placeholder')}
/> />
{searchQuery && ( {searchQuery && (
@@ -868,7 +868,7 @@ export default function SettingsPage() {
<button <button
key={`${tab.id}:${sub.label}`} key={`${tab.id}:${sub.label}`}
onClick={() => handleSubResultSelect(tab.id, sub)} onClick={() => handleSubResultSelect(tab.id, sub)}
className="w-full flex items-center pl-12 pr-5 py-2 text-xs text-muted-foreground hover:bg-muted hover:text-foreground transition-colors duration-150 text-left" className="w-full flex items-center ps-12 pe-5 py-2 text-xs text-muted-foreground hover:bg-muted hover:text-foreground transition-colors duration-150 text-start"
> >
<span className="truncate">{sub.label}</span> <span className="truncate">{sub.label}</span>
</button> </button>
@@ -932,7 +932,7 @@ export default function SettingsPage() {
<> <>
<div <div
className={cn( className={cn(
"border-r border-border bg-secondary flex flex-col", "border-e border-border bg-secondary flex flex-col",
!isResizing && "transition-[width] duration-300" !isResizing && "transition-[width] duration-300"
)} )}
style={{ width: `${settingsSidebarWidth}px` }} style={{ width: `${settingsSidebarWidth}px` }}
@@ -945,7 +945,7 @@ export default function SettingsPage() {
onClick={() => router.push('/')} onClick={() => router.push('/')}
className="w-full justify-start" className="w-full justify-start"
> >
<ArrowLeft className="w-4 h-4 mr-2" /> <ArrowLeft className="w-4 h-4 me-2" />
{t('back_to_mail')} {t('back_to_mail')}
</Button> </Button>
</div> </div>
@@ -960,7 +960,7 @@ export default function SettingsPage() {
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
placeholder={t('search_placeholder')} placeholder={t('search_placeholder')}
className="pl-8 pr-8 h-9 text-sm" className="ps-8 pe-8 h-9 text-sm"
aria-label={t('search_placeholder')} aria-label={t('search_placeholder')}
/> />
{searchQuery && ( {searchQuery && (
@@ -997,7 +997,7 @@ export default function SettingsPage() {
<button <button
onClick={() => handleTabSelect(tab.id)} onClick={() => handleTabSelect(tab.id)}
className={cn( className={cn(
'w-full text-left px-3 py-2 rounded-md text-sm transition-colors duration-150 flex items-center gap-2.5', 'w-full text-start px-3 py-2 rounded-md text-sm transition-colors duration-150 flex items-center gap-2.5',
effectiveActiveTab === tab.id effectiveActiveTab === tab.id
? 'bg-accent text-accent-foreground font-medium' ? 'bg-accent text-accent-foreground font-medium'
: 'hover:bg-muted text-foreground' : 'hover:bg-muted text-foreground'
@@ -1013,7 +1013,7 @@ export default function SettingsPage() {
<button <button
key={`${tab.id}:${sub.label}`} key={`${tab.id}:${sub.label}`}
onClick={() => handleSubResultSelect(tab.id, sub)} onClick={() => handleSubResultSelect(tab.id, sub)}
className="w-full text-left pl-9 pr-3 py-1.5 rounded-md text-xs text-muted-foreground hover:bg-muted hover:text-foreground transition-colors duration-150" className="w-full text-start ps-9 pe-3 py-1.5 rounded-md text-xs text-muted-foreground hover:bg-muted hover:text-foreground transition-colors duration-150"
> >
<span className="truncate block">{sub.label}</span> <span className="truncate block">{sub.label}</span>
</button> </button>
@@ -220,7 +220,7 @@ export function JmapServersSection({ value, source, onChange, onRevert }: Props)
Per-server OAuth (optional, overrides global) Per-server OAuth (optional, overrides global)
</button> </button>
{d.oauthExpanded && ( {d.oauthExpanded && (
<div className="grid grid-cols-1 sm:grid-cols-3 gap-2 pl-4 border-l border-border"> <div className="grid grid-cols-1 sm:grid-cols-3 gap-2 ps-4 border-s border-border">
<div> <div>
<label className="block text-[11px] font-medium text-muted-foreground mb-1">OAuth Client ID</label> <label className="block text-[11px] font-medium text-muted-foreground mb-1">OAuth Client ID</label>
<input <input
+4 -4
View File
@@ -96,10 +96,10 @@ export function LogsTab() {
<table className="w-full text-sm"> <table className="w-full text-sm">
<thead> <thead>
<tr className="border-b border-border bg-muted/30"> <tr className="border-b border-border bg-muted/30">
<th className="text-left px-4 py-2 font-medium text-muted-foreground whitespace-nowrap">Time</th> <th className="text-start px-4 py-2 font-medium text-muted-foreground whitespace-nowrap">Time</th>
<th className="text-left px-4 py-2 font-medium text-muted-foreground whitespace-nowrap">Action</th> <th className="text-start px-4 py-2 font-medium text-muted-foreground whitespace-nowrap">Action</th>
<th className="text-left px-4 py-2 font-medium text-muted-foreground">Details</th> <th className="text-start px-4 py-2 font-medium text-muted-foreground">Details</th>
<th className="text-left px-4 py-2 font-medium text-muted-foreground whitespace-nowrap">IP</th> <th className="text-start px-4 py-2 font-medium text-muted-foreground whitespace-nowrap">IP</th>
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-border"> <tbody className="divide-y divide-border">
+2 -2
View File
@@ -171,7 +171,7 @@ export function MarketplaceTab() {
placeholder="Search extensions..." placeholder="Search extensions..."
value={searchInput} value={searchInput}
onChange={(e) => setSearchInput(e.target.value)} onChange={(e) => setSearchInput(e.target.value)}
className="w-full h-9 pl-9 pr-3 rounded-md border border-input bg-background text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring/20 focus:border-ring" className="w-full h-9 ps-9 pe-3 rounded-md border border-input bg-background text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring/20 focus:border-ring"
/> />
</div> </div>
<div className="flex items-center gap-1 rounded-md border border-input bg-background p-0.5 self-start sm:self-auto"> <div className="flex items-center gap-1 rounded-md border border-input bg-background p-0.5 self-start sm:self-auto">
@@ -210,7 +210,7 @@ export function MarketplaceTab() {
{loading && !error && ( {loading && !error && (
<div className="flex items-center justify-center py-12"> <div className="flex items-center justify-center py-12">
<Loader2 className="w-5 h-5 animate-spin text-muted-foreground" /> <Loader2 className="w-5 h-5 animate-spin text-muted-foreground" />
<span className="ml-2 text-sm text-muted-foreground">Searching extensions...</span> <span className="ms-2 text-sm text-muted-foreground">Searching extensions...</span>
</div> </div>
)} )}
@@ -155,7 +155,7 @@ export function PluginConfigPanel({ pluginId, onBack }: Props) {
if (loading) { if (loading) {
return ( return (
<div className="flex items-center justify-center py-12 text-muted-foreground text-sm"> <div className="flex items-center justify-center py-12 text-muted-foreground text-sm">
<Loader2 className="w-4 h-4 animate-spin mr-2" /> <Loader2 className="w-4 h-4 animate-spin me-2" />
Loading... Loading...
</div> </div>
); );
@@ -217,7 +217,7 @@ export function PluginConfigPanel({ pluginId, onBack }: Props) {
<div key={key}> <div key={key}>
<label className="text-sm font-medium text-foreground block mb-1"> <label className="text-sm font-medium text-foreground block mb-1">
{field.label} {field.label}
{field.required && <span className="text-destructive ml-0.5">*</span>} {field.required && <span className="text-destructive ms-0.5">*</span>}
</label> </label>
{field.description && ( {field.description && (
<p className="text-xs text-muted-foreground mb-1.5">{field.description}</p> <p className="text-xs text-muted-foreground mb-1.5">{field.description}</p>
@@ -250,7 +250,7 @@ export function PluginConfigPanel({ pluginId, onBack }: Props) {
value={formValues[key] ?? ''} value={formValues[key] ?? ''}
onChange={(e) => setFormValues(prev => ({ ...prev, [key]: e.target.value }))} onChange={(e) => setFormValues(prev => ({ ...prev, [key]: e.target.value }))}
placeholder={config[key] ? '•••••••• (unchanged)' : (field.placeholder || '')} placeholder={config[key] ? '•••••••• (unchanged)' : (field.placeholder || '')}
className="w-full h-9 px-3 pr-10 rounded-md border border-input bg-background text-sm focus:outline-none focus:ring-2 focus:ring-ring font-mono" className="w-full h-9 px-3 pe-10 rounded-md border border-input bg-background text-sm focus:outline-none focus:ring-2 focus:ring-ring font-mono"
/> />
<button <button
type="button" type="button"
+2 -2
View File
@@ -117,7 +117,7 @@ export function SettingsTab() {
<TextSetting label="JMAP Server URL" configKey="jmapServerUrl" value={currentValue('jmapServerUrl') as string} source={config.jmapServerUrl?.source} onChange={handleChange} onRevert={handleRevert} placeholder="https://mail.example.com" /> <TextSetting label="JMAP Server URL" configKey="jmapServerUrl" value={currentValue('jmapServerUrl') as string} source={config.jmapServerUrl?.source} onChange={handleChange} onRevert={handleRevert} placeholder="https://mail.example.com" />
<ToggleSetting label="Allow Custom JMAP Endpoint" description="Show a JMAP server URL field on the login form, allowing users to connect to any JMAP server" configKey="allowCustomJmapEndpoint" value={currentValue('allowCustomJmapEndpoint') as boolean} source={config.allowCustomJmapEndpoint?.source} onChange={handleChange} onRevert={handleRevert} /> <ToggleSetting label="Allow Custom JMAP Endpoint" description="Show a JMAP server URL field on the login form, allowing users to connect to any JMAP server" configKey="allowCustomJmapEndpoint" value={currentValue('allowCustomJmapEndpoint') as boolean} source={config.allowCustomJmapEndpoint?.source} onChange={handleChange} onRevert={handleRevert} />
{!!currentValue('allowCustomJmapEndpoint') && ( {!!currentValue('allowCustomJmapEndpoint') && (
<div className="px-4 py-2.5 bg-amber-50 dark:bg-amber-950/30 border-l-2 border-amber-400 dark:border-amber-600"> <div className="px-4 py-2.5 bg-amber-50 dark:bg-amber-950/30 border-s-2 border-amber-400 dark:border-amber-600">
<p className="text-xs text-amber-800 dark:text-amber-300 leading-relaxed"> <p className="text-xs text-amber-800 dark:text-amber-300 leading-relaxed">
<strong>CORS warning:</strong> External JMAP servers must include this domain in their CORS <code className="text-[11px] bg-amber-100 dark:bg-amber-900/50 px-1 py-0.5 rounded">Access-Control-Allow-Origin</code> header, or requests from the browser will be blocked. <strong>CORS warning:</strong> External JMAP servers must include this domain in their CORS <code className="text-[11px] bg-amber-100 dark:bg-amber-900/50 px-1 py-0.5 rounded">Access-Control-Allow-Origin</code> header, or requests from the browser will be blocked.
</p> </p>
@@ -145,7 +145,7 @@ export function SettingsTab() {
onRevert={() => handleRevert('jmapServers')} onRevert={() => handleRevert('jmapServers')}
/> />
{Array.isArray(currentValue('jmapServers')) && (currentValue('jmapServers') as JmapServerEntry[]).length > 0 && ( {Array.isArray(currentValue('jmapServers')) && (currentValue('jmapServers') as JmapServerEntry[]).length > 0 && (
<div className="px-4 py-2.5 bg-amber-50 dark:bg-amber-950/30 border-l-2 border-amber-400 dark:border-amber-600"> <div className="px-4 py-2.5 bg-amber-50 dark:bg-amber-950/30 border-s-2 border-amber-400 dark:border-amber-600">
<p className="text-xs text-amber-800 dark:text-amber-300 leading-relaxed"> <p className="text-xs text-amber-800 dark:text-amber-300 leading-relaxed">
<strong>CORS warning:</strong> Each JMAP server must allow this webmail's origin in its <code className="text-[11px] bg-amber-100 dark:bg-amber-900/50 px-1 py-0.5 rounded">Access-Control-Allow-Origin</code> header, or browser requests will be blocked. <strong>CORS warning:</strong> Each JMAP server must allow this webmail's origin in its <code className="text-[11px] bg-amber-100 dark:bg-amber-900/50 px-1 py-0.5 rounded">Access-Control-Allow-Origin</code> header, or browser requests will be blocked.
</p> </p>
+1 -1
View File
@@ -65,7 +65,7 @@ export default function ChangePasswordPage() {
value={currentPassword} value={currentPassword}
onChange={e => setCurrentPassword(e.target.value)} onChange={e => setCurrentPassword(e.target.value)}
required required
className="w-full h-9 pl-9 pr-3 rounded-md border border-input bg-background text-sm text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" className="w-full h-9 ps-9 pe-3 rounded-md border border-input bg-background text-sm text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
autoComplete="current-password" autoComplete="current-password"
/> />
</div> </div>
+12 -12
View File
@@ -212,7 +212,7 @@ export default function AdminLayout({ children }: { children: React.ReactNode })
type="button" type="button"
onClick={handleClick} onClick={handleClick}
className={cn( className={cn(
'w-full text-left px-3 py-2 rounded-md text-sm transition-colors duration-150 flex items-center gap-2.5', 'w-full text-start px-3 py-2 rounded-md text-sm transition-colors duration-150 flex items-center gap-2.5',
active active
? 'bg-accent text-accent-foreground font-medium' ? 'bg-accent text-accent-foreground font-medium'
: 'hover:bg-muted text-foreground' : 'hover:bg-muted text-foreground'
@@ -248,7 +248,7 @@ export default function AdminLayout({ children }: { children: React.ReactNode })
<Link <Link
href="/admin/change-password" href="/admin/change-password"
className={cn( className={cn(
'w-full text-left px-3 py-2 rounded-md text-sm transition-colors duration-150 flex items-center gap-2.5', 'w-full text-start px-3 py-2 rounded-md text-sm transition-colors duration-150 flex items-center gap-2.5',
pathname === '/admin/change-password' pathname === '/admin/change-password'
? 'bg-accent text-accent-foreground font-medium' ? 'bg-accent text-accent-foreground font-medium'
: 'hover:bg-muted text-foreground' : 'hover:bg-muted text-foreground'
@@ -263,7 +263,7 @@ export default function AdminLayout({ children }: { children: React.ReactNode })
)} )}
<button <button
onClick={handleLogout} onClick={handleLogout}
className="w-full text-left px-3 py-2 rounded-md text-sm transition-colors duration-150 flex items-center gap-2.5 hover:bg-muted text-foreground" className="w-full text-start px-3 py-2 rounded-md text-sm transition-colors duration-150 flex items-center gap-2.5 hover:bg-muted text-foreground"
> >
<LogOut className="w-4 h-4 shrink-0 text-muted-foreground" /> <LogOut className="w-4 h-4 shrink-0 text-muted-foreground" />
Sign out Sign out
@@ -275,7 +275,7 @@ export default function AdminLayout({ children }: { children: React.ReactNode })
return ( return (
<div className="min-h-screen flex bg-background"> <div className="min-h-screen flex bg-background">
{/* Slim webmail nav rail (desktop only) */} {/* Slim webmail nav rail (desktop only) */}
<nav className="hidden md:flex w-14 bg-secondary flex-col items-center py-3 gap-2 border-r border-border sticky top-0 h-screen shrink-0"> <nav className="hidden md:flex w-14 bg-secondary flex-col items-center py-3 gap-2 border-e border-border sticky top-0 h-screen shrink-0">
{logoUrl ? ( {logoUrl ? (
<img src={logoUrl} alt="" className="w-7 h-7 object-contain mb-2" /> <img src={logoUrl} alt="" className="w-7 h-7 object-contain mb-2" />
) : ( ) : (
@@ -326,12 +326,12 @@ export default function AdminLayout({ children }: { children: React.ReactNode })
</nav> </nav>
{/* Admin Sidebar (desktop only) */} {/* Admin Sidebar (desktop only) */}
<aside className="hidden md:flex w-60 border-r border-border bg-secondary flex-col sticky top-0 h-screen"> <aside className="hidden md:flex w-60 border-e border-border bg-secondary flex-col sticky top-0 h-screen">
<div className="h-14 flex items-center px-4 border-b border-border shrink-0"> <div className="h-14 flex items-center px-4 border-b border-border shrink-0">
{logoUrl ? ( {logoUrl ? (
<img src={logoUrl} alt="" className="w-5 h-5 object-contain mr-2" /> <img src={logoUrl} alt="" className="w-5 h-5 object-contain me-2" />
) : ( ) : (
<Shield className="w-5 h-5 text-primary mr-2" /> <Shield className="w-5 h-5 text-primary me-2" />
)} )}
<span className="font-semibold text-sm text-foreground">Admin Panel</span> <span className="font-semibold text-sm text-foreground">Admin Panel</span>
</div> </div>
@@ -350,7 +350,7 @@ export default function AdminLayout({ children }: { children: React.ReactNode })
{/* Mobile drawer */} {/* Mobile drawer */}
<aside <aside
className={cn( className={cn(
'md:hidden fixed inset-y-0 left-0 z-50 w-72 max-w-[85vw] border-r border-border bg-secondary flex flex-col transition-transform duration-200 ease-out', 'md:hidden fixed inset-y-0 left-0 z-50 w-72 max-w-[85vw] border-e border-border bg-secondary flex flex-col transition-transform duration-200 ease-out',
mobileNavOpen ? 'translate-x-0' : '-translate-x-full' mobileNavOpen ? 'translate-x-0' : '-translate-x-full'
)} )}
aria-label="Admin navigation" aria-label="Admin navigation"
@@ -359,9 +359,9 @@ export default function AdminLayout({ children }: { children: React.ReactNode })
<div className="h-14 flex items-center justify-between px-3 border-b border-border shrink-0"> <div className="h-14 flex items-center justify-between px-3 border-b border-border shrink-0">
<div className="flex items-center min-w-0"> <div className="flex items-center min-w-0">
{logoUrl ? ( {logoUrl ? (
<img src={logoUrl} alt="" className="w-5 h-5 object-contain mr-2" /> <img src={logoUrl} alt="" className="w-5 h-5 object-contain me-2" />
) : ( ) : (
<Shield className="w-5 h-5 text-primary mr-2" /> <Shield className="w-5 h-5 text-primary me-2" />
)} )}
<span className="font-semibold text-sm text-foreground truncate">Admin Panel</span> <span className="font-semibold text-sm text-foreground truncate">Admin Panel</span>
</div> </div>
@@ -391,9 +391,9 @@ export default function AdminLayout({ children }: { children: React.ReactNode })
</button> </button>
<div className="flex items-center min-w-0"> <div className="flex items-center min-w-0">
{logoUrl ? ( {logoUrl ? (
<img src={logoUrl} alt="" className="w-5 h-5 object-contain mr-2" /> <img src={logoUrl} alt="" className="w-5 h-5 object-contain me-2" />
) : ( ) : (
<Shield className="w-5 h-5 text-primary mr-2" /> <Shield className="w-5 h-5 text-primary me-2" />
)} )}
<span className="font-semibold text-sm text-foreground truncate">Admin Panel</span> <span className="font-semibold text-sm text-foreground truncate">Admin Panel</span>
</div> </div>
+3 -3
View File
@@ -186,7 +186,7 @@ export default function MarketplacePreviewPage() {
if (loading) { if (loading) {
return ( return (
<div className="flex items-center justify-center py-12 text-muted-foreground text-sm"> <div className="flex items-center justify-center py-12 text-muted-foreground text-sm">
<Loader2 className="w-4 h-4 animate-spin mr-2" /> <Loader2 className="w-4 h-4 animate-spin me-2" />
Loading... Loading...
</div> </div>
); );
@@ -512,7 +512,7 @@ export default function MarketplacePreviewPage() {
<section className="border border-border rounded-lg"> <section className="border border-border rounded-lg">
<button <button
onClick={() => setShowManifest(v => !v)} onClick={() => setShowManifest(v => !v)}
className="w-full flex items-center justify-between gap-2 px-4 py-3 text-left hover:bg-muted/30 transition-colors" className="w-full flex items-center justify-between gap-2 px-4 py-3 text-start hover:bg-muted/30 transition-colors"
> >
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<FileCode className="w-4 h-4 text-muted-foreground" /> <FileCode className="w-4 h-4 text-muted-foreground" />
@@ -532,7 +532,7 @@ export default function MarketplacePreviewPage() {
<section className="border border-border rounded-lg"> <section className="border border-border rounded-lg">
<button <button
onClick={() => setShowSource(v => !v)} onClick={() => setShowSource(v => !v)}
className="w-full flex items-center justify-between gap-2 px-4 py-3 text-left hover:bg-muted/30 transition-colors" className="w-full flex items-center justify-between gap-2 px-4 py-3 text-start hover:bg-muted/30 transition-colors"
> >
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<FileCode className="w-4 h-4 text-muted-foreground" /> <FileCode className="w-4 h-4 text-muted-foreground" />
+1 -1
View File
@@ -44,7 +44,7 @@ export default function GlobalError({
onClick={reset} onClick={reset}
className="inline-flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors" className="inline-flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
> >
<RefreshCw className="w-4 h-4 mr-2" /> <RefreshCw className="w-4 h-4 me-2" />
Try again Try again
</button> </button>
</div> </div>
+2 -1
View File
@@ -1,4 +1,5 @@
import type { Metadata, Viewport } from "next"; import type { Metadata, Viewport } from "next";
import { getLocaleDirection } from "@/i18n/direction";
import { Geist, Geist_Mono } from "next/font/google"; import { Geist, Geist_Mono } from "next/font/google";
import { headers } from "next/headers"; import { headers } from "next/headers";
import { getLocale, getTranslations } from "next-intl/server"; import { getLocale, getTranslations } from "next-intl/server";
@@ -76,7 +77,7 @@ export default async function RootLayout({
const parentOrigin = process.env.NEXT_PUBLIC_PARENT_ORIGIN || ""; const parentOrigin = process.env.NEXT_PUBLIC_PARENT_ORIGIN || "";
return ( return (
<html lang={locale} suppressHydrationWarning> <html lang={locale} dir={getLocaleDirection(locale)} suppressHydrationWarning>
<head> <head>
<meta name="theme-color" content="#ffffff" /> <meta name="theme-color" content="#ffffff" />
<meta name="mobile-web-app-capable" content="yes" /> <meta name="mobile-web-app-capable" content="yes" />
+5 -5
View File
@@ -780,7 +780,7 @@ function ServerStep({ config, setConfig, onNext }: Pick<StepProps, 'config' | 's
</p> </p>
</div> </div>
</div> </div>
<label className="mt-3 ml-[3.25rem] flex items-center gap-2 cursor-pointer"> <label className="mt-3 ms-[3.25rem] flex items-center gap-2 cursor-pointer">
<input <input
type="checkbox" type="checkbox"
checked={confirmedNonJmap} checked={confirmedNonJmap}
@@ -870,7 +870,7 @@ function ServerStep({ config, setConfig, onNext }: Pick<StepProps, 'config' | 's
)} )}
{hasRowErrors && ( {hasRowErrors && (
<ul className="text-xs text-destructive list-disc pl-5 space-y-0.5"> <ul className="text-xs text-destructive list-disc ps-5 space-y-0.5">
{rowErrors.map((err, i) => ( {rowErrors.map((err, i) => (
<li key={i}>{err}</li> <li key={i}>{err}</li>
))} ))}
@@ -1384,7 +1384,7 @@ function BrandingAsset({
</div> </div>
{showUrlField && ( {showUrlField && (
<div className="mt-3 pl-[4.75rem]"> <div className="mt-3 ps-[4.75rem]">
<Input <Input
value={value} value={value}
onChange={onChange} onChange={onChange}
@@ -1394,7 +1394,7 @@ function BrandingAsset({
)} )}
{uploadError && ( {uploadError && (
<p className="mt-2 pl-[4.75rem] text-xs text-destructive">{uploadError}</p> <p className="mt-2 ps-[4.75rem] text-xs text-destructive">{uploadError}</p>
)} )}
</div> </div>
); );
@@ -1601,7 +1601,7 @@ function SummaryRow({ label, value, mono }: { label: string; value: string; mono
return ( return (
<div className="flex justify-between items-baseline gap-3 text-sm"> <div className="flex justify-between items-baseline gap-3 text-sm">
<span className="text-muted-foreground shrink-0">{label}</span> <span className="text-muted-foreground shrink-0">{label}</span>
<span className={'text-foreground text-right truncate min-w-0 ' + (mono ? 'font-mono text-xs' : '')}> <span className={'text-foreground text-end truncate min-w-0 ' + (mono ? 'font-mono text-xs' : '')}>
{value || <span className="text-muted-foreground italic">-</span>} {value || <span className="text-muted-foreground italic">-</span>}
</span> </span>
</div> </div>
+22 -5
View File
@@ -660,13 +660,13 @@ body {
.tiptap ul { .tiptap ul {
list-style-type: disc; list-style-type: disc;
padding-left: 1.5rem; padding-inline-start: 1.5rem;
margin: 0.25rem 0; margin: 0.25rem 0;
} }
.tiptap ol { .tiptap ol {
list-style-type: decimal; list-style-type: decimal;
padding-left: 1.5rem; padding-inline-start: 1.5rem;
margin: 0.25rem 0; margin: 0.25rem 0;
} }
@@ -675,8 +675,8 @@ body {
} }
.tiptap blockquote { .tiptap blockquote {
border-left: 3px solid var(--color-border); border-inline-start: 3px solid var(--color-border);
padding-left: 1rem; padding-inline-start: 1rem;
margin: 0.5rem 0; margin: 0.5rem 0;
color: var(--color-muted-foreground); color: var(--color-muted-foreground);
} }
@@ -719,7 +719,7 @@ body {
.tiptap p.is-editor-empty:first-child::before { .tiptap p.is-editor-empty:first-child::before {
content: attr(data-placeholder); content: attr(data-placeholder);
float: left; float: inline-start;
color: var(--color-muted-foreground); color: var(--color-muted-foreground);
pointer-events: none; pointer-events: none;
height: 0; height: 0;
@@ -808,3 +808,20 @@ body {
border-radius: 8px; border-radius: 8px;
animation: settings-search-pulse 1.6s ease-in-out forwards; animation: settings-search-pulse 1.6s ease-in-out forwards;
} }
/* RTL: mirror directional icons (chevrons/arrows) so prev/next, back/forward,
and panel-collapse affordances point the correct way in right-to-left layouts.
lucide-react emits a `lucide-<name>` class per icon, so we target the
directional ones only — vertical chevrons (up/down) are intentionally left. */
[dir="rtl"] .lucide-chevron-left,
[dir="rtl"] .lucide-chevron-right,
[dir="rtl"] .lucide-chevrons-left,
[dir="rtl"] .lucide-chevrons-right,
[dir="rtl"] .lucide-arrow-left,
[dir="rtl"] .lucide-arrow-right,
[dir="rtl"] .lucide-arrow-big-left,
[dir="rtl"] .lucide-arrow-big-right,
[dir="rtl"] .lucide-panel-left,
[dir="rtl"] .lucide-panel-right {
transform: scaleX(-1);
}
+2 -2
View File
@@ -131,7 +131,7 @@ export function CalendarAgendaView({
)}> )}>
{formatDateHeader(group.date)} {formatDateHeader(group.date)}
</span> </span>
<span className="text-xs text-muted-foreground ml-2"> <span className="text-xs text-muted-foreground ms-2">
{intlFormatter.dateTime(group.date, { month: "short", day: "numeric", year: "numeric" })} {intlFormatter.dateTime(group.date, { month: "short", day: "numeric", year: "numeric" })}
</span> </span>
</div> </div>
@@ -163,7 +163,7 @@ export function CalendarAgendaView({
onMouseLeave={() => onHoverLeave?.()} onMouseLeave={() => onHoverLeave?.()}
onContextMenu={onContextMenuEvent ? (e) => onContextMenuEvent(e, ev) : undefined} onContextMenu={onContextMenuEvent ? (e) => onContextMenuEvent(e, ev) : undefined}
className={cn( className={cn(
"w-full flex items-start px-4 hover:bg-muted/50 transition-colors text-left", "w-full flex items-start px-4 hover:bg-muted/50 transition-colors text-start",
isCancelled && "opacity-60" isCancelled && "opacity-60"
)} )}
style={{ gap: 'var(--density-item-gap)', paddingBlock: 'var(--density-item-py)' }} style={{ gap: 'var(--density-item-gap)', paddingBlock: 'var(--density-item-py)' }}
+4 -4
View File
@@ -218,7 +218,7 @@ export function CalendarDayView({
{HOURS.map((h) => ( {HOURS.map((h) => (
<div <div
key={h} key={h}
className="relative text-muted-foreground text-right pr-2" className="relative text-muted-foreground text-end pe-2"
style={{ height: HOUR_HEIGHT }} style={{ height: HOUR_HEIGHT }}
> >
{h > 0 && ( {h > 0 && (
@@ -231,7 +231,7 @@ export function CalendarDayView({
</div> </div>
<div <div
className="flex-1 relative border-l border-border" className="flex-1 relative border-s border-border"
role="row" role="row"
aria-label={t("views.day")} aria-label={t("views.day")}
onPointerDown={(e) => handleGridPointerDown(e, dayKey, selectedDate)} onPointerDown={(e) => handleGridPointerDown(e, dayKey, selectedDate)}
@@ -312,7 +312,7 @@ export function CalendarDayView({
style={{ top: (nowMinutes / 60) * HOUR_HEIGHT }} style={{ top: (nowMinutes / 60) * HOUR_HEIGHT }}
> >
<div className="flex items-center"> <div className="flex items-center">
<div className="w-2.5 h-2.5 rounded-full bg-destructive -ml-1" /> <div className="w-2.5 h-2.5 rounded-full bg-destructive -ms-1" />
<div className="flex-1 h-px bg-destructive" /> <div className="flex-1 h-px bg-destructive" />
</div> </div>
</div> </div>
@@ -346,7 +346,7 @@ export function CalendarDayView({
style={{ top: (dropTarget.minutes / 60) * HOUR_HEIGHT }} style={{ top: (dropTarget.minutes / 60) * HOUR_HEIGHT }}
> >
<div className="flex items-center"> <div className="flex items-center">
<div className="w-2.5 h-2.5 rounded-full bg-primary -ml-1" /> <div className="w-2.5 h-2.5 rounded-full bg-primary -ms-1" />
<div className="flex-1 h-0.5 bg-primary rounded-full" /> <div className="flex-1 h-0.5 bg-primary rounded-full" />
</div> </div>
<div className="absolute -top-4 left-2 text-[10px] font-medium text-primary bg-background/90 px-1 rounded shadow-sm"> <div className="absolute -top-4 left-2 text-[10px] font-medium text-primary bg-background/90 px-1 rounded shadow-sm">
+2 -2
View File
@@ -145,7 +145,7 @@ export function CalendarMonthView({
<div className="grid grid-cols-7 border-b border-border" role="row"> <div className="grid grid-cols-7 border-b border-border" role="row">
{dayHeaders.map((d) => ( {dayHeaders.map((d) => (
<div key={d} role="columnheader" className={cn( <div key={d} role="columnheader" className={cn(
"text-center text-xs font-medium text-muted-foreground py-2 border-r border-border last:border-r-0", "text-center text-xs font-medium text-muted-foreground py-2 border-e border-border last:border-e-0",
isMobile && "py-1.5 text-[11px]" isMobile && "py-1.5 text-[11px]"
)}> )}>
{isMobile ? t(`days.${d}`).slice(0, 2) : t(`days.${d}`)} {isMobile ? t(`days.${d}`).slice(0, 2) : t(`days.${d}`)}
@@ -181,7 +181,7 @@ export function CalendarMonthView({
onDragLeave={handleCellDragLeave} onDragLeave={handleCellDragLeave}
onDrop={(e) => handleCellDrop(e, day)} onDrop={(e) => handleCellDrop(e, day)}
className={cn( className={cn(
"border-r border-border last:border-r-0 p-1 cursor-pointer transition-colors touch-manipulation", "border-e border-border last:border-e-0 p-1 cursor-pointer transition-colors touch-manipulation",
!inMonth && "bg-muted/30", !inMonth && "bg-muted/30",
"hover:bg-muted/50", "hover:bg-muted/50",
selected && isMobile && "bg-primary/10", selected && isMobile && "bg-primary/10",
@@ -397,7 +397,7 @@ export function CalendarSidebarPanel({
<ListTodo className="w-4 h-4 text-muted-foreground" /> <ListTodo className="w-4 h-4 text-muted-foreground" />
<span>{t('tasks.label')}</span> <span>{t('tasks.label')}</span>
{pendingTaskCount > 0 && ( {pendingTaskCount > 0 && (
<span className="ml-auto text-xs text-muted-foreground">{pendingTaskCount}</span> <span className="ms-auto text-xs text-muted-foreground">{pendingTaskCount}</span>
)} )}
{overdueTaskCount > 0 && ( {overdueTaskCount > 0 && (
<span className="text-xs text-destructive font-medium">{overdueTaskCount} {t('tasks.filter_overdue').toLowerCase()}</span> <span className="text-xs text-destructive font-medium">{overdueTaskCount} {t('tasks.filter_overdue').toLowerCase()}</span>
@@ -437,7 +437,7 @@ export function CalendarSidebarPanel({
onCreateCalendar(); onCreateCalendar();
} }
}} }}
className="ml-auto p-0.5 rounded text-muted-foreground/70 opacity-0 group-hover:opacity-100 hover:text-foreground hover:bg-muted transition-colors cursor-pointer" className="ms-auto p-0.5 rounded text-muted-foreground/70 opacity-0 group-hover:opacity-100 hover:text-foreground hover:bg-muted transition-colors cursor-pointer"
title={tMgmt('add_calendar')} title={tMgmt('add_calendar')}
> >
<Plus className="w-3 h-3" /> <Plus className="w-3 h-3" />
@@ -445,7 +445,7 @@ export function CalendarSidebarPanel({
)} )}
</button> </button>
{expanded && ( {expanded && (
<div className="mt-1 pl-3"> <div className="mt-1 ps-3">
{owned.length > 0 && ( {owned.length > 0 && (
<div> <div>
<div className="px-1 mb-1 text-[10px] font-medium text-muted-foreground/80 uppercase tracking-wider"> <div className="px-1 mb-1 text-[10px] font-medium text-muted-foreground/80 uppercase tracking-wider">
+9 -9
View File
@@ -124,7 +124,7 @@ export function CalendarToolbar({
variant="ghost" variant="ghost"
size="icon" size="icon"
onClick={onMenuClick} onClick={onMenuClick}
className="h-8 w-8 -ml-1 mr-1" className="h-8 w-8 -ms-1 me-1"
aria-label={t("nav_open_menu")} aria-label={t("nav_open_menu")}
> >
<Menu className="w-4 h-4" /> <Menu className="w-4 h-4" />
@@ -138,7 +138,7 @@ export function CalendarToolbar({
{onMenuClick && ( {onMenuClick && (
<button <button
onClick={onMenuClick} onClick={onMenuClick}
className="p-1.5 -ml-1 rounded-md hover:bg-muted transition-colors touch-manipulation" className="p-1.5 -ms-1 rounded-md hover:bg-muted transition-colors touch-manipulation"
aria-label={t("nav_open_menu")} aria-label={t("nav_open_menu")}
> >
<Menu className="w-4 h-4" /> <Menu className="w-4 h-4" />
@@ -147,7 +147,7 @@ export function CalendarToolbar({
{onNavigateBack && ( {onNavigateBack && (
<button <button
onClick={onNavigateBack} onClick={onNavigateBack}
className="p-1.5 -ml-1 rounded-md hover:bg-muted transition-colors touch-manipulation" className="p-1.5 -ms-1 rounded-md hover:bg-muted transition-colors touch-manipulation"
aria-label={t("back_to_month")} aria-label={t("back_to_month")}
> >
<ArrowLeft className="w-4 h-4" /> <ArrowLeft className="w-4 h-4" />
@@ -162,7 +162,7 @@ export function CalendarToolbar({
<button onClick={onNext} className="p-1.5 rounded-md hover:bg-muted transition-colors touch-manipulation" aria-label={t("nav_next")}> <button onClick={onNext} className="p-1.5 rounded-md hover:bg-muted transition-colors touch-manipulation" aria-label={t("nav_next")}>
<ChevronRight className="w-4 h-4" /> <ChevronRight className="w-4 h-4" />
</button> </button>
<Button variant="ghost" size="sm" onClick={onToday} className="touch-manipulation text-xs h-7 px-2 ml-0.5"> <Button variant="ghost" size="sm" onClick={onToday} className="touch-manipulation text-xs h-7 px-2 ms-0.5">
{t("views.today")} {t("views.today")}
</Button> </Button>
</div> </div>
@@ -284,7 +284,7 @@ export function CalendarToolbar({
{/* ── DESKTOP TOOLBAR ── */} {/* ── DESKTOP TOOLBAR ── */}
{!isMobile && ( {!isMobile && (
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<Button variant="outline" size="sm" onClick={onToday} className="h-8 mr-1"> <Button variant="outline" size="sm" onClick={onToday} className="h-8 me-1">
{t("views.today")} {t("views.today")}
</Button> </Button>
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={onPrev} aria-label={t("nav_prev")}> <Button variant="ghost" size="icon" className="h-8 w-8" onClick={onPrev} aria-label={t("nav_prev")}>
@@ -293,7 +293,7 @@ export function CalendarToolbar({
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={onNext} aria-label={t("nav_next")}> <Button variant="ghost" size="icon" className="h-8 w-8" onClick={onNext} aria-label={t("nav_next")}>
<ChevronRight className="w-4 h-4" /> <ChevronRight className="w-4 h-4" />
</Button> </Button>
<span className="text-base font-semibold ml-2 select-none"> <span className="text-base font-semibold ms-2 select-none">
{getDateLabel()} {getDateLabel()}
</span> </span>
</div> </div>
@@ -328,9 +328,9 @@ export function CalendarToolbar({
{(onImport || onSubscribe) && !isMobile && ( {(onImport || onSubscribe) && !isMobile && (
<div className="relative" ref={importDropdownRef}> <div className="relative" ref={importDropdownRef}>
<Button variant="outline" size="sm" className="h-8" onClick={() => setShowImportDropdown((v) => !v)}> <Button variant="outline" size="sm" className="h-8" onClick={() => setShowImportDropdown((v) => !v)}>
<Upload className="w-4 h-4 mr-1" /> <Upload className="w-4 h-4 me-1" />
{t("import.title")} {t("import.title")}
<ChevronDown className="w-3 h-3 ml-1" /> <ChevronDown className="w-3 h-3 ms-1" />
</Button> </Button>
{showImportDropdown && ( {showImportDropdown && (
<div className="absolute top-full right-0 mt-1 z-50 bg-background border border-border rounded-lg shadow-lg p-1 min-w-[180px]"> <div className="absolute top-full right-0 mt-1 z-50 bg-background border border-border rounded-lg shadow-lg p-1 min-w-[180px]">
@@ -359,7 +359,7 @@ export function CalendarToolbar({
{!isMobile && ( {!isMobile && (
<Button size="sm" className="h-8" onClick={onCreateEvent} data-tour="create-event-button"> <Button size="sm" className="h-8" onClick={onCreateEvent} data-tour="create-event-button">
<Plus className="w-4 h-4 mr-1" /> <Plus className="w-4 h-4 me-1" />
{t("events.create")} {t("events.create")}
</Button> </Button>
)} )}
+8 -8
View File
@@ -211,7 +211,7 @@ export function CalendarWeekView({
<div className={cn("flex min-h-0 flex-col flex-1", isMobile && "min-w-[880px]")}> {hasAllDay && ( <div className={cn("flex min-h-0 flex-col flex-1", isMobile && "min-w-[880px]")}> {hasAllDay && (
<div className="flex border-b border-border"> <div className="flex border-b border-border">
<div <div
className={cn("flex-shrink-0 text-[10px] text-muted-foreground p-1 text-right", isMobile ? "w-10 sticky left-0 z-10 bg-background" : "w-14")} className={cn("flex-shrink-0 text-[10px] text-muted-foreground p-1 text-end", isMobile ? "w-10 sticky left-0 z-10 bg-background" : "w-14")}
style={{ minHeight: Math.max(28, (allDayRowCount + taskRowCount) * 24 + 4) }} style={{ minHeight: Math.max(28, (allDayRowCount + taskRowCount) * 24 + 4) }}
> >
{t("events.all_day")} {t("events.all_day")}
@@ -306,7 +306,7 @@ export function CalendarWeekView({
<div className="flex border-b border-border" role="row"> <div className="flex border-b border-border" role="row">
<div className={cn("flex-shrink-0", isMobile ? "w-10 sticky left-0 z-10 bg-background" : "w-14")} /> <div className={cn("flex-shrink-0", isMobile ? "w-10 sticky left-0 z-10 bg-background" : "w-14")} />
<div className="flex-1 border-l border-border grid grid-cols-7"> <div className="flex-1 border-s border-border grid grid-cols-7">
{weekDays.map((day) => { {weekDays.map((day) => {
const todayCol = isToday(day); const todayCol = isToday(day);
const selected = isSameDay(day, selectedDate); const selected = isSameDay(day, selectedDate);
@@ -318,7 +318,7 @@ export function CalendarWeekView({
role="columnheader" role="columnheader"
aria-label={fullLabel} aria-label={fullLabel}
className={cn( className={cn(
"text-center py-2 text-sm border-r border-border last:border-r-0 transition-colors touch-manipulation", "text-center py-2 text-sm border-e border-border last:border-e-0 transition-colors touch-manipulation",
"hover:bg-muted/50", "hover:bg-muted/50",
todayCol && "font-bold", todayCol && "font-bold",
)} )}
@@ -345,7 +345,7 @@ export function CalendarWeekView({
{HOURS.map((h) => ( {HOURS.map((h) => (
<div <div
key={h} key={h}
className="relative text-muted-foreground text-right pr-2" className="relative text-muted-foreground text-end pe-2"
style={{ height: HOUR_HEIGHT }} style={{ height: HOUR_HEIGHT }}
> >
{h > 0 && ( {h > 0 && (
@@ -357,7 +357,7 @@ export function CalendarWeekView({
))} ))}
</div> </div>
<div className="flex-1 border-l border-border relative grid grid-cols-7"> <div className="flex-1 border-s border-border relative grid grid-cols-7">
{weekDays.map((day) => { {weekDays.map((day) => {
const key = format(day, "yyyy-MM-dd"); const key = format(day, "yyyy-MM-dd");
const dayEvents = timedEvents.get(key) || []; const dayEvents = timedEvents.get(key) || [];
@@ -367,7 +367,7 @@ export function CalendarWeekView({
return ( return (
<div <div
key={key} key={key}
className="relative border-r border-border last:border-r-0" className="relative border-e border-border last:border-e-0"
role="row" role="row"
aria-label={intlFormatter.dateTime(day, { weekday: "long", month: "long", day: "numeric" })} aria-label={intlFormatter.dateTime(day, { weekday: "long", month: "long", day: "numeric" })}
onPointerDown={(e) => handleGridPointerDown(e, key, day)} onPointerDown={(e) => handleGridPointerDown(e, key, day)}
@@ -448,7 +448,7 @@ export function CalendarWeekView({
style={{ top: (nowMinutes / 60) * HOUR_HEIGHT }} style={{ top: (nowMinutes / 60) * HOUR_HEIGHT }}
> >
<div className="flex items-center"> <div className="flex items-center">
<div className="w-2 h-2 rounded-full bg-destructive -ml-1" /> <div className="w-2 h-2 rounded-full bg-destructive -ms-1" />
<div className="flex-1 h-px bg-destructive" /> <div className="flex-1 h-px bg-destructive" />
</div> </div>
</div> </div>
@@ -482,7 +482,7 @@ export function CalendarWeekView({
style={{ top: (dropTarget.minutes / 60) * HOUR_HEIGHT }} style={{ top: (dropTarget.minutes / 60) * HOUR_HEIGHT }}
> >
<div className="flex items-center"> <div className="flex items-center">
<div className="w-2 h-2 rounded-full bg-primary -ml-1" /> <div className="w-2 h-2 rounded-full bg-primary -ms-1" />
<div className="flex-1 h-0.5 bg-primary rounded-full" /> <div className="flex-1 h-0.5 bg-primary rounded-full" />
</div> </div>
<div className="absolute -top-4 left-2 text-[10px] font-medium text-primary bg-background/90 px-1 rounded shadow-sm"> <div className="absolute -top-4 left-2 text-[10px] font-medium text-primary bg-background/90 px-1 rounded shadow-sm">
@@ -137,7 +137,7 @@ export function CreateCalendarModal({ client, onClose }: CreateCalendarModalProp
<Button onClick={handleSubmit} disabled={!isValid || isSubmitting}> <Button onClick={handleSubmit} disabled={!isValid || isSubmitting}>
{isSubmitting ? ( {isSubmitting ? (
<> <>
<Loader2 className="w-4 h-4 animate-spin mr-2" /> <Loader2 className="w-4 h-4 animate-spin me-2" />
{tCommon("loading")} {tCommon("loading")}
</> </>
) : ( ) : (
+4 -4
View File
@@ -134,7 +134,7 @@ export function EventCard({ event, calendar, variant, onClick, onMouseEnter, onM
aria-label={ariaLabel} aria-label={ariaLabel}
{...dragProps} {...dragProps}
className={cn( className={cn(
"flex items-center gap-1 w-full text-left text-xs px-1 py-0.5 rounded truncate", "flex items-center gap-1 w-full text-start text-xs px-1 py-0.5 rounded truncate",
"min-h-[44px] sm:min-h-0", "min-h-[44px] sm:min-h-0",
"hover:opacity-80 transition-opacity", "hover:opacity-80 transition-opacity",
isSelected && "ring-2 ring-primary", isSelected && "ring-2 ring-primary",
@@ -163,10 +163,10 @@ export function EventCard({ event, calendar, variant, onClick, onMouseEnter, onM
aria-label={ariaLabel} aria-label={ariaLabel}
{...dragProps} {...dragProps}
className={cn( className={cn(
"w-full h-full text-left rounded-r px-1.5 py-0.5 text-xs overflow-hidden", "w-full h-full text-start rounded-r px-1.5 py-0.5 text-xs overflow-hidden",
"hover:opacity-90 transition-opacity cursor-pointer", "hover:opacity-90 transition-opacity cursor-pointer",
continuesAfter && "rounded-r-sm", continuesAfter && "rounded-r-sm",
continuesAfter && "pr-2", continuesAfter && "pe-2",
isSelected && "ring-2 ring-primary", isSelected && "ring-2 ring-primary",
isBeingDragged && "opacity-50", isBeingDragged && "opacity-50",
isCancelled && !isBeingDragged && "opacity-60", isCancelled && !isBeingDragged && "opacity-60",
@@ -194,7 +194,7 @@ export function EventCard({ event, calendar, variant, onClick, onMouseEnter, onM
{...dragProps} {...dragProps}
data-calendar-event data-calendar-event
className={cn( className={cn(
"w-full h-full text-left rounded-r px-1.5 py-0.5 text-xs overflow-hidden", "w-full h-full text-start rounded-r px-1.5 py-0.5 text-xs overflow-hidden",
"hover:opacity-90 transition-opacity cursor-pointer", "hover:opacity-90 transition-opacity cursor-pointer",
isSelected && "ring-2 ring-primary", isSelected && "ring-2 ring-primary",
isBeingDragged && "opacity-50", isBeingDragged && "opacity-50",
+14 -14
View File
@@ -298,15 +298,15 @@ export function EventDetailPopover({
</h3> </h3>
</div> </div>
{calendar && ( {calendar && (
<p className="text-xs text-muted-foreground mt-0.5 pl-[18px]"> <p className="text-xs text-muted-foreground mt-0.5 ps-[18px]">
{calendar.name} {calendar.name}
{event.status === "tentative" && ( {event.status === "tentative" && (
<span className="ml-2 inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium bg-warning/15 text-warning"> <span className="ms-2 inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium bg-warning/15 text-warning">
{t("detail.tentative")} {t("detail.tentative")}
</span> </span>
)} )}
{event.status === "cancelled" && ( {event.status === "cancelled" && (
<span className="ml-2 inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400"> <span className="ms-2 inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400 line-through">
{t("detail.cancelled")} {t("detail.cancelled")}
</span> </span>
)} )}
@@ -346,13 +346,13 @@ export function EventDetailPopover({
<> <>
<div className="font-medium text-foreground"> <div className="font-medium text-foreground">
{formatEventDate(startDate)} {formatEventDate(startDate)}
<span className="ml-1.5 font-normal text-muted-foreground"> <span className="ms-1.5 font-normal text-muted-foreground">
{formatTime(startDate)} {formatTime(startDate)}
</span> </span>
</div> </div>
<div className="font-medium text-foreground"> <div className="font-medium text-foreground">
{formatEventDate(endDate)} {formatEventDate(endDate)}
<span className="ml-1.5 font-normal text-muted-foreground"> <span className="ms-1.5 font-normal text-muted-foreground">
{formatTime(endDate)} {formatTime(endDate)}
</span> </span>
</div> </div>
@@ -367,11 +367,11 @@ export function EventDetailPopover({
{formatEventDate(startDate)} {formatEventDate(startDate)}
</span> </span>
{event.showWithoutTime ? ( {event.showWithoutTime ? (
<span className="text-muted-foreground ml-1.5">{t("events.all_day")}</span> <span className="text-muted-foreground ms-1.5">{t("events.all_day")}</span>
) : ( ) : (
<div className="text-muted-foreground"> <div className="text-muted-foreground">
{formatTime(startDate)} {formatTime(endDate)} {formatTime(startDate)} {formatTime(endDate)}
<span className="ml-1.5 text-xs">({formatDurationDisplay(durationMinutes)})</span> <span className="ms-1.5 text-xs">({formatDurationDisplay(durationMinutes)})</span>
</div> </div>
)} )}
</> </>
@@ -437,7 +437,7 @@ export function EventDetailPopover({
<span className="truncate text-foreground"> <span className="truncate text-foreground">
{p.name || p.email} {p.name || p.email}
{p.isOrganizer && ( {p.isOrganizer && (
<span className="text-muted-foreground ml-1"> <span className="text-muted-foreground ms-1">
({t("participants.organizer").toLowerCase()}) ({t("participants.organizer").toLowerCase()})
</span> </span>
)} )}
@@ -515,7 +515,7 @@ export function EventDetailPopover({
disabled={!noteText.trim() || isSavingNote} disabled={!noteText.trim() || isSavingNote}
className="h-7 text-xs" className="h-7 text-xs"
> >
<Send className="w-3 h-3 mr-1" /> <Send className="w-3 h-3 me-1" />
{t("detail.save_note")} {t("detail.save_note")}
</Button> </Button>
</div> </div>
@@ -549,7 +549,7 @@ export function EventDetailPopover({
: "text-success border-success/30 hover:bg-success/10" : "text-success border-success/30 hover:bg-success/10"
} }
> >
{userCurrentStatus === "accepted" && <Check className="w-3.5 h-3.5 mr-1" />} {userCurrentStatus === "accepted" && <Check className="w-3.5 h-3.5 me-1" />}
{t("participants.accepted")} {t("participants.accepted")}
</Button> </Button>
<Button <Button
@@ -562,7 +562,7 @@ export function EventDetailPopover({
: "border border-warning/30 text-warning hover:bg-warning/10" : "border border-warning/30 text-warning hover:bg-warning/10"
} }
> >
{userCurrentStatus === "tentative" && <Check className="w-3.5 h-3.5 mr-1" />} {userCurrentStatus === "tentative" && <Check className="w-3.5 h-3.5 me-1" />}
{t("participants.tentative")} {t("participants.tentative")}
</Button> </Button>
<Button <Button
@@ -575,7 +575,7 @@ export function EventDetailPopover({
: "text-destructive hover:bg-destructive/10" : "text-destructive hover:bg-destructive/10"
} }
> >
{userCurrentStatus === "declined" && <Check className="w-3.5 h-3.5 mr-1" />} {userCurrentStatus === "declined" && <Check className="w-3.5 h-3.5 me-1" />}
{t("participants.declined")} {t("participants.declined")}
</Button> </Button>
</div> </div>
@@ -609,7 +609,7 @@ export function EventDetailPopover({
) : ( ) : (
<> <>
<Button variant="default" size="sm" onClick={onEdit} className="h-7 text-xs"> <Button variant="default" size="sm" onClick={onEdit} className="h-7 text-xs">
<Pencil className="w-3.5 h-3.5 mr-1" /> <Pencil className="w-3.5 h-3.5 me-1" />
{t("events.edit")} {t("events.edit")}
</Button> </Button>
<Button <Button
@@ -619,7 +619,7 @@ export function EventDetailPopover({
className="h-7 text-xs" className="h-7 text-xs"
title={t("events.duplicate")} title={t("events.duplicate")}
> >
<Copy className="w-3.5 h-3.5 mr-1" /> <Copy className="w-3.5 h-3.5 me-1" />
{t("events.duplicate")} {t("events.duplicate")}
</Button> </Button>
<div className="flex-1" /> <div className="flex-1" />
+18 -18
View File
@@ -666,11 +666,11 @@ export function EventModal({
<div className="text-sm"> <div className="text-sm">
<div> <div>
<span className="font-medium">{formatEventDate(startD)}</span> <span className="font-medium">{formatEventDate(startD)}</span>
<span className="text-muted-foreground ml-2">{format(startD, timeDisplayFmt)}</span> <span className="text-muted-foreground ms-2">{format(startD, timeDisplayFmt)}</span>
</div> </div>
<div> <div>
<span className="font-medium">{formatEventDate(endD)}</span> <span className="font-medium">{formatEventDate(endD)}</span>
<span className="text-muted-foreground ml-2">{format(endD, timeDisplayFmt)}</span> <span className="text-muted-foreground ms-2">{format(endD, timeDisplayFmt)}</span>
</div> </div>
</div> </div>
); );
@@ -679,7 +679,7 @@ export function EventModal({
<div className="text-sm"> <div className="text-sm">
<span className="font-medium">{formatEventDate(startD)}</span> <span className="font-medium">{formatEventDate(startD)}</span>
{!event.showWithoutTime && ( {!event.showWithoutTime && (
<span className="text-muted-foreground ml-2"> <span className="text-muted-foreground ms-2">
{format(startD, timeDisplayFmt)} {format(endD, timeDisplayFmt)} {format(startD, timeDisplayFmt)} {format(endD, timeDisplayFmt)}
</span> </span>
)} )}
@@ -701,7 +701,7 @@ export function EventModal({
<Users className="w-4 h-4" /> <Users className="w-4 h-4" />
{t("participants.title")} {t("participants.title")}
</div> </div>
<div className="space-y-1 pl-5"> <div className="space-y-1 ps-5">
{participants.map(p => ( {participants.map(p => (
<div key={p.id} className="flex items-center justify-between text-sm"> <div key={p.id} className="flex items-center justify-between text-sm">
<span className="truncate">{p.name || p.email}</span> <span className="truncate">{p.name || p.email}</span>
@@ -726,7 +726,7 @@ export function EventModal({
? "bg-success hover:bg-success/80 text-success-foreground" ? "bg-success hover:bg-success/80 text-success-foreground"
: "text-success border-success/30 hover:bg-success/10"} : "text-success border-success/30 hover:bg-success/10"}
> >
{userCurrentStatus === "accepted" && <Check className="w-4 h-4 mr-1" />} {userCurrentStatus === "accepted" && <Check className="w-4 h-4 me-1" />}
{t("participants.accepted")} {t("participants.accepted")}
</Button> </Button>
<Button <Button
@@ -737,7 +737,7 @@ export function EventModal({
? "bg-warning hover:bg-warning/80 text-warning-foreground" ? "bg-warning hover:bg-warning/80 text-warning-foreground"
: "border border-warning/30 text-warning hover:bg-warning/10"} : "border border-warning/30 text-warning hover:bg-warning/10"}
> >
{userCurrentStatus === "tentative" && <Check className="w-4 h-4 mr-1" />} {userCurrentStatus === "tentative" && <Check className="w-4 h-4 me-1" />}
{t("participants.tentative")} {t("participants.tentative")}
</Button> </Button>
<Button <Button
@@ -748,7 +748,7 @@ export function EventModal({
? "bg-destructive hover:bg-destructive/80 text-destructive-foreground" ? "bg-destructive hover:bg-destructive/80 text-destructive-foreground"
: "text-destructive hover:bg-destructive/10"} : "text-destructive hover:bg-destructive/10"}
> >
{userCurrentStatus === "declined" && <Check className="w-4 h-4 mr-1" />} {userCurrentStatus === "declined" && <Check className="w-4 h-4 me-1" />}
{t("participants.declined")} {t("participants.declined")}
</Button> </Button>
</div> </div>
@@ -784,7 +784,7 @@ export function EventModal({
<h2 className="text-lg font-semibold truncate">{event.title || t("events.no_title")}</h2> <h2 className="text-lg font-semibold truncate">{event.title || t("events.no_title")}</h2>
</div> </div>
{eventCalendar && ( {eventCalendar && (
<p className="text-xs text-muted-foreground mt-0.5 pl-[18px]">{eventCalendar.name}</p> <p className="text-xs text-muted-foreground mt-0.5 ps-[18px]">{eventCalendar.name}</p>
)} )}
</div> </div>
<button onClick={onClose} className="p-1.5 rounded-md hover:bg-muted transition-colors duration-150 flex-shrink-0 mt-0.5 text-muted-foreground hover:text-foreground" aria-label={t("form.cancel")}> <button onClick={onClose} className="p-1.5 rounded-md hover:bg-muted transition-colors duration-150 flex-shrink-0 mt-0.5 text-muted-foreground hover:text-foreground" aria-label={t("form.cancel")}>
@@ -816,13 +816,13 @@ export function EventModal({
<> <>
<div className="font-medium text-foreground"> <div className="font-medium text-foreground">
{formatEventDate(startD)} {formatEventDate(startD)}
<span className="ml-1.5 font-normal text-muted-foreground"> <span className="ms-1.5 font-normal text-muted-foreground">
{format(startD, timeDisplayFmt)} {format(startD, timeDisplayFmt)}
</span> </span>
</div> </div>
<div className="font-medium text-foreground"> <div className="font-medium text-foreground">
{formatEventDate(endD)} {formatEventDate(endD)}
<span className="ml-1.5 font-normal text-muted-foreground"> <span className="ms-1.5 font-normal text-muted-foreground">
{format(endD, timeDisplayFmt)} {format(endD, timeDisplayFmt)}
</span> </span>
</div> </div>
@@ -838,11 +838,11 @@ export function EventModal({
{formatEventDate(startD)} {formatEventDate(startD)}
</span> </span>
{event.showWithoutTime ? ( {event.showWithoutTime ? (
<span className="text-muted-foreground ml-1.5">{t("events.all_day")}</span> <span className="text-muted-foreground ms-1.5">{t("events.all_day")}</span>
) : ( ) : (
<div className="text-muted-foreground"> <div className="text-muted-foreground">
{format(startD, timeDisplayFmt)} {format(endD, timeDisplayFmt)} {format(startD, timeDisplayFmt)} {format(endD, timeDisplayFmt)}
<span className="ml-1.5 text-xs">({formatDurationDisplay(durMin)})</span> <span className="ms-1.5 text-xs">({formatDurationDisplay(durMin)})</span>
</div> </div>
)} )}
</> </>
@@ -889,7 +889,7 @@ export function EventModal({
<span className="truncate text-foreground"> <span className="truncate text-foreground">
{p.name || p.email} {p.name || p.email}
{p.isOrganizer && ( {p.isOrganizer && (
<span className="text-muted-foreground ml-1">({t("participants.organizer").toLowerCase()})</span> <span className="text-muted-foreground ms-1">({t("participants.organizer").toLowerCase()})</span>
)} )}
</span> </span>
<StatusBadge status={p.status} isOrganizer={p.isOrganizer} t={t} /> <StatusBadge status={p.status} isOrganizer={p.isOrganizer} t={t} />
@@ -942,21 +942,21 @@ export function EventModal({
</div> </div>
) : ( ) : (
<Button variant="ghost" size="sm" onClick={() => setShowDeleteConfirm(true)} className="text-destructive"> <Button variant="ghost" size="sm" onClick={() => setShowDeleteConfirm(true)} className="text-destructive">
<Trash2 className="w-4 h-4 mr-1" /> <Trash2 className="w-4 h-4 me-1" />
{t("events.delete")} {t("events.delete")}
</Button> </Button>
) )
)} )}
{onDuplicate && !showDeleteConfirm && ( {onDuplicate && !showDeleteConfirm && (
<Button variant="ghost" size="sm" onClick={handleDuplicate} aria-label={t("events.duplicate")}> <Button variant="ghost" size="sm" onClick={handleDuplicate} aria-label={t("events.duplicate")}>
<Copy className="w-4 h-4 mr-1" /> <Copy className="w-4 h-4 me-1" />
{t("events.duplicate")} {t("events.duplicate")}
</Button> </Button>
)} )}
</div> </div>
{!showDeleteConfirm && ( {!showDeleteConfirm && (
<Button onClick={() => setMode("edit")}> <Button onClick={() => setMode("edit")}>
<Pencil className="w-4 h-4 mr-1" /> <Pencil className="w-4 h-4 me-1" />
{t("events.edit")} {t("events.edit")}
</Button> </Button>
)} )}
@@ -1320,7 +1320,7 @@ export function EventModal({
onClick={() => setShowDeleteConfirm(true)} onClick={() => setShowDeleteConfirm(true)}
className="text-red-600 dark:text-red-400" className="text-red-600 dark:text-red-400"
> >
<Trash2 className="w-4 h-4 mr-1" /> <Trash2 className="w-4 h-4 me-1" />
{t("events.delete")} {t("events.delete")}
</Button> </Button>
) )
@@ -1332,7 +1332,7 @@ export function EventModal({
onClick={handleDuplicate} onClick={handleDuplicate}
aria-label={t("events.duplicate")} aria-label={t("events.duplicate")}
> >
<Copy className="w-4 h-4 mr-1" /> <Copy className="w-4 h-4 me-1" />
{t("events.duplicate")} {t("events.duplicate")}
</Button> </Button>
)} )}
+1 -1
View File
@@ -444,7 +444,7 @@ export function ICalImportModal({ calendars, client, onClose, initialUrl }: ICal
onClick={handleImport} onClick={handleImport}
disabled={selectedIndices.size === 0} disabled={selectedIndices.size === 0}
> >
<Check className="w-4 h-4 mr-1" /> <Check className="w-4 h-4 me-1" />
{t("import_button")} ({selectedIndices.size}) {t("import_button")} ({selectedIndices.size})
</Button> </Button>
)} )}
@@ -208,7 +208,7 @@ export function ICalSubscriptionModal({ client, onClose, editSubscription, initi
<Button onClick={handleSubmit} disabled={!isValid || isSubmitting}> <Button onClick={handleSubmit} disabled={!isValid || isSubmitting}>
{isSubmitting ? ( {isSubmitting ? (
<> <>
<Loader2 className="w-4 h-4 animate-spin mr-2" /> <Loader2 className="w-4 h-4 animate-spin me-2" />
{isEdit ? t("saving") : t("subscribing")} {isEdit ? t("saving") : t("subscribing")}
</> </>
) : ( ) : (
+1 -1
View File
@@ -302,7 +302,7 @@ export function TaskModal({
className="text-destructive hover:text-destructive" className="text-destructive hover:text-destructive"
onClick={() => onDelete(task.id)} onClick={() => onDelete(task.id)}
> >
<Trash2 className="h-4 w-4 mr-1" /> <Trash2 className="h-4 w-4 me-1" />
{t("tasks.delete")} {t("tasks.delete")}
</Button> </Button>
)} )}
+2 -2
View File
@@ -44,7 +44,7 @@ export function TaskToolbar({
))} ))}
</div> </div>
<label className="flex items-center gap-1.5 text-xs text-muted-foreground cursor-pointer select-none ml-2"> <label className="flex items-center gap-1.5 text-xs text-muted-foreground cursor-pointer select-none ms-2">
<input <input
type="checkbox" type="checkbox"
checked={showCompleted} checked={showCompleted}
@@ -57,7 +57,7 @@ export function TaskToolbar({
<div className="flex-1" /> <div className="flex-1" />
<Button size="sm" onClick={onCreateTask}> <Button size="sm" onClick={onCreateTask}>
<Plus className="w-4 h-4 mr-1" /> <Plus className="w-4 h-4 me-1" />
{t("tasks.create")} {t("tasks.create")}
</Button> </Button>
</div> </div>
+2 -2
View File
@@ -229,7 +229,7 @@ export function ContactActivity({ contact }: ContactActivityProps) {
key={email.id} key={email.id}
type="button" type="button"
onClick={() => handleOpenEmail(email)} onClick={() => handleOpenEmail(email)}
className="w-full text-left flex items-start gap-3 px-2 py-2 rounded-md hover:bg-muted/60 transition-colors touch-manipulation" className="w-full text-start flex items-start gap-3 px-2 py-2 rounded-md hover:bg-muted/60 transition-colors touch-manipulation"
> >
<Avatar name={sender.name} email={sender.address} size="sm" /> <Avatar name={sender.name} email={sender.address} size="sm" />
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
@@ -277,7 +277,7 @@ export function ContactActivity({ contact }: ContactActivityProps) {
key={event.id} key={event.id}
type="button" type="button"
onClick={() => handleOpenEvent(event)} onClick={() => handleOpenEvent(event)}
className="w-full text-left flex items-baseline gap-3 px-2 py-2 rounded-md hover:bg-muted/60 transition-colors touch-manipulation" className="w-full text-start flex items-baseline gap-3 px-2 py-2 rounded-md hover:bg-muted/60 transition-colors touch-manipulation"
> >
<span className="text-xs text-muted-foreground tabular-nums w-20 flex-shrink-0"> <span className="text-xs text-muted-foreground tabular-nums w-20 flex-shrink-0">
{formatEventTime(event)} {formatEventTime(event)}
+4 -4
View File
@@ -201,7 +201,7 @@ export function ContactDetail({ contact, onEdit, onDelete, onAddToGroup, onDupli
onClick={onCompose} onClick={onCompose}
className="touch-manipulation" className="touch-manipulation"
> >
<Send className="w-4 h-4 mr-1" /> <Send className="w-4 h-4 me-1" />
{t("detail.compose_email")} {t("detail.compose_email")}
</Button> </Button>
)} )}
@@ -210,12 +210,12 @@ export function ContactDetail({ contact, onEdit, onDelete, onAddToGroup, onDupli
href={`tel:${phone}`} href={`tel:${phone}`}
className="inline-flex items-center justify-center rounded-md font-medium h-9 px-3 text-sm border border-input bg-background hover:bg-accent hover:text-accent-foreground transition-colors touch-manipulation" className="inline-flex items-center justify-center rounded-md font-medium h-9 px-3 text-sm border border-input bg-background hover:bg-accent hover:text-accent-foreground transition-colors touch-manipulation"
> >
<Phone className="w-4 h-4 mr-1" /> <Phone className="w-4 h-4 me-1" />
{t("context_menu.call")} {t("context_menu.call")}
</a> </a>
)} )}
<Button variant="outline" size="sm" onClick={onEdit} className="touch-manipulation"> <Button variant="outline" size="sm" onClick={onEdit} className="touch-manipulation">
<Pencil className="w-4 h-4 mr-1" /> <Pencil className="w-4 h-4 me-1" />
{t("form.edit_title")} {t("form.edit_title")}
</Button> </Button>
<MoreActionsMenu items={moreItems} label={t("detail.more_actions")} /> <MoreActionsMenu items={moreItems} label={t("detail.more_actions")} />
@@ -587,7 +587,7 @@ function MoreActionsMenu({ items, label }: { items: MoreItem[]; label: string })
setOpen(false); setOpen(false);
}} }}
className={cn( className={cn(
"w-full flex items-center gap-2 px-3 py-1.5 text-sm text-left hover:bg-muted focus:bg-muted focus:outline-none transition-colors", "w-full flex items-center gap-2 px-3 py-1.5 text-sm text-start hover:bg-muted focus:bg-muted focus:outline-none transition-colors",
item.destructive && "text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-950 focus:bg-red-50 dark:focus:bg-red-950", item.destructive && "text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-950 focus:bg-red-50 dark:focus:bg-red-950",
)} )}
> >
+10 -10
View File
@@ -72,7 +72,7 @@ function FormSection({ icon: Icon, title, children, collapsible, defaultOpen = t
<button <button
type="button" type="button"
className={cn( className={cn(
"flex items-center gap-2 w-full text-left", "flex items-center gap-2 w-full text-start",
collapsible ? "cursor-pointer" : "cursor-default" collapsible ? "cursor-pointer" : "cursor-default"
)} )}
onClick={() => collapsible && setOpen(!open)} onClick={() => collapsible && setOpen(!open)}
@@ -736,12 +736,12 @@ export function ContactForm({ contact, addressBooks, allKeywords, defaultAddress
)} )}
</div> </div>
{emailErrors[i] && ( {emailErrors[i] && (
<p className="text-xs text-red-600 dark:text-red-400 mt-1 ml-1">{emailErrors[i]}</p> <p className="text-xs text-red-600 dark:text-red-400 mt-1 ms-1">{emailErrors[i]}</p>
)} )}
</div> </div>
))} ))}
<Button type="button" variant="ghost" size="sm" onClick={() => setEmails([...emails, { address: "", context: "" }])} className="text-xs"> <Button type="button" variant="ghost" size="sm" onClick={() => setEmails([...emails, { address: "", context: "" }])} className="text-xs">
<Plus className="w-3 h-3 mr-1" /> <Plus className="w-3 h-3 me-1" />
{t("add_email")} {t("add_email")}
</Button> </Button>
</div> </div>
@@ -799,7 +799,7 @@ export function ContactForm({ contact, addressBooks, allKeywords, defaultAddress
</div> </div>
))} ))}
<Button type="button" variant="ghost" size="sm" onClick={() => setPhones([...phones, { number: "", context: "", feature: "" }])} className="text-xs"> <Button type="button" variant="ghost" size="sm" onClick={() => setPhones([...phones, { number: "", context: "", feature: "" }])} className="text-xs">
<Plus className="w-3 h-3 mr-1" /> <Plus className="w-3 h-3 me-1" />
{t("add_phone")} {t("add_phone")}
</Button> </Button>
</div> </div>
@@ -855,7 +855,7 @@ export function ContactForm({ contact, addressBooks, allKeywords, defaultAddress
</div> </div>
))} ))}
<Button type="button" variant="ghost" size="sm" onClick={() => setAddresses([...addresses, { street: "", locality: "", region: "", postcode: "", country: "", context: "" }])} className="text-xs"> <Button type="button" variant="ghost" size="sm" onClick={() => setAddresses([...addresses, { street: "", locality: "", region: "", postcode: "", country: "", context: "" }])} className="text-xs">
<Plus className="w-3 h-3 mr-1" /> <Plus className="w-3 h-3 me-1" />
{t("add_address")} {t("add_address")}
</Button> </Button>
</div> </div>
@@ -884,7 +884,7 @@ export function ContactForm({ contact, addressBooks, allKeywords, defaultAddress
</div> </div>
))} ))}
<Button type="button" variant="ghost" size="sm" onClick={() => setOnlineServices([...onlineServices, { uri: "", service: "", label: "" }])} className="text-xs"> <Button type="button" variant="ghost" size="sm" onClick={() => setOnlineServices([...onlineServices, { uri: "", service: "", label: "" }])} className="text-xs">
<Plus className="w-3 h-3 mr-1" /> <Plus className="w-3 h-3 me-1" />
{t("add_online_service")} {t("add_online_service")}
</Button> </Button>
</div> </div>
@@ -916,7 +916,7 @@ export function ContactForm({ contact, addressBooks, allKeywords, defaultAddress
</div> </div>
))} ))}
<Button type="button" variant="ghost" size="sm" onClick={() => setAnniversaries([...anniversaries, { date: "", kind: "birth" }])} className="text-xs"> <Button type="button" variant="ghost" size="sm" onClick={() => setAnniversaries([...anniversaries, { date: "", kind: "birth" }])} className="text-xs">
<Plus className="w-3 h-3 mr-1" /> <Plus className="w-3 h-3 me-1" />
{t("add_anniversary")} {t("add_anniversary")}
</Button> </Button>
</div> </div>
@@ -957,7 +957,7 @@ export function ContactForm({ contact, addressBooks, allKeywords, defaultAddress
</div> </div>
))} ))}
<Button type="button" variant="ghost" size="sm" onClick={() => setPersonalInfoEntries([...personalInfoEntries, { value: "", kind: "hobby", level: "" }])} className="text-xs"> <Button type="button" variant="ghost" size="sm" onClick={() => setPersonalInfoEntries([...personalInfoEntries, { value: "", kind: "hobby", level: "" }])} className="text-xs">
<Plus className="w-3 h-3 mr-1" /> <Plus className="w-3 h-3 me-1" />
{t("add_personal_info")} {t("add_personal_info")}
</Button> </Button>
</div> </div>
@@ -1145,7 +1145,7 @@ function CategoryComboBox({
<button <button
key={kw} key={kw}
type="button" type="button"
className="w-full flex items-center gap-2 px-3 py-1.5 text-sm hover:bg-accent transition-colors text-left" className="w-full flex items-center gap-2 px-3 py-1.5 text-sm hover:bg-accent transition-colors text-start"
onClick={() => { addKeyword(kw); inputRef.current?.focus(); }} onClick={() => { addKeyword(kw); inputRef.current?.focus(); }}
> >
<Tag className="w-3.5 h-3.5 text-muted-foreground flex-shrink-0" /> <Tag className="w-3.5 h-3.5 text-muted-foreground flex-shrink-0" />
@@ -1155,7 +1155,7 @@ function CategoryComboBox({
{canAddNew && ( {canAddNew && (
<button <button
type="button" type="button"
className="w-full flex items-center gap-2 px-3 py-1.5 text-sm hover:bg-accent transition-colors text-left text-primary" className="w-full flex items-center gap-2 px-3 py-1.5 text-sm hover:bg-accent transition-colors text-start text-primary"
onClick={() => { addKeyword(inputValue); inputRef.current?.focus(); }} onClick={() => { addKeyword(inputValue); inputRef.current?.focus(); }}
> >
<Plus className="w-3.5 h-3.5 flex-shrink-0" /> <Plus className="w-3.5 h-3.5 flex-shrink-0" />
+2 -2
View File
@@ -53,7 +53,7 @@ export function ContactGroupDetail({
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<Button variant="outline" size="sm" onClick={onEdit} className="touch-manipulation"> <Button variant="outline" size="sm" onClick={onEdit} className="touch-manipulation">
<Pencil className="w-4 h-4 mr-1" /> <Pencil className="w-4 h-4 me-1" />
{t("form.edit_title")} {t("form.edit_title")}
</Button> </Button>
<Button <Button
@@ -106,7 +106,7 @@ export function ContactGroupDetail({
className="flex items-center gap-3 px-3 py-2.5 rounded-md hover:bg-muted group transition-colors" className="flex items-center gap-3 px-3 py-2.5 rounded-md hover:bg-muted group transition-colors"
> >
<button <button
className="flex items-center gap-3 flex-1 min-w-0 text-left" className="flex items-center gap-3 flex-1 min-w-0 text-start"
onClick={() => onSelectMember(member.id)} onClick={() => onSelectMember(member.id)}
> >
<Avatar name={mName} email={mEmail} size="sm" /> <Avatar name={mName} email={mEmail} size="sm" />
+2 -2
View File
@@ -114,7 +114,7 @@ export function ContactGroupForm({
placeholder={t("groups.search_members")} placeholder={t("groups.search_members")}
value={memberSearch} value={memberSearch}
onChange={(e) => setMemberSearch(e.target.value)} onChange={(e) => setMemberSearch(e.target.value)}
className="pl-9" className="ps-9"
/> />
</div> </div>
@@ -134,7 +134,7 @@ export function ContactGroupForm({
type="button" type="button"
onClick={() => toggleMember(contact.id)} onClick={() => toggleMember(contact.id)}
className={cn( className={cn(
"w-full flex items-center gap-3 px-3 py-2.5 text-left transition-colors", "w-full flex items-center gap-3 px-3 py-2.5 text-start transition-colors",
"hover:bg-muted", "hover:bg-muted",
isSelected && "bg-primary/5" isSelected && "bg-primary/5"
)} )}
+2 -2
View File
@@ -45,7 +45,7 @@ export function ContactGroupList({
<div className={cn("flex flex-col", className)}> <div className={cn("flex flex-col", className)}>
<div className="px-4 py-2 border-b border-border"> <div className="px-4 py-2 border-b border-border">
<Button size="sm" variant="outline" onClick={onCreateGroup} className="w-full"> <Button size="sm" variant="outline" onClick={onCreateGroup} className="w-full">
<Plus className="w-4 h-4 mr-1" /> <Plus className="w-4 h-4 me-1" />
{t("groups.create")} {t("groups.create")}
</Button> </Button>
</div> </div>
@@ -69,7 +69,7 @@ export function ContactGroupList({
key={group.id} key={group.id}
onClick={() => onSelectGroup(group.id)} onClick={() => onSelectGroup(group.id)}
className={cn( className={cn(
"w-full flex items-center px-4 text-left transition-colors", "w-full flex items-center px-4 text-start transition-colors",
"hover:bg-muted", "hover:bg-muted",
group.id === selectedGroupId && "bg-accent text-accent-foreground" group.id === selectedGroupId && "bg-accent text-accent-foreground"
)} )}
@@ -187,7 +187,7 @@ export function ContactImportDialog({
type="button" type="button"
onClick={() => toggleSelect(idx)} onClick={() => toggleSelect(idx)}
className={cn( className={cn(
"w-full flex items-center gap-3 px-3 py-2.5 text-left transition-colors hover:bg-muted", "w-full flex items-center gap-3 px-3 py-2.5 text-start transition-colors hover:bg-muted",
isSelected && "bg-primary/5" isSelected && "bg-primary/5"
)} )}
> >
+7 -7
View File
@@ -310,7 +310,7 @@ export function ContactList({
placeholder={t("search_placeholder")} placeholder={t("search_placeholder")}
value={searchQuery} value={searchQuery}
onChange={(e) => onSearchChange(e.target.value)} onChange={(e) => onSearchChange(e.target.value)}
className={cn("pl-9 h-9", searchQuery && "pr-8")} className={cn("ps-9 h-9", searchQuery && "pe-8")}
/> />
{searchQuery && ( {searchQuery && (
<button <button
@@ -359,7 +359,7 @@ export function ContactList({
onClick={() => setFilters(EMPTY_FILTERS)} onClick={() => setFilters(EMPTY_FILTERS)}
className="h-7 px-2 text-xs" className="h-7 px-2 text-xs"
> >
<RotateCcw className="w-3 h-3 mr-1" /> <RotateCcw className="w-3 h-3 me-1" />
{t("filters.clear")} {t("filters.clear")}
</Button> </Button>
<Button <Button
@@ -479,11 +479,11 @@ export function ContactList({
</span> </span>
<div className="flex-1" /> <div className="flex-1" />
<Button variant="ghost" size="sm" onClick={onBulkAddToGroup} className="h-7 text-xs"> <Button variant="ghost" size="sm" onClick={onBulkAddToGroup} className="h-7 text-xs">
<Users className="w-3.5 h-3.5 mr-1" /> <Users className="w-3.5 h-3.5 me-1" />
{t("bulk.add_to_group")} {t("bulk.add_to_group")}
</Button> </Button>
<Button variant="ghost" size="sm" onClick={onBulkExport} className="h-7 text-xs"> <Button variant="ghost" size="sm" onClick={onBulkExport} className="h-7 text-xs">
<Download className="w-3.5 h-3.5 mr-1" /> <Download className="w-3.5 h-3.5 me-1" />
{t("bulk.export")} {t("bulk.export")}
</Button> </Button>
<Button <Button
@@ -492,7 +492,7 @@ export function ContactList({
onClick={onBulkDelete} onClick={onBulkDelete}
className="h-7 text-xs text-red-600 dark:text-red-400 hover:text-red-700 dark:hover:text-red-300" className="h-7 text-xs text-red-600 dark:text-red-400 hover:text-red-700 dark:hover:text-red-300"
> >
<Trash2 className="w-3.5 h-3.5 mr-1" /> <Trash2 className="w-3.5 h-3.5 me-1" />
{t("bulk.delete")} {t("bulk.delete")}
</Button> </Button>
<Button variant="ghost" size="icon" onClick={onClearSelection} className="h-7 w-7"> <Button variant="ghost" size="icon" onClick={onClearSelection} className="h-7 w-7">
@@ -526,7 +526,7 @@ export function ContactList({
)} )}
{activeFilters > 0 && ( {activeFilters > 0 && (
<Button variant="outline" size="sm" onClick={() => setFilters(EMPTY_FILTERS)}> <Button variant="outline" size="sm" onClick={() => setFilters(EMPTY_FILTERS)}>
<RotateCcw className="w-3.5 h-3.5 mr-1" /> <RotateCcw className="w-3.5 h-3.5 me-1" />
{t("filters.clear")} {t("filters.clear")}
</Button> </Button>
)} )}
@@ -538,7 +538,7 @@ export function ContactList({
<p className="text-sm font-medium text-foreground">{t("empty_state_title")}</p> <p className="text-sm font-medium text-foreground">{t("empty_state_title")}</p>
<p className="text-xs text-muted-foreground mt-1">{t("empty_state_subtitle")}</p> <p className="text-xs text-muted-foreground mt-1">{t("empty_state_subtitle")}</p>
<Button size="sm" className="mt-3" onClick={onCreateNew}> <Button size="sm" className="mt-3" onClick={onCreateNew}>
<UserPlus className="w-4 h-4 mr-1.5" /> <UserPlus className="w-4 h-4 me-1.5" />
{t("create_new")} {t("create_new")}
</Button> </Button>
</> </>
+20 -20
View File
@@ -287,14 +287,14 @@ export function ContactsSidebar({
className="absolute right-0 top-full mt-1 w-44 rounded-md border border-border bg-background text-foreground shadow-md z-50 py-1" className="absolute right-0 top-full mt-1 w-44 rounded-md border border-border bg-background text-foreground shadow-md z-50 py-1"
> >
<button <button
className="w-full flex items-center gap-2 px-3 py-1.5 text-sm hover:bg-accent transition-colors text-left" className="w-full flex items-center gap-2 px-3 py-1.5 text-sm hover:bg-accent transition-colors text-start"
onClick={() => { setShowMenu(false); onCreateContact(); }} onClick={() => { setShowMenu(false); onCreateContact(); }}
> >
<UserPlus className="w-4 h-4" /> <UserPlus className="w-4 h-4" />
{t("create_new")} {t("create_new")}
</button> </button>
<button <button
className="w-full flex items-center gap-2 px-3 py-1.5 text-sm hover:bg-accent transition-colors text-left" className="w-full flex items-center gap-2 px-3 py-1.5 text-sm hover:bg-accent transition-colors text-start"
onClick={() => { setShowMenu(false); onCreateGroup(); }} onClick={() => { setShowMenu(false); onCreateGroup(); }}
> >
<UsersRound className="w-4 h-4" /> <UsersRound className="w-4 h-4" />
@@ -302,7 +302,7 @@ export function ContactsSidebar({
</button> </button>
{onCreateAddressBook && ( {onCreateAddressBook && (
<button <button
className="w-full flex items-center gap-2 px-3 py-1.5 text-sm hover:bg-accent transition-colors text-left" className="w-full flex items-center gap-2 px-3 py-1.5 text-sm hover:bg-accent transition-colors text-start"
onClick={() => { setShowMenu(false); onCreateAddressBook(); }} onClick={() => { setShowMenu(false); onCreateAddressBook(); }}
> >
<BookPlus className="w-4 h-4" /> <BookPlus className="w-4 h-4" />
@@ -311,7 +311,7 @@ export function ContactsSidebar({
)} )}
{onImport && ( {onImport && (
<button <button
className="w-full flex items-center gap-2 px-3 py-1.5 text-sm hover:bg-accent transition-colors text-left" className="w-full flex items-center gap-2 px-3 py-1.5 text-sm hover:bg-accent transition-colors text-start"
onClick={() => { setShowMenu(false); onImport(); }} onClick={() => { setShowMenu(false); onImport(); }}
> >
<Upload className="w-4 h-4" /> <Upload className="w-4 h-4" />
@@ -338,7 +338,7 @@ export function ContactsSidebar({
> >
<BookUser className="w-4 h-4 flex-shrink-0" /> <BookUser className="w-4 h-4 flex-shrink-0" />
<span className="truncate">{t("tabs.all")}</span> <span className="truncate">{t("tabs.all")}</span>
<span className="ml-auto text-xs text-muted-foreground tabular-nums"> <span className="ms-auto text-xs text-muted-foreground tabular-nums">
{individuals.length} {individuals.length}
</span> </span>
</button> </button>
@@ -355,7 +355,7 @@ export function ContactsSidebar({
<div className="flex items-center px-3 py-1 group"> <div className="flex items-center px-3 py-1 group">
<button <button
onClick={() => toggleSection(sectionKey)} onClick={() => toggleSection(sectionKey)}
className="flex items-center gap-1 flex-1 min-w-0 text-left" className="flex items-center gap-1 flex-1 min-w-0 text-start"
> >
{expanded ? ( {expanded ? (
<ChevronDown className="w-3 h-3 text-muted-foreground" /> <ChevronDown className="w-3 h-3 text-muted-foreground" />
@@ -369,7 +369,7 @@ export function ContactsSidebar({
</button> </button>
</div> </div>
{expanded && ( {expanded && (
<div className="pl-2"> <div className="ps-2">
{owned.length > 0 && ( {owned.length > 0 && (
<div className="mt-1"> <div className="mt-1">
<div className="px-3 py-0.5 text-[10px] font-medium text-muted-foreground/80 uppercase tracking-wider"> <div className="px-3 py-0.5 text-[10px] font-medium text-muted-foreground/80 uppercase tracking-wider">
@@ -418,7 +418,7 @@ export function ContactsSidebar({
<div className="flex items-center px-3 py-1 group"> <div className="flex items-center px-3 py-1 group">
<button <button
onClick={() => toggleSection("addressBooks")} onClick={() => toggleSection("addressBooks")}
className="flex items-center gap-1 flex-1 text-left" className="flex items-center gap-1 flex-1 text-start"
> >
{collapsed.addressBooks ? ( {collapsed.addressBooks ? (
<ChevronRight className="w-3 h-3 text-muted-foreground" /> <ChevronRight className="w-3 h-3 text-muted-foreground" />
@@ -461,7 +461,7 @@ export function ContactsSidebar({
<div className="mt-2"> <div className="mt-2">
<button <button
onClick={() => toggleSection("groups")} onClick={() => toggleSection("groups")}
className="flex items-center gap-1 px-3 py-1 w-full text-left group" className="flex items-center gap-1 px-3 py-1 w-full text-start group"
> >
{collapsed.groups ? ( {collapsed.groups ? (
<ChevronRight className="w-3 h-3 text-muted-foreground" /> <ChevronRight className="w-3 h-3 text-muted-foreground" />
@@ -483,7 +483,7 @@ export function ContactsSidebar({
onClick={() => onSelectCategory({ groupId: group.id })} onClick={() => onSelectCategory({ groupId: group.id })}
onContextMenu={(e) => openGroupContextMenu(e, group)} onContextMenu={(e) => openGroupContextMenu(e, group)}
className={cn( className={cn(
"w-full flex items-center gap-2 pl-5 pr-3 text-sm transition-colors", "w-full flex items-center gap-2 ps-5 pe-3 text-sm transition-colors",
isActive isActive
? "bg-accent text-accent-foreground font-medium" ? "bg-accent text-accent-foreground font-medium"
: "text-foreground/80 hover:bg-muted" : "text-foreground/80 hover:bg-muted"
@@ -492,7 +492,7 @@ export function ContactsSidebar({
> >
<Users className="w-4 h-4 flex-shrink-0" /> <Users className="w-4 h-4 flex-shrink-0" />
<span className="truncate">{getContactDisplayName(group)}</span> <span className="truncate">{getContactDisplayName(group)}</span>
<span className="ml-auto text-xs text-muted-foreground tabular-nums"> <span className="ms-auto text-xs text-muted-foreground tabular-nums">
{memberCount} {memberCount}
</span> </span>
</button> </button>
@@ -505,7 +505,7 @@ export function ContactsSidebar({
<div className="mt-2"> <div className="mt-2">
<button <button
onClick={() => toggleSection("categories")} onClick={() => toggleSection("categories")}
className="flex items-center gap-1 px-3 py-1 w-full text-left group" className="flex items-center gap-1 px-3 py-1 w-full text-start group"
> >
{collapsed.categories ? ( {collapsed.categories ? (
<ChevronRight className="w-3 h-3 text-muted-foreground" /> <ChevronRight className="w-3 h-3 text-muted-foreground" />
@@ -523,7 +523,7 @@ export function ContactsSidebar({
<button <button
onClick={() => onSelectCategory("uncategorized")} onClick={() => onSelectCategory("uncategorized")}
className={cn( className={cn(
"w-full flex items-center gap-2 pl-5 pr-3 text-sm transition-colors", "w-full flex items-center gap-2 ps-5 pe-3 text-sm transition-colors",
activeCategory === "uncategorized" activeCategory === "uncategorized"
? "bg-accent text-accent-foreground font-medium" ? "bg-accent text-accent-foreground font-medium"
: "text-foreground/80 hover:bg-muted" : "text-foreground/80 hover:bg-muted"
@@ -532,7 +532,7 @@ export function ContactsSidebar({
> >
<Tag className="w-3.5 h-3.5 flex-shrink-0 opacity-50" /> <Tag className="w-3.5 h-3.5 flex-shrink-0 opacity-50" />
<span className="truncate italic">{t("no_category")}</span> <span className="truncate italic">{t("no_category")}</span>
<span className="ml-auto text-xs text-muted-foreground tabular-nums"> <span className="ms-auto text-xs text-muted-foreground tabular-nums">
{uncategorizedCount} {uncategorizedCount}
</span> </span>
</button> </button>
@@ -561,7 +561,7 @@ export function ContactsSidebar({
<div className="flex items-center px-3 py-1 group"> <div className="flex items-center px-3 py-1 group">
<button <button
onClick={() => toggleSection(`shared-${group.accountId}`)} onClick={() => toggleSection(`shared-${group.accountId}`)}
className="flex items-center gap-1 flex-1 min-w-0 text-left" className="flex items-center gap-1 flex-1 min-w-0 text-start"
> >
{collapsed[`shared-${group.accountId}`] ? ( {collapsed[`shared-${group.accountId}`] ? (
<ChevronRight className="w-3 h-3 text-muted-foreground" /> <ChevronRight className="w-3 h-3 text-muted-foreground" />
@@ -779,7 +779,7 @@ function CategoryItem({
onDragLeave={handleDragLeave} onDragLeave={handleDragLeave}
onDrop={handleDrop} onDrop={handleDrop}
className={cn( className={cn(
"w-full flex items-center gap-2 pl-5 pr-3 text-sm transition-colors", "w-full flex items-center gap-2 ps-5 pe-3 text-sm transition-colors",
isActive isActive
? "bg-accent text-accent-foreground font-medium" ? "bg-accent text-accent-foreground font-medium"
: "text-foreground/80 hover:bg-muted", : "text-foreground/80 hover:bg-muted",
@@ -789,7 +789,7 @@ function CategoryItem({
> >
<Tag className="w-3.5 h-3.5 flex-shrink-0" /> <Tag className="w-3.5 h-3.5 flex-shrink-0" />
<span className="truncate">{keyword}</span> <span className="truncate">{keyword}</span>
<span className="ml-auto text-xs text-muted-foreground tabular-nums"> <span className="ms-auto text-xs text-muted-foreground tabular-nums">
{count} {count}
</span> </span>
</button> </button>
@@ -847,7 +847,7 @@ function AddressBookItem({
onDragLeave={handleDragLeave} onDragLeave={handleDragLeave}
onDrop={handleDrop} onDrop={handleDrop}
className={cn( className={cn(
"w-full flex items-center gap-2 pl-5 pr-3 text-sm transition-colors", "w-full flex items-center gap-2 ps-5 pe-3 text-sm transition-colors",
isActive isActive
? "bg-accent text-accent-foreground font-medium" ? "bg-accent text-accent-foreground font-medium"
: "text-foreground/80 hover:bg-muted", : "text-foreground/80 hover:bg-muted",
@@ -858,11 +858,11 @@ function AddressBookItem({
<Book className="w-4 h-4 flex-shrink-0" /> <Book className="w-4 h-4 flex-shrink-0" />
<span className="truncate">{book.name}</span> <span className="truncate">{book.name}</span>
{!book.isShared && Object.keys(book.shareWith || {}).length > 0 && ( {!book.isShared && Object.keys(book.shareWith || {}).length > 0 && (
<Users className="w-3 h-3 text-muted-foreground flex-shrink-0 ml-auto" /> <Users className="w-3 h-3 text-muted-foreground flex-shrink-0 ms-auto" />
)} )}
<span className={cn( <span className={cn(
"text-xs text-muted-foreground tabular-nums", "text-xs text-muted-foreground tabular-nums",
!(!book.isShared && Object.keys(book.shareWith || {}).length > 0) && "ml-auto" !(!book.isShared && Object.keys(book.shareWith || {}).length > 0) && "ms-auto"
)}> )}>
{contactCount} {contactCount}
</span> </span>
@@ -1053,7 +1053,7 @@ export function CalendarInvitationBanner({ email }: CalendarInvitationBannerProp
setShowCalendarPicker(false); setShowCalendarPicker(false);
handleImport(cal.id); 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 <span
className="w-3 h-3 rounded-full flex-shrink-0" className="w-3 h-3 rounded-full flex-shrink-0"
@@ -1095,7 +1095,7 @@ export function CalendarInvitationBanner({ email }: CalendarInvitationBannerProp
)} )}
{isProcessing && ( {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> </div>
)} )}
+11 -11
View File
@@ -1990,7 +1990,7 @@ export function EmailComposer({
<div ref={composerRootRef} className={cn("flex h-full bg-background", className)}> <div ref={composerRootRef} className={cn("flex h-full bg-background", className)}>
<PluginSlot <PluginSlot
name="composer-sidebar" 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. */} {/* Right-side composer sidebar slot is rendered after the main content div below. */}
<div <div
@@ -2047,7 +2047,7 @@ export function EmailComposer({
size="sm" size="sm"
className="md:hidden h-9 px-4" 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')} {t('send')}
</Button> </Button>
</div> </div>
@@ -2445,7 +2445,7 @@ export function EmailComposer({
)} )}
<button <button
onClick={() => removeAttachment(index)} 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} title={att.uploading ? t('upload_cancel') : undefined}
> >
<X className="w-3 h-3" /> <X className="w-3 h-3" />
@@ -2540,9 +2540,9 @@ export function EmailComposer({
onClick={() => handleSend()} onClick={() => handleSend()}
disabled={!canSend || isSending} disabled={!canSend || isSending}
title={getSendTooltip()} 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')} {t('send')}
</Button> </Button>
<Button <Button
@@ -2550,7 +2550,7 @@ export function EmailComposer({
onClick={() => setShowSendMenu((open) => !open)} onClick={() => setShowSendMenu((open) => !open)}
disabled={!canSend || isSending} disabled={!canSend || isSending}
title={t('schedule_send')} title={t('schedule_send')}
className="rounded-l-none px-2" className="rounded-s-none px-2"
aria-haspopup="menu" aria-haspopup="menu"
aria-expanded={showSendMenu} aria-expanded={showSendMenu}
> >
@@ -2565,7 +2565,7 @@ export function EmailComposer({
type="button" type="button"
role="menuitem" role="menuitem"
onClick={openScheduleDialog} 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" /> <CalendarClock className="w-4 h-4" />
{t('schedule_send')} {t('schedule_send')}
@@ -2580,7 +2580,7 @@ export function EmailComposer({
title={getSendTooltip()} title={getSendTooltip()}
className="hidden md:inline-flex" className="hidden md:inline-flex"
> >
<Send className="w-4 h-4 mr-2" /> <Send className="w-4 h-4 me-2" />
{t('send')} {t('send')}
</Button> </Button>
)} )}
@@ -2699,7 +2699,7 @@ export function EmailComposer({
{t('discard')} {t('discard')}
</Button> </Button>
<Button onClick={handleSaveDraftAndClose}> <Button onClick={handleSaveDraftAndClose}>
<Save className="w-4 h-4 mr-2" /> <Save className="w-4 h-4 me-2" />
{t('save_draft')} {t('save_draft')}
</Button> </Button>
</div> </div>
@@ -2718,7 +2718,7 @@ export function EmailComposer({
</div> </div>
<PluginSlot <PluginSlot
name="composer-sidebar-right" 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> </div>
); );
@@ -2743,7 +2743,7 @@ const AutocompleteDropdown = React.forwardRef<HTMLDivElement, {
role="option" role="option"
aria-selected={i === selectedIndex} aria-selected={i === selectedIndex}
className={cn( 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" i === selectedIndex ? "bg-accent text-accent-foreground" : "hover:bg-muted"
)} )}
onMouseDown={(e) => { onMouseDown={(e) => {
+2 -2
View File
@@ -334,7 +334,7 @@ export function EmailContextMenu({
</div> </div>
)} )}
{node.children.length > 0 && ( {node.children.length > 0 && (
<div className="pl-4"> <div className="ps-4">
{renderNodes(node.children)} {renderNodes(node.children)}
</div> </div>
)} )}
@@ -377,7 +377,7 @@ export function EmailContextMenu({
role="menuitem" role="menuitem"
onClick={() => handleAction(() => onSetColorTag?.(option.value))} onClick={() => handleAction(() => onSetColorTag?.(option.value))}
className={cn( 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" isActive && "bg-accent font-medium"
)} )}
> >
+8 -7
View File
@@ -181,16 +181,17 @@ export function EmailHoverActions({
return ( return (
<div <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 <div
className={cn("w-8 h-full", hoverBackgroundClassName)} className={cn(
style={{ "w-8 h-full",
WebkitMaskImage: "linear-gradient(to right, transparent, black)", "[mask-image:linear-gradient(to_right,transparent,black)] [-webkit-mask-image:linear-gradient(to_right,transparent,black)]",
maskImage: "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} {actionButtons}
</div> </div>
</div> </div>
+1 -1
View File
@@ -176,7 +176,7 @@ export function EmailListItem({ email, selected, onClick, onDoubleClick, onConte
{/* Unread indicator */} {/* Unread indicator */}
{isUnread && ( {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" /> <Circle className="w-2 h-2 fill-unread text-unread" />
</div> </div>
)} )}
+2 -2
View File
@@ -417,9 +417,9 @@ export function EmailList({
className="text-destructive border-destructive/30 hover:bg-destructive/10 text-xs" className="text-destructive border-destructive/30 hover:bg-destructive/10 text-xs"
> >
{isProcessing ? ( {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')} {t('empty_folder.button')}
</Button> </Button>
+65 -65
View File
@@ -333,7 +333,7 @@ function renderClickableRecipients(
return ( return (
<span key={r.email + index} className="inline-flex items-center"> <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 <RecipientPopover
name={r.name} name={r.name}
email={r.email} email={r.email}
@@ -382,7 +382,7 @@ export function ContactSidebarPanel({
}; };
return ( 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 */} {/* Header */}
<div className="flex items-center justify-between px-4 py-3 border-b border-border"> <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> <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" /> <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> <h4 className="text-xs font-medium text-muted-foreground uppercase tracking-wider">{title}</h4>
</div> </div>
<div className="space-y-1 pl-5.5">{children}</div> <div className="space-y-1 ps-5.5">{children}</div>
</div> </div>
); );
} }
@@ -2637,7 +2637,7 @@ export function EmailViewer({
<h3 className="text-xl font-semibold text-foreground mb-3">{tDemoWelcome('title')}</h3> <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> <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="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"> <div className="flex items-center gap-2">
<Mail className="w-4 h-4 text-primary shrink-0" /> <Mail className="w-4 h-4 text-primary shrink-0" />
<span>{tDemoWelcome('feature_email')}</span> <span>{tDemoWelcome('feature_email')}</span>
@@ -2679,7 +2679,7 @@ export function EmailViewer({
<p className="text-muted-foreground">{t('no_conversation_description')}</p> <p className="text-muted-foreground">{t('no_conversation_description')}</p>
{onCompose && ( {onCompose && (
<Button onClick={onCompose} className="mt-6" title={t('compose_hint')}> <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')} {t('compose')}
</Button> </Button>
)} )}
@@ -2704,7 +2704,7 @@ export function EmailViewer({
variant="ghost" variant="ghost"
size="icon" size="icon"
onClick={onBack} 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')} aria-label={t('back_to_list')}
> >
<ChevronLeft className="w-5 h-5" /> <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>} {showToolbarLabels && <span className="text-[10px] leading-tight sm:text-sm">{t('move')}</span>}
</Button> </Button>
{moveMenuOpen && ( {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) => { const renderNodes = (nodes: MailboxNode[], depth = 0) => {
return nodes.map((node) => { return nodes.map((node) => {
@@ -2850,7 +2850,7 @@ export function EmailViewer({
{isTarget ? ( {isTarget ? (
<button <button
onClick={() => { onMoveToMailbox(node.id); setMoveMenuOpen(false); }} 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` }} style={{ paddingLeft: `${0.75 + depth * 1}rem` }}
> >
<Icon className="w-4 h-4 flex-shrink-0" /> <Icon className="w-4 h-4 flex-shrink-0" />
@@ -2910,7 +2910,7 @@ export function EmailViewer({
)} )}
</button> </button>
{tagMenuOpen && ( {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) => { {colorOptions.map((option) => {
const isActive = currentColors.includes(option.value); const isActive = currentColors.includes(option.value);
return ( return (
@@ -2918,13 +2918,13 @@ export function EmailViewer({
key={option.value} key={option.value}
onClick={() => { if (email) onSetColorTag?.(email.id, option.value); setTagMenuOpen(false); }} onClick={() => { if (email) onSetColorTag?.(email.id, option.value); setTagMenuOpen(false); }}
className={cn( 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" isActive && "bg-accent font-medium"
)} )}
> >
<span className={cn("w-3 h-3 rounded-full flex-shrink-0", option.color)} /> <span className={cn("w-3 h-3 rounded-full flex-shrink-0", option.color)} />
<span className="truncate">{option.name}</span> <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> </button>
); );
})} })}
@@ -2933,7 +2933,7 @@ export function EmailViewer({
<div className="h-px bg-border my-1" /> <div className="h-px bg-border my-1" />
<button <button
onClick={() => { if (email) onSetColorTag?.(email.id, null); setTagMenuOpen(false); }} 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" /> <X className="w-3 h-3 flex-shrink-0" />
<span>{t('remove_color')}</span> <span>{t('remove_color')}</span>
@@ -3037,11 +3037,11 @@ export function EmailViewer({
<span className="text-[10px] leading-tight sm:hidden">{t('more_actions')}</span> <span className="text-[10px] leading-tight sm:hidden">{t('more_actions')}</span>
</Button> </Button>
{moreMenuOpen && !isMobile && ( {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 */} {/* Star toggle */}
<button <button
onClick={() => { onToggleStar?.(); setMoreMenuOpen(false); setMoreMenuSub(null); }} 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")} /> <Star className={cn("w-4 h-4", isStarred && "fill-yellow-400 text-yellow-400")} />
{isStarred ? t('tooltips.unstar') : t('tooltips.star')} {isStarred ? t('tooltips.unstar') : t('tooltips.star')}
@@ -3049,7 +3049,7 @@ export function EmailViewer({
{/* Overflow: reply */} {/* Overflow: reply */}
<button <button
onClick={() => { onReply?.(); setMoreMenuOpen(false); setMoreMenuSub(null); }} 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" /> <Reply className="w-4 h-4" />
{t('reply')} {t('reply')}
@@ -3057,7 +3057,7 @@ export function EmailViewer({
{/* Overflow: reply all */} {/* Overflow: reply all */}
<button <button
onClick={() => { onReplyAll?.(); setMoreMenuOpen(false); setMoreMenuSub(null); }} 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" /> <ReplyAll className="w-4 h-4" />
{t('reply_all')} {t('reply_all')}
@@ -3065,7 +3065,7 @@ export function EmailViewer({
{/* Overflow: forward */} {/* Overflow: forward */}
<button <button
onClick={() => { onForward?.(); setMoreMenuOpen(false); setMoreMenuSub(null); }} 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" /> <Forward className="w-4 h-4" />
{t('forward')} {t('forward')}
@@ -3073,7 +3073,7 @@ export function EmailViewer({
{/* Overflow: archive */} {/* Overflow: archive */}
<button <button
onClick={() => { onArchive?.(); setMoreMenuOpen(false); setMoreMenuSub(null); }} 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" /> <Archive className="w-4 h-4" />
{t('archive')} {t('archive')}
@@ -3086,14 +3086,14 @@ export function EmailViewer({
> >
<button <button
onClick={() => setMoreMenuSub(moreMenuSub === 'move' ? null : 'move')} 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" /> <FolderInput className="w-4 h-4" />
<span className="flex-1">{t('move_to')}</span> <span className="flex-1">{t('move_to')}</span>
<ChevronRight className="w-3 h-3 text-muted-foreground" /> <ChevronRight className="w-3 h-3 text-muted-foreground" />
</button> </button>
{moreMenuSub === 'move' && ( {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) => { const renderMobileNodes = (nodes: MailboxNode[], depth = 0) => {
return nodes.map((node) => { return nodes.map((node) => {
@@ -3104,7 +3104,7 @@ export function EmailViewer({
{isTarget ? ( {isTarget ? (
<button <button
onClick={() => { onMoveToMailbox(node.id); setMoreMenuOpen(false); setMoreMenuSub(null); }} 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` }} style={{ paddingLeft: `${0.75 + depth * 1}rem` }}
> >
<Icon className="w-4 h-4 flex-shrink-0" /> <Icon className="w-4 h-4 flex-shrink-0" />
@@ -3138,14 +3138,14 @@ export function EmailViewer({
> >
<button <button
onClick={() => setMoreMenuSub(moreMenuSub === 'tag' ? null : 'tag')} 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" /> <Tag className="w-4 h-4" />
<span className="flex-1">{t('tag')}</span> <span className="flex-1">{t('tag')}</span>
<ChevronRight className="w-3 h-3 text-muted-foreground" /> <ChevronRight className="w-3 h-3 text-muted-foreground" />
</button> </button>
{moreMenuSub === 'tag' && ( {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) => { {colorOptions.map((option) => {
const isActive = currentColors.includes(option.value); const isActive = currentColors.includes(option.value);
return ( return (
@@ -3153,13 +3153,13 @@ export function EmailViewer({
key={option.value} key={option.value}
onClick={() => { if (email) onSetColorTag?.(email.id, option.value); setMoreMenuOpen(false); setMoreMenuSub(null); }} onClick={() => { if (email) onSetColorTag?.(email.id, option.value); setMoreMenuOpen(false); setMoreMenuSub(null); }}
className={cn( 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" isActive && "bg-accent font-medium"
)} )}
> >
<span className={cn("w-3 h-3 rounded-full flex-shrink-0", option.color)} /> <span className={cn("w-3 h-3 rounded-full flex-shrink-0", option.color)} />
<span className="truncate">{option.name}</span> <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> </button>
); );
})} })}
@@ -3168,7 +3168,7 @@ export function EmailViewer({
<div className="h-px bg-border my-1" /> <div className="h-px bg-border my-1" />
<button <button
onClick={() => { if (email) onSetColorTag?.(email.id, null); setMoreMenuOpen(false); setMoreMenuSub(null); }} 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" /> <X className="w-3 h-3 flex-shrink-0" />
<span>{t('remove_color')}</span> <span>{t('remove_color')}</span>
@@ -3183,7 +3183,7 @@ export function EmailViewer({
{spamApplicable && (onMarkAsSpam || onUndoSpam) && ( {spamApplicable && (onMarkAsSpam || onUndoSpam) && (
<button <button
onClick={() => { (isInJunkFolder ? onUndoSpam : onMarkAsSpam)?.(); setMoreMenuOpen(false); setMoreMenuSub(null); }} 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 ? ( {isInJunkFolder ? (
<ShieldCheck className="h-4 w-4 text-green-600 dark:text-green-400" /> <ShieldCheck className="h-4 w-4 text-green-600 dark:text-green-400" />
@@ -3196,7 +3196,7 @@ export function EmailViewer({
{/* Overflow: toggle read */} {/* Overflow: toggle read */}
<button <button
onClick={() => { onMarkAsRead?.(email.id, isUnread); setMoreMenuOpen(false); setMoreMenuSub(null); }} 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 ? <MailOpen className="w-4 h-4" /> : <Mail className="w-4 h-4" />}
{isUnread ? t('mark_read') : t('mark_unread')} {isUnread ? t('mark_read') : t('mark_unread')}
@@ -3204,7 +3204,7 @@ export function EmailViewer({
{/* Overflow: print */} {/* Overflow: print */}
<button <button
onClick={() => { handlePrint(); setMoreMenuOpen(false); setMoreMenuSub(null); }} 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" /> <Printer className="w-4 h-4" />
{t('print')} {t('print')}
@@ -3212,7 +3212,7 @@ export function EmailViewer({
{/* Overflow: view source */} {/* Overflow: view source */}
<button <button
onClick={() => { setShowSourceModal(true); setMoreMenuOpen(false); setMoreMenuSub(null); }} 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" /> <Code className="w-4 h-4" />
{t('view_source')} {t('view_source')}
@@ -3221,7 +3221,7 @@ export function EmailViewer({
{effectiveEmailContent.isHtml && ( {effectiveEmailContent.isHtml && (
<button <button
onClick={() => { setEmailViewDarkOverride(prev => prev === null ? !(resolvedTheme === 'dark') : !prev); setMoreMenuOpen(false); setMoreMenuSub(null); }} 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 ? <Sun className="w-4 h-4" /> : <Moon className="w-4 h-4" />}
{isDark ? 'View in light mode' : 'View in dark mode'} {isDark ? 'View in light mode' : 'View in dark mode'}
@@ -3231,7 +3231,7 @@ export function EmailViewer({
{/* Export email */} {/* Export email */}
<button <button
onClick={() => { handleExportEmail(); setMoreMenuOpen(false); setMoreMenuSub(null); }} 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" /> <Download className="w-4 h-4" />
{t('export_email')} {t('export_email')}
@@ -3239,7 +3239,7 @@ export function EmailViewer({
{/* Import email */} {/* Import email */}
<button <button
onClick={() => { handleImportEmail(); setMoreMenuOpen(false); setMoreMenuSub(null); }} 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" /> <Upload className="w-4 h-4" />
{t('import_email')} {t('import_email')}
@@ -3247,7 +3247,7 @@ export function EmailViewer({
{onShowShortcuts && ( {onShowShortcuts && (
<button <button
onClick={() => { onShowShortcuts(); setMoreMenuOpen(false); setMoreMenuSub(null); }} 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" /> <Keyboard className="w-4 h-4" />
{t('keyboard_shortcuts')} {t('keyboard_shortcuts')}
@@ -3275,7 +3275,7 @@ export function EmailViewer({
)} )}
{!isScheduled && isMobile && ( {!isScheduled && isMobile && (
<div className={cn( <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", "transform transition-transform duration-300 ease-in-out",
"flex flex-col", "flex flex-col",
moreMenuOpen ? "translate-x-0" : "translate-x-full" moreMenuOpen ? "translate-x-0" : "translate-x-full"
@@ -3284,7 +3284,7 @@ export function EmailViewer({
{moreMenuSub ? ( {moreMenuSub ? (
<button <button
onClick={() => setMoreMenuSub(null)} 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" /> <ChevronLeft className="w-5 h-5" />
{moreMenuSub === 'move' ? t('move_to') : t('tag')} {moreMenuSub === 'move' ? t('move_to') : t('tag')}
@@ -3302,7 +3302,7 @@ export function EmailViewer({
{/* Star toggle */} {/* Star toggle */}
<button <button
onClick={() => { onToggleStar?.(); setMoreMenuOpen(false); }} 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")} /> <Star className={cn("w-5 h-5", isStarred && "fill-yellow-400 text-yellow-400")} />
{isStarred ? t('tooltips.unstar') : t('tooltips.star')} {isStarred ? t('tooltips.unstar') : t('tooltips.star')}
@@ -3311,12 +3311,12 @@ export function EmailViewer({
{colorOptions.length > 0 && ( {colorOptions.length > 0 && (
<button <button
onClick={() => setMoreMenuSub('tag')} 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" /> <Tag className="w-5 h-5" />
<span className="flex-1">{t('tag')}</span> <span className="flex-1">{t('tag')}</span>
{currentColors.length > 0 && ( {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) => { {currentColors.slice(0, 3).map((c) => {
const opt = colorOptions.find((o) => o.value === 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; 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 <button
onClick={() => { handlePrint(); setMoreMenuOpen(false); }} 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" /> <Printer className="w-5 h-5" />
{t('print')} {t('print')}
</button> </button>
<button <button
onClick={() => { setShowSourceModal(true); setMoreMenuOpen(false); }} 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" /> <Code className="w-5 h-5" />
{t('view_source')} {t('view_source')}
@@ -3343,7 +3343,7 @@ export function EmailViewer({
{effectiveEmailContent.isHtml && ( {effectiveEmailContent.isHtml && (
<button <button
onClick={() => { setEmailViewDarkOverride(prev => prev === null ? !(resolvedTheme === 'dark') : !prev); setMoreMenuOpen(false); }} 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 ? <Sun className="w-5 h-5" /> : <Moon className="w-5 h-5" />}
{isDark ? 'View in light mode' : 'View in dark mode'} {isDark ? 'View in light mode' : 'View in dark mode'}
@@ -3352,14 +3352,14 @@ export function EmailViewer({
<div className="h-px bg-border my-1" /> <div className="h-px bg-border my-1" />
<button <button
onClick={() => { handleExportEmail(); setMoreMenuOpen(false); }} 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" /> <Download className="w-5 h-5" />
{t('export_email')} {t('export_email')}
</button> </button>
<button <button
onClick={() => { handleImportEmail(); setMoreMenuOpen(false); }} 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" /> <Upload className="w-5 h-5" />
{t('import_email')} {t('import_email')}
@@ -3367,7 +3367,7 @@ export function EmailViewer({
{onShowShortcuts && ( {onShowShortcuts && (
<button <button
onClick={() => { onShowShortcuts(); setMoreMenuOpen(false); }} 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" /> <Keyboard className="w-5 h-5" />
{t('keyboard_shortcuts')} {t('keyboard_shortcuts')}
@@ -3385,7 +3385,7 @@ export function EmailViewer({
{isTarget ? ( {isTarget ? (
<button <button
onClick={() => { onMoveToMailbox(node.id); setMoreMenuOpen(false); setMoreMenuSub(null); }} 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` }} style={{ paddingLeft: `${1 + depth * 1}rem` }}
> >
<Icon className="w-5 h-5 flex-shrink-0" /> <Icon className="w-5 h-5 flex-shrink-0" />
@@ -3416,20 +3416,20 @@ export function EmailViewer({
key={option.value} key={option.value}
onClick={() => { if (email) onSetColorTag?.(email.id, option.value); setMoreMenuOpen(false); setMoreMenuSub(null); }} onClick={() => { if (email) onSetColorTag?.(email.id, option.value); setMoreMenuOpen(false); setMoreMenuSub(null); }}
className={cn( 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" isActive && "bg-accent font-medium"
)} )}
> >
<span className={cn("w-3.5 h-3.5 rounded-full flex-shrink-0", option.color)} /> <span className={cn("w-3.5 h-3.5 rounded-full flex-shrink-0", option.color)} />
<span className="truncate">{option.name}</span> <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> </button>
); );
})} })}
{currentColors.length > 0 && ( {currentColors.length > 0 && (
<button <button
onClick={() => { if (email) onSetColorTag?.(email.id, null); setMoreMenuOpen(false); setMoreMenuSub(null); }} 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" /> <X className="w-4 h-4 flex-shrink-0" />
<span>{t('remove_color')}</span> <span>{t('remove_color')}</span>
@@ -3469,7 +3469,7 @@ export function EmailViewer({
variant="ghost" variant="ghost"
size="icon" size="icon"
onClick={onBack} 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')} aria-label={t('back_to_list')}
> >
<ChevronLeft className="w-5 h-5" /> <ChevronLeft className="w-5 h-5" />
@@ -3513,7 +3513,7 @@ export function EmailViewer({
</div> </div>
</div> </div>
{/* Date/time on the right of subject row - hidden on mobile, shown next to sender */} {/* 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"> <span className="text-xs lg:text-sm text-muted-foreground whitespace-nowrap">
{formatDateTime(email.receivedAt, timeFormat, { weekday: 'short', year: 'numeric', month: 'short', day: 'numeric' })} {formatDateTime(email.receivedAt, timeFormat, { weekday: 'short', year: 'numeric', month: 'short', day: 'numeric' })}
</span> </span>
@@ -3569,7 +3569,7 @@ export function EmailViewer({
name={sender?.name} name={sender?.name}
email={sender.email} email={sender.email}
onViewContact={handleViewContactSidebar} onViewContact={handleViewContactSidebar}
className="font-semibold text-left" className="font-semibold text-start"
/> />
) : ( ) : (
<span className="font-semibold text-foreground">{t('unknown_sender')}</span> <span className="font-semibold text-foreground">{t('unknown_sender')}</span>
@@ -3633,7 +3633,7 @@ export function EmailViewer({
)} )}
<button <button
onClick={() => setShowFullHeaders(!showFullHeaders)} 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 ? ( {showFullHeaders ? (
<> <>
@@ -3756,7 +3756,7 @@ export function EmailViewer({
<span className="text-xs text-foreground truncate max-w-[180px]"> <span className="text-xs text-foreground truncate max-w-[180px]">
{getAttachmentDisplayName(attachment.name, attachment.type)} {getAttachmentDisplayName(attachment.name, attachment.type)}
</span> </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)} {formatFileSize(attachment.size)}
</span> </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"> <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} name={sender?.name}
email={sender.email} email={sender.email}
onViewContact={handleViewContactSidebar} 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> <span className="text-sm font-semibold text-foreground">{t('unknown_sender')}</span>
@@ -3857,7 +3857,7 @@ export function EmailViewer({
)} )}
<button <button
onClick={() => setShowFullHeaders(!showFullHeaders)} 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 ? ( {showFullHeaders ? (
<> <>
@@ -3874,7 +3874,7 @@ export function EmailViewer({
</div> </div>
</div> </div>
{/* Date/time + size on the right (mobile) */} {/* 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"> <span className="text-xs text-muted-foreground whitespace-nowrap">
{formatDateTime(email.receivedAt, timeFormat, { weekday: 'short', year: 'numeric', month: 'short', day: 'numeric' })} {formatDateTime(email.receivedAt, timeFormat, { weekday: 'short', year: 'numeric', month: 'short', day: 'numeric' })}
</span> </span>
@@ -4006,7 +4006,7 @@ export function EmailViewer({
email={sender?.email || ''} email={sender?.email || ''}
displayLabel={sender?.name && sender?.email ? `${sender.name} <${sender.email}>` : undefined} displayLabel={sender?.name && sender?.email ? `${sender.name} <${sender.email}>` : undefined}
onViewContact={handleViewContactSidebar} onViewContact={handleViewContactSidebar}
className="text-sm text-left" className="text-sm text-start"
/> />
</div> </div>
</Row> </Row>
@@ -4534,7 +4534,7 @@ export function EmailViewer({
<span className="text-sm text-foreground truncate max-w-[220px]"> <span className="text-sm text-foreground truncate max-w-[220px]">
{getAttachmentDisplayName(attachment.name, attachment.type)} {getAttachmentDisplayName(attachment.name, attachment.type)}
</span> </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)} {formatFileSize(attachment.size)}
</span> </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"> <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]"> <span className="text-xs text-foreground truncate max-w-[180px]">
{getAttachmentDisplayName(attachment.name, attachment.type)} {getAttachmentDisplayName(attachment.name, attachment.type)}
</span> </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)} {formatFileSize(attachment.size)}
</span> </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"> <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} disabled={isSendingQuickReply}
className="text-muted-foreground" className="text-muted-foreground"
> >
<MoreVertical className="w-4 h-4 mr-1" /> <MoreVertical className="w-4 h-4 me-1" />
{t('more_options')} {t('more_options')}
</Button> </Button>
<Button <Button
@@ -4825,12 +4825,12 @@ export function EmailViewer({
> >
{isSendingQuickReply ? ( {isSendingQuickReply ? (
<> <>
<Loader2 className="w-4 h-4 mr-1 animate-spin" /> <Loader2 className="w-4 h-4 me-1 animate-spin" />
{t('sending')} {t('sending')}
</> </>
) : ( ) : (
<> <>
<Reply className="w-4 h-4 mr-1" /> <Reply className="w-4 h-4 me-1" />
{t('send')} {t('send')}
</> </>
)} )}
@@ -4966,7 +4966,7 @@ export function EmailViewer({
<> <>
{/* Collapse toggle when sidebar is collapsed */} {/* Collapse toggle when sidebar is collapsed */}
{detailSidebarCollapsed && ( {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 <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
@@ -4990,7 +4990,7 @@ export function EmailViewer({
onDoubleClick={() => setDetailSidebarWidth(280)} onDoubleClick={() => setDetailSidebarWidth(280)}
/> />
<div <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 }} style={{ width: detailSidebarWidth, minWidth: detailSidebarWidth }}
> >
<div className="flex items-center justify-end px-1 py-1 border-b border-border shrink-0"> <div className="flex items-center justify-end px-1 py-1 border-b border-border shrink-0">
+1 -1
View File
@@ -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" /> <MailCheck className="w-4 h-4 shrink-0 text-amber-600 dark:text-amber-400" />
<span className="text-foreground">{t('prompt')}</span> <span className="text-foreground">{t('prompt')}</span>
<span className="break-all text-muted-foreground">{requestedBy}</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 <button
onClick={async () => { onClick={async () => {
setState('sending'); setState('sending');
+1 -1
View File
@@ -221,7 +221,7 @@ export function RecipientPopover({ name, email, displayLabel, onViewContact, cla
{onViewContact && ( {onViewContact && (
<button <button
onClick={handleViewContact} 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"} title={contact ? "View contact" : "View details"}
> >
{contact ? <ExternalLink className="w-3.5 h-3.5" /> : <UserPlus className="w-3.5 h-3.5" />} {contact ? <ExternalLink className="w-3.5 h-3.5" /> : <UserPlus className="w-3.5 h-3.5" />}
+29 -8
View File
@@ -8,6 +8,7 @@ import Heading from "@tiptap/extension-heading";
import Underline from "@tiptap/extension-underline"; import Underline from "@tiptap/extension-underline";
import Link from "@tiptap/extension-link"; import Link from "@tiptap/extension-link";
import TextAlign from "@tiptap/extension-text-align"; import TextAlign from "@tiptap/extension-text-align";
import { TextDirection } from "@/components/email/text-direction";
import { TextStyle } from "@tiptap/extension-text-style"; import { TextStyle } from "@tiptap/extension-text-style";
import Color from "@tiptap/extension-color"; import Color from "@tiptap/extension-color";
import { ResizableImage } from "@/components/email/resizable-image"; 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 { QuotedHtml, serializeEditorContent } from "@/components/email/quoted-html";
import { SignatureBlock } from "@/components/email/signature-block"; import { SignatureBlock } from "@/components/email/signature-block";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { useSettingsStore } from "@/stores/settings-store";
import { import {
Bold, Bold,
Italic, Italic,
@@ -29,6 +31,7 @@ import {
AlignLeft, AlignLeft,
AlignCenter, AlignCenter,
AlignRight, AlignRight,
ArrowLeftRight,
Link as LinkIcon, Link as LinkIcon,
Undo, Undo,
Redo, Redo,
@@ -183,6 +186,7 @@ export function RichTextEditor({
hasError, hasError,
onEditorReady, onEditorReady,
}: RichTextEditorProps) { }: RichTextEditorProps) {
const rtlEditingSupport = useSettingsStore((st) => st.rtlEditingSupport);
const onImageUploadRef = React.useRef(onImageUpload); const onImageUploadRef = React.useRef(onImageUpload);
onImageUploadRef.current = onImageUpload; onImageUploadRef.current = onImageUpload;
const onEditorReadyRef = React.useRef(onEditorReady); const onEditorReadyRef = React.useRef(onEditorReady);
@@ -240,6 +244,7 @@ export function RichTextEditor({
// rich/branded signatures keep their inline styling in the editor and // rich/branded signatures keep their inline styling in the editor and
// in the sent mail (see signature-block.ts). // in the sent mail (see signature-block.ts).
SignatureBlock, SignatureBlock,
TextDirection,
], ],
content, content,
editorProps: { editorProps: {
@@ -454,6 +459,22 @@ export function RichTextEditor({
<AlignRight className="w-4 h-4" /> <AlignRight className="w-4 h-4" />
</ToolbarButton> </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 /> <ToolbarSeparator />
<ToolbarButton <ToolbarButton
@@ -478,28 +499,28 @@ export function RichTextEditor({
<div className="flex flex-col gap-0.5"> <div className="flex flex-col gap-0.5">
<button <button
type="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); }} onClick={() => { editor.chain().focus().addRowBefore().run(); setTableMenuOpen(false); }}
> >
<Rows3 className="w-4 h-4" /> Add row above <Rows3 className="w-4 h-4" /> Add row above
</button> </button>
<button <button
type="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); }} onClick={() => { editor.chain().focus().addRowAfter().run(); setTableMenuOpen(false); }}
> >
<Rows3 className="w-4 h-4" /> Add row below <Rows3 className="w-4 h-4" /> Add row below
</button> </button>
<button <button
type="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); }} onClick={() => { editor.chain().focus().addColumnBefore().run(); setTableMenuOpen(false); }}
> >
<Columns3 className="w-4 h-4" /> Add column before <Columns3 className="w-4 h-4" /> Add column before
</button> </button>
<button <button
type="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); }} onClick={() => { editor.chain().focus().addColumnAfter().run(); setTableMenuOpen(false); }}
> >
<Columns3 className="w-4 h-4" /> Add column after <Columns3 className="w-4 h-4" /> Add column after
@@ -507,21 +528,21 @@ export function RichTextEditor({
<div className="h-px bg-border my-1" /> <div className="h-px bg-border my-1" />
<button <button
type="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); }} onClick={() => { editor.chain().focus().deleteRow().run(); setTableMenuOpen(false); }}
> >
<Trash2 className="w-4 h-4" /> Delete row <Trash2 className="w-4 h-4" /> Delete row
</button> </button>
<button <button
type="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); }} onClick={() => { editor.chain().focus().deleteColumn().run(); setTableMenuOpen(false); }}
> >
<Trash2 className="w-4 h-4" /> Delete column <Trash2 className="w-4 h-4" /> Delete column
</button> </button>
<button <button
type="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); }} onClick={() => { editor.chain().focus().toggleHeaderRow().run(); setTableMenuOpen(false); }}
> >
<Rows3 className="w-4 h-4" /> Toggle header row <Rows3 className="w-4 h-4" /> Toggle header row
@@ -529,7 +550,7 @@ export function RichTextEditor({
<div className="h-px bg-border my-1" /> <div className="h-px bg-border my-1" />
<button <button
type="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); }} onClick={() => { editor.chain().focus().deleteTable().run(); setTableMenuOpen(false); }}
> >
<Trash2 className="w-4 h-4" /> Delete table <Trash2 className="w-4 h-4" /> Delete table
+58
View File
@@ -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)' }}> <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 <button
onClick={onBack} 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" /> <ArrowLeft className="w-5 h-5" />
</button> </button>
@@ -488,13 +488,13 @@ function EmailCard({
<div className={cn( <div className={cn(
"rounded-lg border border-border overflow-hidden transition-all duration-200", "rounded-lg border border-border overflow-hidden transition-all duration-200",
isExpanded ? "bg-background shadow-sm" : "bg-muted/30", 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 */} {/* Card Header - Always visible */}
<button <button
onClick={onToggleExpanded} onClick={onToggleExpanded}
className={cn( 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" !isExpanded && "hover:bg-muted/50"
)} )}
style={{ gap: 'var(--density-item-gap)', padding: 'var(--density-card-p)' }} style={{ gap: 'var(--density-item-gap)', padding: 'var(--density-card-p)' }}
@@ -657,7 +657,7 @@ function EmailCard({
}} }}
className="flex-1" className="flex-1"
> >
<Reply className="w-4 h-4 mr-2" /> <Reply className="w-4 h-4 me-2" />
{t("email_viewer.reply")} {t("email_viewer.reply")}
</Button> </Button>
)} )}
@@ -671,7 +671,7 @@ function EmailCard({
}} }}
className="flex-1" className="flex-1"
> >
<ReplyAll className="w-4 h-4 mr-2" /> <ReplyAll className="w-4 h-4 me-2" />
{t("email_viewer.reply_all")} {t("email_viewer.reply_all")}
</Button> </Button>
)} )}
@@ -685,7 +685,7 @@ function EmailCard({
}} }}
className="flex-1" className="flex-1"
> >
<Forward className="w-4 h-4 mr-2" /> <Forward className="w-4 h-4 me-2" />
{t("email_viewer.forward")} {t("email_viewer.forward")}
</Button> </Button>
)} )}
+3 -3
View File
@@ -86,8 +86,8 @@ export function ThreadEmailItem({
{...longPressHandlers} {...longPressHandlers}
className={cn( className={cn(
"relative cursor-pointer select-none transition-all duration-150", "relative cursor-pointer select-none transition-all duration-150",
"pl-12 pr-4", "ps-12 pe-4",
"border-l-2 border-l-transparent", "border-s-2 border-l-transparent",
selected selected
? "bg-selection border-l-primary" ? "bg-selection border-l-primary"
: "hover:bg-muted/50", : "hover:bg-muted/50",
@@ -130,7 +130,7 @@ export function ThreadEmailItem({
{/* Unread indicator */} {/* Unread indicator */}
{isUnread && ( {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" /> <Circle className="w-1.5 h-1.5 fill-unread text-unread" />
</div> </div>
)} )}
+3 -3
View File
@@ -218,7 +218,7 @@ const SingleEmailItem = React.forwardRef<HTMLDivElement, SingleEmailItemProps>(
)} )}
{isUnread && ( {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" /> <Circle className="w-2 h-2 fill-unread text-unread" />
</div> </div>
)} )}
@@ -643,7 +643,7 @@ export const ThreadListItem = React.forwardRef<HTMLDivElement, ThreadListItemPro
)} )}
{hasUnread && ( {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" /> <Circle className="w-2 h-2 fill-unread text-unread" />
</div> </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"> <div className="bg-muted/20 animate-in slide-in-from-top-2 duration-200">
{isLoading ? ( {isLoading ? (
<div className="py-4 flex items-center justify-center text-sm text-muted-foreground"> <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')} {t('loading')}
</div> </div>
) : ( ) : (
+2 -2
View File
@@ -96,7 +96,7 @@ export function UnsubscribeBanner({
if (success) { if (success) {
return ( 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" /> <CheckCircle className="w-3 h-3 text-green-600 dark:text-green-400" />
<span className="text-xs text-green-600 dark:text-green-400"> <span className="text-xs text-green-600 dark:text-green-400">
{t(unsubMethod === 'http' {t(unsubMethod === 'http'
@@ -110,7 +110,7 @@ export function UnsubscribeBanner({
if (error) { if (error) {
return ( 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" /> <AlertCircle className="w-3 h-3 text-red-500 dark:text-red-400" />
<button <button
onClick={onDismiss} onClick={onDismiss}
+6 -6
View File
@@ -21,7 +21,7 @@ export function PageErrorFallback({ error: _error, resetError, t }: FallbackProp
{t("page_error_description")} {t("page_error_description")}
</p> </p>
<Button onClick={resetError}> <Button onClick={resetError}>
<RefreshCw className="w-4 h-4 mr-2" /> <RefreshCw className="w-4 h-4 me-2" />
{t("try_again")} {t("try_again")}
</Button> </Button>
</div> </div>
@@ -34,13 +34,13 @@ export function PageErrorFallback({ error: _error, resetError, t }: FallbackProp
*/ */
export function SidebarErrorFallback({ resetError, t }: FallbackProps) { export function SidebarErrorFallback({ resetError, t }: FallbackProps) {
return ( return (
<div className="w-64 h-full border-r border-border bg-secondary flex flex-col items-center justify-center p-4"> <div className="w-64 h-full border-e border-border bg-secondary flex flex-col items-center justify-center p-4">
<FolderOpen className="w-10 h-10 text-muted-foreground mb-3" /> <FolderOpen className="w-10 h-10 text-muted-foreground mb-3" />
<p className="text-sm text-muted-foreground text-center mb-4"> <p className="text-sm text-muted-foreground text-center mb-4">
{t("sidebar_error")} {t("sidebar_error")}
</p> </p>
<Button variant="outline" size="sm" onClick={resetError}> <Button variant="outline" size="sm" onClick={resetError}>
<RefreshCw className="w-3 h-3 mr-1" /> <RefreshCw className="w-3 h-3 me-1" />
{t("reload")} {t("reload")}
</Button> </Button>
</div> </div>
@@ -58,7 +58,7 @@ export function EmailListErrorFallback({ resetError, t }: FallbackProps) {
{t("email_list_error")} {t("email_list_error")}
</p> </p>
<Button variant="outline" size="sm" onClick={resetError}> <Button variant="outline" size="sm" onClick={resetError}>
<RefreshCw className="w-4 h-4 mr-2" /> <RefreshCw className="w-4 h-4 me-2" />
{t("reload_emails")} {t("reload_emails")}
</Button> </Button>
</div> </div>
@@ -81,7 +81,7 @@ export function EmailViewerErrorFallback({ resetError, t }: FallbackProps) {
{t("viewer_error_description")} {t("viewer_error_description")}
</p> </p>
<Button onClick={resetError}> <Button onClick={resetError}>
<RefreshCw className="w-4 h-4 mr-2" /> <RefreshCw className="w-4 h-4 me-2" />
{t("try_again")} {t("try_again")}
</Button> </Button>
</div> </div>
@@ -119,7 +119,7 @@ export function SettingsErrorFallback({ resetError, t }: FallbackProps) {
{t("settings_error_description")} {t("settings_error_description")}
</p> </p>
<Button onClick={resetError}> <Button onClick={resetError}>
<RefreshCw className="w-4 h-4 mr-2" /> <RefreshCw className="w-4 h-4 me-2" />
{t("reload_settings")} {t("reload_settings")}
</Button> </Button>
</div> </div>
+37 -37
View File
@@ -821,8 +821,8 @@ export function FileBrowser({
const SortIndicator = ({ column }: { column: SortKey }) => { const SortIndicator = ({ column }: { column: SortKey }) => {
if (sortKey !== column) return null; if (sortKey !== column) return null;
return sortDir === "asc" return sortDir === "asc"
? <ArrowUp className="w-3 h-3 inline ml-1" /> ? <ArrowUp className="w-3 h-3 inline ms-1" />
: <ArrowDown className="w-3 h-3 inline ml-1" />; : <ArrowDown className="w-3 h-3 inline ms-1" />;
}; };
// Keyboard shortcuts // Keyboard shortcuts
@@ -942,7 +942,7 @@ export function FileBrowser({
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
className="h-8 w-8 -ml-2" className="h-8 w-8 -ms-2"
onClick={() => setNarrowSidebarOpen((v) => !v)} onClick={() => setNarrowSidebarOpen((v) => !v)}
aria-label={t("open_folder_tree")} aria-label={t("open_folder_tree")}
> >
@@ -982,7 +982,7 @@ export function FileBrowser({
className="h-8" className="h-8"
onClick={() => onBatchDownload([...selectedResources].filter(n => !resources.find(r => r.name === n)?.isDirectory))} onClick={() => onBatchDownload([...selectedResources].filter(n => !resources.find(r => r.name === n)?.isDirectory))}
> >
<Download className="w-4 h-4 mr-1" /> <Download className="w-4 h-4 me-1" />
{t("download")} ({[...selectedResources].filter(n => !resources.find(r => r.name === n)?.isDirectory).length}) {t("download")} ({[...selectedResources].filter(n => !resources.find(r => r.name === n)?.isDirectory).length})
</Button> </Button>
<Button <Button
@@ -991,7 +991,7 @@ export function FileBrowser({
className="h-8 text-destructive hover:text-destructive" className="h-8 text-destructive hover:text-destructive"
onClick={() => onBatchDelete([...selectedResources])} onClick={() => onBatchDelete([...selectedResources])}
> >
<Trash2 className="w-4 h-4 mr-1" /> <Trash2 className="w-4 h-4 me-1" />
{t("delete")} ({selectedResources.size}) {t("delete")} ({selectedResources.size})
</Button> </Button>
</> </>
@@ -1003,7 +1003,7 @@ export function FileBrowser({
className="h-8" className="h-8"
onClick={onPaste} onClick={onPaste}
> >
<Clipboard className="w-4 h-4 mr-1" /> <Clipboard className="w-4 h-4 me-1" />
{t("paste")} ({clipboard.names.length}) {t("paste")} ({clipboard.names.length})
</Button> </Button>
)} )}
@@ -1157,7 +1157,7 @@ export function FileBrowser({
className="h-7 text-destructive hover:text-destructive shrink-0" className="h-7 text-destructive hover:text-destructive shrink-0"
onClick={onRefresh} onClick={onRefresh}
> >
<RefreshCw className="w-3.5 h-3.5 mr-1" /> <RefreshCw className="w-3.5 h-3.5 me-1" />
{t("retry")} {t("retry")}
</Button> </Button>
</div> </div>
@@ -1181,12 +1181,12 @@ export function FileBrowser({
<span className="truncate"> <span className="truncate">
{t("uploading")} {uploadProgress.name} {t("uploading")} {uploadProgress.name}
{uploadProgress.totalFiles > 1 && ( {uploadProgress.totalFiles > 1 && (
<span className="text-muted-foreground ml-1"> <span className="text-muted-foreground ms-1">
({uploadProgress.current}/{uploadProgress.totalFiles}) ({uploadProgress.current}/{uploadProgress.totalFiles})
</span> </span>
)} )}
</span> </span>
<span className="ml-auto tabular-nums shrink-0 flex items-center gap-2"> <span className="ms-auto tabular-nums shrink-0 flex items-center gap-2">
{uploadProgress.total > 0 {uploadProgress.total > 0
? `${Math.round((uploadProgress.loaded / uploadProgress.total) * 100)}%` ? `${Math.round((uploadProgress.loaded / uploadProgress.total) * 100)}%`
: "…"} : "…"}
@@ -1267,7 +1267,7 @@ export function FileBrowser({
)} )}
{/* Favorites & Recent sidebar (when layout is inline) */} {/* Favorites & Recent sidebar (when layout is inline) */}
{folderLayout === "inline" && (favorites.length > 0 || recentFiles.length > 0) && ( {folderLayout === "inline" && (favorites.length > 0 || recentFiles.length > 0) && (
<div className="w-48 border-r border-border bg-background overflow-y-auto shrink-0 hidden lg:block"> <div className="w-48 border-e border-border bg-background overflow-y-auto shrink-0 hidden lg:block">
{favorites.length > 0 && ( {favorites.length > 0 && (
<div className="p-3"> <div className="p-3">
<h4 className="text-xs font-medium text-muted-foreground uppercase tracking-wider mb-2 flex items-center gap-1"> <h4 className="text-xs font-medium text-muted-foreground uppercase tracking-wider mb-2 flex items-center gap-1">
@@ -1280,7 +1280,7 @@ export function FileBrowser({
key={fav} key={fav}
onClick={() => onNavigate(fav)} onClick={() => onNavigate(fav)}
className={cn( className={cn(
"w-full flex items-center gap-2 px-2 py-1.5 rounded text-sm hover:bg-muted transition-colors text-left", "w-full flex items-center gap-2 px-2 py-1.5 rounded text-sm hover:bg-muted transition-colors text-start",
currentPath === fav && "bg-muted font-medium" currentPath === fav && "bg-muted font-medium"
)} )}
> >
@@ -1301,7 +1301,7 @@ export function FileBrowser({
{recentFiles.slice(0, 10).map((recent) => ( {recentFiles.slice(0, 10).map((recent) => (
<button <button
key={recent.id} key={recent.id}
className="w-full flex items-center gap-2 px-2 py-1.5 rounded text-sm hover:bg-muted transition-colors text-left" className="w-full flex items-center gap-2 px-2 py-1.5 rounded text-sm hover:bg-muted transition-colors text-start"
title={recent.name} title={recent.name}
> >
<File className="w-3.5 h-3.5 text-muted-foreground shrink-0" /> <File className="w-3.5 h-3.5 text-muted-foreground shrink-0" />
@@ -1322,14 +1322,14 @@ export function FileBrowser({
<table className="w-full text-sm"> <table className="w-full text-sm">
<thead className="bg-muted/50 sticky top-0 z-10"> <thead className="bg-muted/50 sticky top-0 z-10">
<tr className="border-b border-border"> <tr className="border-b border-border">
<th className="text-left px-4 py-2 font-medium text-muted-foreground"> <th className="text-start px-4 py-2 font-medium text-muted-foreground">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-4 h-4" /> <div className="w-4 h-4" />
{t("name")} {t("name")}
</div> </div>
</th> </th>
<th className="text-left px-4 py-2 font-medium text-muted-foreground hidden md:table-cell w-24">{t("size")}</th> <th className="text-start px-4 py-2 font-medium text-muted-foreground hidden md:table-cell w-24">{t("size")}</th>
<th className="text-left px-4 py-2 font-medium text-muted-foreground hidden lg:table-cell w-44">{t("modified")}</th> <th className="text-start px-4 py-2 font-medium text-muted-foreground hidden lg:table-cell w-44">{t("modified")}</th>
<th className="w-10 px-2 py-2" /> <th className="w-10 px-2 py-2" />
</tr> </tr>
</thead> </thead>
@@ -1354,7 +1354,7 @@ export function FileBrowser({
key={`__account__:${acc.accountId}`} key={`__account__:${acc.accountId}`}
onClick={() => onSelectAccount(acc.accountId)} onClick={() => onSelectAccount(acc.accountId)}
title={acc.email} title={acc.email}
className="flex items-center gap-3 p-3 rounded-lg border border-border hover:bg-muted/50 transition-colors text-left min-w-0" className="flex items-center gap-3 p-3 rounded-lg border border-border hover:bg-muted/50 transition-colors text-start min-w-0"
> >
<Avatar <Avatar
name={acc.label} name={acc.label}
@@ -1514,7 +1514,7 @@ export function FileBrowser({
> >
<thead className="bg-muted/50 sticky top-0 z-10"> <thead className="bg-muted/50 sticky top-0 z-10">
<tr className="border-b border-border"> <tr className="border-b border-border">
<th className="text-left px-4 py-2 font-medium text-muted-foreground"> <th className="text-start px-4 py-2 font-medium text-muted-foreground">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<input <input
type="checkbox" type="checkbox"
@@ -1530,13 +1530,13 @@ export function FileBrowser({
</button> </button>
</div> </div>
</th> </th>
<th className="text-left px-4 py-2 font-medium text-muted-foreground hidden md:table-cell w-24"> <th className="text-start px-4 py-2 font-medium text-muted-foreground hidden md:table-cell w-24">
<button onClick={() => handleSortClick("size")} className="hover:text-foreground transition-colors"> <button onClick={() => handleSortClick("size")} className="hover:text-foreground transition-colors">
{t("size")} {t("size")}
<SortIndicator column="size" /> <SortIndicator column="size" />
</button> </button>
</th> </th>
<th className="text-left px-4 py-2 font-medium text-muted-foreground hidden lg:table-cell w-44"> <th className="text-start px-4 py-2 font-medium text-muted-foreground hidden lg:table-cell w-44">
<button onClick={() => handleSortClick("modified")} className="hover:text-foreground transition-colors"> <button onClick={() => handleSortClick("modified")} className="hover:text-foreground transition-colors">
{t("modified")} {t("modified")}
<SortIndicator column="modified" /> <SortIndicator column="modified" />
@@ -1681,7 +1681,7 @@ export function FileBrowser({
> >
{!resources.find(r => r.name === contextMenu.name)?.isDirectory && isPreviewable(contextMenu.name) && ( {!resources.find(r => r.name === contextMenu.name)?.isDirectory && isPreviewable(contextMenu.name) && (
<button <button
className="w-full flex items-center gap-2 px-3 py-2 text-sm hover:bg-muted transition-colors text-left" className="w-full flex items-center gap-2 px-3 py-2 text-sm hover:bg-muted transition-colors text-start"
onClick={() => { onClick={() => {
if (isImageFile(contextMenu.name)) { if (isImageFile(contextMenu.name)) {
onPreviewImage(contextMenu.name); onPreviewImage(contextMenu.name);
@@ -1697,7 +1697,7 @@ export function FileBrowser({
)} )}
{!resources.find(r => r.name === contextMenu.name)?.isDirectory && ( {!resources.find(r => r.name === contextMenu.name)?.isDirectory && (
<button <button
className="w-full flex items-center gap-2 px-3 py-2 text-sm hover:bg-muted transition-colors text-left" className="w-full flex items-center gap-2 px-3 py-2 text-sm hover:bg-muted transition-colors text-start"
onClick={() => { onClick={() => {
onDownload(contextMenu.name); onDownload(contextMenu.name);
setContextMenu(null); setContextMenu(null);
@@ -1708,7 +1708,7 @@ export function FileBrowser({
</button> </button>
)} )}
<button <button
className="w-full flex items-center gap-2 px-3 py-2 text-sm hover:bg-muted transition-colors text-left" className="w-full flex items-center gap-2 px-3 py-2 text-sm hover:bg-muted transition-colors text-start"
onClick={() => { onClick={() => {
onCut([contextMenu.name]); onCut([contextMenu.name]);
setContextMenu(null); setContextMenu(null);
@@ -1718,7 +1718,7 @@ export function FileBrowser({
{t("cut")} {t("cut")}
</button> </button>
<button <button
className="w-full flex items-center gap-2 px-3 py-2 text-sm hover:bg-muted transition-colors text-left" className="w-full flex items-center gap-2 px-3 py-2 text-sm hover:bg-muted transition-colors text-start"
onClick={() => { onClick={() => {
onCopy([contextMenu.name]); onCopy([contextMenu.name]);
setContextMenu(null); setContextMenu(null);
@@ -1729,7 +1729,7 @@ export function FileBrowser({
</button> </button>
{clipboard && ( {clipboard && (
<button <button
className="w-full flex items-center gap-2 px-3 py-2 text-sm hover:bg-muted transition-colors text-left" className="w-full flex items-center gap-2 px-3 py-2 text-sm hover:bg-muted transition-colors text-start"
onClick={() => { onClick={() => {
onPaste(); onPaste();
setContextMenu(null); setContextMenu(null);
@@ -1741,7 +1741,7 @@ export function FileBrowser({
)} )}
{!resources.find(r => r.name === contextMenu.name)?.isDirectory && ( {!resources.find(r => r.name === contextMenu.name)?.isDirectory && (
<button <button
className="w-full flex items-center gap-2 px-3 py-2 text-sm hover:bg-muted transition-colors text-left" className="w-full flex items-center gap-2 px-3 py-2 text-sm hover:bg-muted transition-colors text-start"
onClick={() => { onClick={() => {
onDuplicate(contextMenu.name); onDuplicate(contextMenu.name);
setContextMenu(null); setContextMenu(null);
@@ -1753,7 +1753,7 @@ export function FileBrowser({
)} )}
{canShare(resources.find(r => r.name === contextMenu.name)) && ( {canShare(resources.find(r => r.name === contextMenu.name)) && (
<button <button
className="w-full flex items-center gap-2 px-3 py-2 text-sm hover:bg-muted transition-colors text-left" className="w-full flex items-center gap-2 px-3 py-2 text-sm hover:bg-muted transition-colors text-start"
onClick={() => { onClick={() => {
const r = resources.find(res => res.name === contextMenu.name); const r = resources.find(res => res.name === contextMenu.name);
if (r) setShareTargetId(r.id); if (r) setShareTargetId(r.id);
@@ -1766,7 +1766,7 @@ export function FileBrowser({
)} )}
<div className="h-px bg-border my-1" /> <div className="h-px bg-border my-1" />
<button <button
className="w-full flex items-center gap-2 px-3 py-2 text-sm hover:bg-muted transition-colors text-left" className="w-full flex items-center gap-2 px-3 py-2 text-sm hover:bg-muted transition-colors text-start"
onClick={() => { onClick={() => {
onShowDetails(contextMenu.name); onShowDetails(contextMenu.name);
setContextMenu(null); setContextMenu(null);
@@ -1776,7 +1776,7 @@ export function FileBrowser({
{t("details")} {t("details")}
</button> </button>
<button <button
className="w-full flex items-center gap-2 px-3 py-2 text-sm hover:bg-muted transition-colors text-left" className="w-full flex items-center gap-2 px-3 py-2 text-sm hover:bg-muted transition-colors text-start"
onClick={() => { onClick={() => {
setRenameTarget(contextMenu.name); setRenameTarget(contextMenu.name);
setContextMenu(null); setContextMenu(null);
@@ -1786,7 +1786,7 @@ export function FileBrowser({
{t("rename")} {t("rename")}
</button> </button>
<button <button
className="w-full flex items-center gap-2 px-3 py-2 text-sm hover:bg-muted text-destructive transition-colors text-left" className="w-full flex items-center gap-2 px-3 py-2 text-sm hover:bg-muted text-destructive transition-colors text-start"
onClick={() => { onClick={() => {
onDelete(contextMenu.name); onDelete(contextMenu.name);
setContextMenu(null); setContextMenu(null);
@@ -1808,7 +1808,7 @@ export function FileBrowser({
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
> >
<button <button
className="w-full flex items-center gap-2 px-3 py-2 text-sm hover:bg-muted transition-colors text-left" className="w-full flex items-center gap-2 px-3 py-2 text-sm hover:bg-muted transition-colors text-start"
onClick={() => { onClick={() => {
setShowNewFolder(true); setShowNewFolder(true);
setEmptyContextMenu(null); setEmptyContextMenu(null);
@@ -1818,7 +1818,7 @@ export function FileBrowser({
{t("new_folder")} {t("new_folder")}
</button> </button>
<button <button
className="w-full flex items-center gap-2 px-3 py-2 text-sm hover:bg-muted transition-colors text-left" className="w-full flex items-center gap-2 px-3 py-2 text-sm hover:bg-muted transition-colors text-start"
onClick={() => { onClick={() => {
setShowNewTextFile(true); setShowNewTextFile(true);
setEmptyContextMenu(null); setEmptyContextMenu(null);
@@ -1828,7 +1828,7 @@ export function FileBrowser({
{t("new_text_file")} {t("new_text_file")}
</button> </button>
<button <button
className="w-full flex items-center gap-2 px-3 py-2 text-sm hover:bg-muted transition-colors text-left" className="w-full flex items-center gap-2 px-3 py-2 text-sm hover:bg-muted transition-colors text-start"
onClick={() => { onClick={() => {
fileInputRef.current?.click(); fileInputRef.current?.click();
setEmptyContextMenu(null); setEmptyContextMenu(null);
@@ -1838,7 +1838,7 @@ export function FileBrowser({
{t("upload")} {t("upload")}
</button> </button>
<button <button
className="w-full flex items-center gap-2 px-3 py-2 text-sm hover:bg-muted transition-colors text-left" className="w-full flex items-center gap-2 px-3 py-2 text-sm hover:bg-muted transition-colors text-start"
onClick={() => { onClick={() => {
folderInputRef.current?.click(); folderInputRef.current?.click();
setEmptyContextMenu(null); setEmptyContextMenu(null);
@@ -1851,7 +1851,7 @@ export function FileBrowser({
<> <>
<div className="h-px bg-border my-1" /> <div className="h-px bg-border my-1" />
<button <button
className="w-full flex items-center gap-2 px-3 py-2 text-sm hover:bg-muted transition-colors text-left" className="w-full flex items-center gap-2 px-3 py-2 text-sm hover:bg-muted transition-colors text-start"
onClick={() => { onClick={() => {
onPaste(); onPaste();
setEmptyContextMenu(null); setEmptyContextMenu(null);
@@ -1864,7 +1864,7 @@ export function FileBrowser({
)} )}
<div className="h-px bg-border my-1" /> <div className="h-px bg-border my-1" />
<button <button
className="w-full flex items-center gap-2 px-3 py-2 text-sm hover:bg-muted transition-colors text-left" className="w-full flex items-center gap-2 px-3 py-2 text-sm hover:bg-muted transition-colors text-start"
onClick={() => { onClick={() => {
onRefresh(); onRefresh();
setEmptyContextMenu(null); setEmptyContextMenu(null);
@@ -1895,7 +1895,7 @@ export function FileBrowser({
return ( return (
<button <button
key={folder.id} key={folder.id}
className="w-full flex items-center gap-2 px-3 py-2 text-sm hover:bg-muted transition-colors text-left" className="w-full flex items-center gap-2 px-3 py-2 text-sm hover:bg-muted transition-colors text-start"
onClick={() => { onClick={() => {
onNavigate(folderPath, folder.id); onNavigate(folderPath, folder.id);
setBreadcrumbDropdown(null); setBreadcrumbDropdown(null);
@@ -1926,7 +1926,7 @@ export function FileBrowser({
{/* Details sidebar */} {/* Details sidebar */}
{showDetails && detailResource && ( {showDetails && detailResource && (
<div role="complementary" aria-label={t("details")} className="w-64 border-l border-border bg-background p-4 overflow-y-auto shrink-0 hidden md:block"> <div role="complementary" aria-label={t("details")} className="w-64 border-s border-border bg-background p-4 overflow-y-auto shrink-0 hidden md:block">
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<h3 className="text-sm font-medium">{t("details")}</h3> <h3 className="text-sm font-medium">{t("details")}</h3>
<button onClick={onToggleDetails} className="text-muted-foreground hover:text-foreground"> <button onClick={onToggleDetails} className="text-muted-foreground hover:text-foreground">
+3 -3
View File
@@ -68,11 +68,11 @@ function SimpleMarkdown({ content }: { content: string }) {
} else if (line.startsWith("---") || line.startsWith("***")) { } else if (line.startsWith("---") || line.startsWith("***")) {
elements.push(<hr key={i} className="my-4 border-border" />); elements.push(<hr key={i} className="my-4 border-border" />);
} else if (line.startsWith("- ") || line.startsWith("* ")) { } else if (line.startsWith("- ") || line.startsWith("* ")) {
elements.push(<li key={i} className="ml-4 list-disc">{processInline(line.slice(2))}</li>); elements.push(<li key={i} className="ms-4 list-disc">{processInline(line.slice(2))}</li>);
} else if (/^\d+\. /.test(line)) { } else if (/^\d+\. /.test(line)) {
elements.push(<li key={i} className="ml-4 list-decimal">{processInline(line.replace(/^\d+\. /, ""))}</li>); elements.push(<li key={i} className="ms-4 list-decimal">{processInline(line.replace(/^\d+\. /, ""))}</li>);
} else if (line.startsWith("> ")) { } else if (line.startsWith("> ")) {
elements.push(<blockquote key={i} className="border-l-4 border-border pl-4 italic text-muted-foreground my-2">{processInline(line.slice(2))}</blockquote>); elements.push(<blockquote key={i} className="border-s-4 border-border ps-4 italic text-muted-foreground my-2">{processInline(line.slice(2))}</blockquote>);
} else if (line.startsWith("```")) { } else if (line.startsWith("```")) {
// Code block - collect until closing ``` // Code block - collect until closing ```
const codeLines: string[] = []; const codeLines: string[] = [];
+2 -2
View File
@@ -68,7 +68,7 @@ export function FileUploadArea({ onUpload, onUploadFolder, onCreateFolder, onCre
size="sm" size="sm"
onClick={onCreateFolder} onClick={onCreateFolder}
> >
<FolderPlus className="w-4 h-4 mr-2" /> <FolderPlus className="w-4 h-4 me-2" />
{t("new_folder")} {t("new_folder")}
</Button> </Button>
{onCreateTextFile && ( {onCreateTextFile && (
@@ -77,7 +77,7 @@ export function FileUploadArea({ onUpload, onUploadFolder, onCreateFolder, onCre
size="sm" size="sm"
onClick={onCreateTextFile} onClick={onCreateTextFile}
> >
<FilePlus className="w-4 h-4 mr-2" /> <FilePlus className="w-4 h-4 me-2" />
{t("new_text_file")} {t("new_text_file")}
</Button> </Button>
)} )}
+8 -8
View File
@@ -135,7 +135,7 @@ export function FolderTreeSidebar({ currentPath, onNavigate, listByParentId, wid
return ( return (
<div <div
className={cn( className={cn(
"border-r border-border bg-secondary overflow-hidden shrink-0 flex flex-col h-full", "border-e border-border bg-secondary overflow-hidden shrink-0 flex flex-col h-full",
!isResizing && "transition-[width] duration-300" !isResizing && "transition-[width] duration-300"
)} )}
style={{ width: `${width}px` }} style={{ width: `${width}px` }}
@@ -154,10 +154,10 @@ export function FolderTreeSidebar({ currentPath, onNavigate, listByParentId, wid
> >
<button <button
onClick={() => handleFolderClick("/", null)} onClick={() => handleFolderClick("/", null)}
className="flex items-center px-1 rounded transition-colors duration-150 flex-1 text-left" className="flex items-center px-1 rounded transition-colors duration-150 flex-1 text-start"
style={{ paddingBlock: "var(--density-sidebar-py)", paddingLeft: "24px" }} style={{ paddingBlock: "var(--density-sidebar-py)", paddingLeft: "24px" }}
> >
<Home className={cn("w-4 h-4 flex-shrink-0 mr-2 transition-colors")} /> <Home className={cn("w-4 h-4 flex-shrink-0 me-2 transition-colors")} />
<span className="truncate">{t("breadcrumb_root")}</span> <span className="truncate">{t("breadcrumb_root")}</span>
</button> </button>
</div> </div>
@@ -207,11 +207,11 @@ export function FolderTreeSidebar({ currentPath, onNavigate, listByParentId, wid
> >
<button <button
onClick={() => handleFolderClick(path, r.id)} onClick={() => handleFolderClick(path, r.id)}
className="flex items-center px-1 rounded transition-colors duration-150 flex-1 text-left min-w-0" className="flex items-center px-1 rounded transition-colors duration-150 flex-1 text-start min-w-0"
style={{ paddingBlock: "var(--density-sidebar-py)", paddingLeft: "24px" }} style={{ paddingBlock: "var(--density-sidebar-py)", paddingLeft: "24px" }}
title={r.ownerName ? t("shared_by", { name: r.ownerName }) : r.name} title={r.ownerName ? t("shared_by", { name: r.ownerName }) : r.name}
> >
<Folder className="w-4 h-4 flex-shrink-0 mr-2 text-primary" /> <Folder className="w-4 h-4 flex-shrink-0 me-2 text-primary" />
<span className="truncate">{r.name}</span> <span className="truncate">{r.name}</span>
</button> </button>
</div> </div>
@@ -281,7 +281,7 @@ function FolderTreeItem({
onToggleExpand(node.id, node.path); onToggleExpand(node.id, node.path);
}} }}
className={cn( className={cn(
"p-0.5 rounded mr-1 transition-all duration-200", "p-0.5 rounded me-1 transition-all duration-200",
"hover:bg-muted active:bg-accent" "hover:bg-muted active:bg-accent"
)} )}
style={{ marginLeft: `${indentPx}px` }} style={{ marginLeft: `${indentPx}px` }}
@@ -297,14 +297,14 @@ function FolderTreeItem({
{/* Folder name */} {/* Folder name */}
<button <button
onClick={() => onFolderClick(node.path, node.id)} onClick={() => onFolderClick(node.path, node.id)}
className="flex items-center px-1 rounded transition-colors duration-150 flex-1 text-left" className="flex items-center px-1 rounded transition-colors duration-150 flex-1 text-start"
style={{ style={{
paddingBlock: "var(--density-sidebar-py)", paddingBlock: "var(--density-sidebar-py)",
paddingLeft: hasChildren ? "4px" : `${indentPx + 24}px`, paddingLeft: hasChildren ? "4px" : `${indentPx + 24}px`,
}} }}
> >
<Icon className={cn( <Icon className={cn(
"w-4 h-4 flex-shrink-0 mr-2 transition-colors", "w-4 h-4 flex-shrink-0 me-2 transition-colors",
isExpanded && hasChildren && "text-primary", isExpanded && hasChildren && "text-primary",
!hasChildren && depth > 0 && "text-muted-foreground" !hasChildren && depth > 0 && "text-muted-foreground"
)} /> )} />
+1 -1
View File
@@ -212,7 +212,7 @@ export function PdfMobileViewer({ url }: { url: string }) {
size="sm" size="sm"
onClick={() => window.open(url, "_blank", "noopener,noreferrer")} onClick={() => window.open(url, "_blank", "noopener,noreferrer")}
> >
<ExternalLink className="w-4 h-4 mr-2" /> <ExternalLink className="w-4 h-4 me-2" />
{t("open_in_new_tab")} {t("open_in_new_tab")}
</Button> </Button>
</div> </div>
+2 -2
View File
@@ -100,7 +100,7 @@ export function SieveEditorModal({
<div className="flex-1 min-h-0 flex border border-border rounded-md overflow-hidden"> <div className="flex-1 min-h-0 flex border border-border rounded-md overflow-hidden">
<div <div
className="w-10 flex-shrink-0 bg-muted border-r border-border py-2 text-right pr-2 select-none overflow-hidden" className="w-10 flex-shrink-0 bg-muted border-e border-border py-2 text-end pe-2 select-none overflow-hidden"
aria-hidden="true" aria-hidden="true"
> >
{Array.from({ length: lineCount }, (_, i) => ( {Array.from({ length: lineCount }, (_, i) => (
@@ -172,7 +172,7 @@ export function SieveEditorModal({
> >
{isValidating ? ( {isValidating ? (
<> <>
<Loader2 className="w-4 h-4 mr-2 animate-spin" /> <Loader2 className="w-4 h-4 me-2 animate-spin" />
{t("validating")} {t("validating")}
</> </>
) : ( ) : (
+1 -1
View File
@@ -352,7 +352,7 @@ function SignatureByteCounter({ id, value }: { id: string; value: string }) {
aria-live="polite" aria-live="polite"
> >
{t('signature_byte_counter', { bytes, max: SIGNATURE_MAX_BYTES })} {t('signature_byte_counter', { bytes, max: SIGNATURE_MAX_BYTES })}
{atLimit && <span className="ml-1">{t('signature_byte_limit_reached')}</span>} {atLimit && <span className="ms-1">{t('signature_byte_limit_reached')}</span>}
</p> </p>
); );
} }
@@ -275,7 +275,7 @@ export function IdentityManagerModal({ isOpen, onClose }: IdentityManagerModalPr
onClick={() => setIsCreating(true)} onClick={() => setIsCreating(true)}
className="mb-6 w-full sm:w-auto" className="mb-6 w-full sm:w-auto"
> >
<Plus className="w-4 h-4 mr-2" /> <Plus className="w-4 h-4 me-2" />
{t('create_new')} {t('create_new')}
</Button> </Button>
)} )}
+1 -1
View File
@@ -128,7 +128,7 @@ export function SubAddressHelper({
title={t('button_tooltip')} title={t('button_tooltip')}
className="h-8 px-2" className="h-8 px-2"
> >
<Plus className="w-4 h-4 mr-1" /> <Plus className="w-4 h-4 me-1" />
<Tag className="w-4 h-4" /> <Tag className="w-4 h-4" />
</Button> </Button>
+1 -1
View File
@@ -174,7 +174,7 @@ function ShortcutRow({
return ( return (
<div className="flex items-center justify-between py-1.5"> <div className="flex items-center justify-between py-1.5">
<span className="text-sm text-muted-foreground">{description}</span> <span className="text-sm text-muted-foreground">{description}</span>
<div className="flex items-center gap-1.5 ml-4"> <div className="flex items-center gap-1.5 ms-4">
{keys.map((key, index) => ( {keys.map((key, index) => (
<span key={index}> <span key={index}>
{index > 0 && <span className="text-muted-foreground/50 mx-1 text-xs">or</span>} {index > 0 && <span className="text-muted-foreground/50 mx-1 text-xs">or</span>}
+2 -2
View File
@@ -155,7 +155,7 @@ export function AccountSwitcher({ variant = "rail", className }: AccountSwitcher
"flex items-center gap-2 rounded-md transition-colors", "flex items-center gap-2 rounded-md transition-colors",
variant === "rail" variant === "rail"
? "justify-center w-10 h-10 hover:bg-muted" ? "justify-center w-10 h-10 hover:bg-muted"
: "w-full px-2 py-1.5 hover:bg-muted text-left min-w-0", : "w-full px-2 py-1.5 hover:bg-muted text-start min-w-0",
className className
)} )}
title={variant === "rail" ? (displayName || displayEmail) : undefined} title={variant === "rail" ? (displayName || displayEmail) : undefined}
@@ -214,7 +214,7 @@ export function AccountSwitcher({ variant = "rail", className }: AccountSwitcher
<button <button
onClick={() => handleSwitch(account.id)} onClick={() => handleSwitch(account.id)}
className={cn( className={cn(
"w-full flex items-start gap-3 px-3 py-2.5 text-left transition-colors", "w-full flex items-start gap-3 px-3 py-2.5 text-start transition-colors",
isActive ? "bg-accent/50" : "hover:bg-muted", isActive ? "bg-accent/50" : "hover:bg-muted",
isDraggable && "pe-7" isDraggable && "pe-7"
)} )}
+1 -1
View File
@@ -95,7 +95,7 @@ export function IconPicker({ value, onChange, className }: IconPickerProps) {
value={search} value={search}
onChange={(e) => setSearch(e.target.value)} onChange={(e) => setSearch(e.target.value)}
placeholder={t('search_icons')} placeholder={t('search_icons')}
className="pl-8 h-8 text-xs" className="ps-8 h-8 text-xs"
/> />
{search && ( {search && (
<button <button
+2 -2
View File
@@ -154,7 +154,7 @@ function SidebarAppForm({
<label className="block text-sm font-medium mb-2"> <label className="block text-sm font-medium mb-2">
{t('icon_label')} <span className="text-destructive">*</span> {t('icon_label')} <span className="text-destructive">*</span>
{SelectedIcon && ( {SelectedIcon && (
<span className="inline-flex items-center gap-1.5 ml-2 text-muted-foreground font-normal"> <span className="inline-flex items-center gap-1.5 ms-2 text-muted-foreground font-normal">
- <SelectedIcon className="w-4 h-4" /> {formData.icon} - <SelectedIcon className="w-4 h-4" /> {formData.icon}
</span> </span>
)} )}
@@ -275,7 +275,7 @@ export function SidebarAppsModal({ isOpen, onClose }: SidebarAppsModalProps) {
onClick={() => setIsCreating(true)} onClick={() => setIsCreating(true)}
className="mb-6 w-full sm:w-auto" className="mb-6 w-full sm:w-auto"
> >
<Plus className="w-4 h-4 mr-2" /> <Plus className="w-4 h-4 me-2" />
{t('add_new')} {t('add_new')}
</Button> </Button>
)} )}
+11 -10
View File
@@ -220,7 +220,8 @@ function SidebarRowCounts({
) : null; ) : null;
return ( return (
<span className="ml-2 flex-shrink-0 flex items-baseline gap-1" title={totalCount > 0 ? `${unreadCount} unread / ${totalCount} total` : `${unreadCount} unread`}> <span className="ms-2 flex-shrink-0 flex items-baseline gap-1" title={totalCount > 0 ? `${unreadCount} unread / ${totalCount} total` : `${unreadCount} unread`}>
{unreadNode} {unreadNode}
{unreadCount > 0 && totalCount > 0 && ( {unreadCount > 0 && totalCount > 0 && (
<span className="text-xs text-muted-foreground/60">/</span> <span className="text-xs text-muted-foreground/60">/</span>
@@ -279,12 +280,12 @@ function SidebarRow({
style={{ paddingBlock: 'var(--density-sidebar-py)' }} style={{ paddingBlock: 'var(--density-sidebar-py)' }}
className={cn( className={cn(
"group w-full flex items-center max-lg:min-h-[44px] text-sm transition-colors duration-150", "group w-full flex items-center max-lg:min-h-[44px] text-sm transition-colors duration-150",
isCollapsed ? "justify-center px-1" : "pr-2", isCollapsed ? "justify-center px-1" : "pe-2",
isVirtual isVirtual
? "text-muted-foreground" ? "text-muted-foreground"
: isSelected : isSelected
? "bg-accent text-accent-foreground font-semibold border-l-2 border-primary" ? "bg-accent text-accent-foreground font-semibold border-s-2 border-primary"
: "hover:bg-muted/50 text-foreground border-l-2 border-transparent", : "hover:bg-muted/50 text-foreground border-s-2 border-transparent",
isValidDropTarget && "bg-primary/20 ring-2 ring-primary ring-inset", isValidDropTarget && "bg-primary/20 ring-2 ring-primary ring-inset",
isInvalidDropTarget && "bg-destructive/10 ring-2 ring-destructive/30 ring-inset opacity-50" isInvalidDropTarget && "bg-destructive/10 ring-2 ring-destructive/30 ring-inset opacity-50"
)} )}
@@ -321,7 +322,7 @@ function SidebarRow({
disabled={isVirtual} disabled={isVirtual}
className={cn( className={cn(
"flex items-center gap-2 min-w-0 transition-colors", "flex items-center gap-2 min-w-0 transition-colors",
isCollapsed ? "justify-center" : "flex-1 text-left", isCollapsed ? "justify-center" : "flex-1 text-start",
isVirtual && "cursor-default select-none" isVirtual && "cursor-default select-none"
)} )}
title={isCollapsed ? label : undefined} title={isCollapsed ? label : undefined}
@@ -390,8 +391,8 @@ function SidebarSectionHeader({
) : ( ) : (
<ChevronRight className="w-3.5 h-3.5 text-muted-foreground flex-shrink-0" /> <ChevronRight className="w-3.5 h-3.5 text-muted-foreground flex-shrink-0" />
)} )}
{icon && <span className="ml-1.5 flex-shrink-0">{icon}</span>} {icon && <span className="ms-1.5 flex-shrink-0">{icon}</span>}
<span className={cn(textClass, icon ? "ml-1.5" : "ml-1.5")}> <span className={cn(textClass, icon ? "ms-1.5" : "ms-1.5")}>
{label} {label}
</span> </span>
{onSettings && ( {onSettings && (
@@ -409,7 +410,7 @@ function SidebarSectionHeader({
onSettings(); onSettings();
} }
}} }}
className="ml-auto p-1 rounded text-muted-foreground/70 hover:text-foreground hover:bg-muted transition-colors cursor-pointer" className="ms-auto p-1 rounded text-muted-foreground/70 hover:text-foreground hover:bg-muted transition-colors cursor-pointer"
title={settingsTitle} title={settingsTitle}
> >
<Settings className="w-3.5 h-3.5" /> <Settings className="w-3.5 h-3.5" />
@@ -665,7 +666,7 @@ function VacationBanner() {
> >
<Palmtree className="w-3.5 h-3.5 flex-shrink-0" /> <Palmtree className="w-3.5 h-3.5 flex-shrink-0" />
<span className="truncate font-medium">{t("vacation_active")}</span> <span className="truncate font-medium">{t("vacation_active")}</span>
<Settings className="w-3 h-3 ml-auto flex-shrink-0 opacity-60" /> <Settings className="w-3 h-3 ms-auto flex-shrink-0 opacity-60" />
</button> </button>
); );
} }
@@ -970,7 +971,7 @@ export function Sidebar({
return ( return (
<div <div
className={cn( className={cn(
"relative flex flex-col h-full border-r transition-all duration-300 overflow-hidden", "relative flex flex-col h-full border-e transition-all duration-300 overflow-hidden",
"bg-secondary border-border", "bg-secondary border-border",
"max-lg:w-full", "max-lg:w-full",
isCollapsed ? "lg:w-12" : "lg:w-full", isCollapsed ? "lg:w-12" : "lg:w-full",
+2 -2
View File
@@ -171,7 +171,7 @@ export function ProTabBar({
className={cn( className={cn(
"group relative flex items-center gap-1.5 px-3 h-9 text-sm cursor-pointer select-none transition-colors", "group relative flex items-center gap-1.5 px-3 h-9 text-sm cursor-pointer select-none transition-colors",
"min-w-0 flex-1 basis-0 max-w-[200px] [min-width:80px]", "min-w-0 flex-1 basis-0 max-w-[200px] [min-width:80px]",
"border-r border-border first:border-l", "border-e border-border first:border-s",
isActive isActive
? "bg-background text-foreground" ? "bg-background text-foreground"
: "text-muted-foreground hover:bg-muted hover:text-foreground", : "text-muted-foreground hover:bg-muted hover:text-foreground",
@@ -193,7 +193,7 @@ export function ProTabBar({
onClose(tab.id); onClose(tab.id);
}} }}
className={cn( className={cn(
"ml-1 flex items-center justify-center w-4 h-4 rounded-sm transition-colors flex-shrink-0", "ms-1 flex items-center justify-center w-4 h-4 rounded-sm transition-colors flex-shrink-0",
"text-muted-foreground hover:bg-muted-foreground/20 hover:text-foreground", "text-muted-foreground hover:bg-muted-foreground/20 hover:text-foreground",
!isActive && "opacity-0 group-hover:opacity-100 focus-visible:opacity-100", !isActive && "opacity-0 group-hover:opacity-100 focus-visible:opacity-100",
)} )}
@@ -106,7 +106,7 @@ export function ProtocolAccountPicker({
disabled={isSwitching} disabled={isSwitching}
onClick={() => onSelect(account.id)} onClick={() => onSelect(account.id)}
className={cn( className={cn(
"flex w-full items-center gap-3 rounded-md px-3 py-2.5 text-left transition-colors", "flex w-full items-center gap-3 rounded-md px-3 py-2.5 text-start transition-colors",
isActive ? "bg-accent/50" : "hover:bg-muted", isActive ? "bg-accent/50" : "hover:bg-muted",
isSwitching && "cursor-wait opacity-70" isSwitching && "cursor-wait opacity-70"
)} )}
+31 -16
View File
@@ -1,13 +1,17 @@
"use client"; "use client";
import { useEffect, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { NextIntlClientProvider } from 'next-intl'; import { NextIntlClientProvider } from 'next-intl';
import { useLocaleStore } from '@/stores/locale-store'; import { useLocaleStore } from '@/stores/locale-store';
import csMessages from '@/locales/cs/common.json'; import csMessages from '@/locales/cs/common.json';
import daMessages from '@/locales/da/common.json'; import daMessages from '@/locales/da/common.json';
import deMessages from '@/locales/de/common.json'; import deMessages from '@/locales/de/common.json';
import { getLocaleDirection } from '@/i18n/direction';
import { mergeMessages } from '@/i18n/merge-messages';
import { detectBrowserLocale } from '@/i18n/detect-locale';
import enMessages from '@/locales/en/common.json'; import enMessages from '@/locales/en/common.json';
import esMessages from '@/locales/es/common.json'; import esMessages from '@/locales/es/common.json';
import heMessages from '@/locales/he/common.json';
import faMessages from '@/locales/fa/common.json'; import faMessages from '@/locales/fa/common.json';
import frMessages from '@/locales/fr/common.json'; import frMessages from '@/locales/fr/common.json';
import huMessages from '@/locales/hu/common.json'; import huMessages from '@/locales/hu/common.json';
@@ -32,6 +36,7 @@ const ALL_MESSAGES = {
de: deMessages, de: deMessages,
en: enMessages, en: enMessages,
es: esMessages, es: esMessages,
he: heMessages,
fa: faMessages, fa: faMessages,
fr: frMessages, fr: frMessages,
hu: huMessages, hu: huMessages,
@@ -58,7 +63,6 @@ interface IntlProviderProps {
export function IntlProvider({ locale: initialLocale, children }: IntlProviderProps) { export function IntlProvider({ locale: initialLocale, children }: IntlProviderProps) {
const currentLocale = useLocaleStore((state) => state.locale); const currentLocale = useLocaleStore((state) => state.locale);
const setLocale = useLocaleStore((state) => state.setLocale);
const [activeLocale, setActiveLocale] = useState(initialLocale); const [activeLocale, setActiveLocale] = useState(initialLocale);
const [timeZone, setTimeZone] = useState<string>('UTC'); const [timeZone, setTimeZone] = useState<string>('UTC');
@@ -74,27 +78,38 @@ export function IntlProvider({ locale: initialLocale, children }: IntlProviderPr
} }
}, []); }, []);
// First mount: seed the store from the server-resolved locale if nothing is persisted. // Resolve the active locale from the user's stored choice. Empty or 'auto'
// means "follow the browser" (English default); a specific code forces it and
// is never overridden by detection.
useEffect(() => { useEffect(() => {
if (!currentLocale) { setActiveLocale(
setLocale(initialLocale); !currentLocale || currentLocale === 'auto'
} else { ? detectBrowserLocale(initialLocale)
setActiveLocale(currentLocale); : currentLocale
} );
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// Switch locale immediately when store changes
useEffect(() => {
if (currentLocale) {
setActiveLocale(currentLocale);
}
}, [currentLocale]); }, [currentLocale]);
// Keep <html> lang/dir in sync with the active locale (RTL for he/fa).
useEffect(() => {
document.documentElement.lang = activeLocale;
document.documentElement.dir = getLocaleDirection(activeLocale);
}, [activeLocale]);
// Fall back to English for any key the active locale has not translated, so
// untranslated strings show English text instead of a raw message key.
const messages = useMemo(
() => mergeMessages(
ALL_MESSAGES.en as Record<string, unknown>,
(ALL_MESSAGES[activeLocale as keyof typeof ALL_MESSAGES] ?? {}) as Record<string, unknown>
),
[activeLocale]
);
return ( return (
<NextIntlClientProvider <NextIntlClientProvider
locale={activeLocale} locale={activeLocale}
messages={ALL_MESSAGES[activeLocale as keyof typeof ALL_MESSAGES] ?? ALL_MESSAGES.en} messages={messages}
timeZone={timeZone} timeZone={timeZone}
> >
{children} {children}
+1 -1
View File
@@ -84,7 +84,7 @@ export function AdvancedSearchPanel({
<span className="text-sm font-medium text-foreground">{t("title")}</span> <span className="text-sm font-medium text-foreground">{t("title")}</span>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<Button variant="ghost" size="sm" onClick={handleClear} className="h-7 px-2 text-xs"> <Button variant="ghost" size="sm" onClick={handleClear} className="h-7 px-2 text-xs">
<RotateCcw className="w-3 h-3 mr-1" /> <RotateCcw className="w-3 h-3 me-1" />
{t("clear")} {t("clear")}
</Button> </Button>
<Button variant="ghost" size="icon" onClick={onClose} className="h-7 w-7"> <Button variant="ghost" size="icon" onClick={onClose} className="h-7 w-7">
+1 -1
View File
@@ -81,7 +81,7 @@ export function SearchChips({
<button <button
type="button" type="button"
onClick={() => onRemoveFilter(chip.key)} onClick={() => onRemoveFilter(chip.key)}
className="ml-0.5 p-0.5 rounded-full hover:bg-primary/20 transition-colors" className="ms-0.5 p-0.5 rounded-full hover:bg-primary/20 transition-colors"
> >
<X className="w-3 h-3" /> <X className="w-3 h-3" />
</button> </button>
+2 -2
View File
@@ -36,7 +36,7 @@ function VersionUpdateTag() {
return ( return (
<span <span
className={cn( className={cn(
"ml-2 inline-flex items-center rounded-full px-1.5 py-0.5 text-[10px] font-medium align-middle", "ms-2 inline-flex items-center rounded-full px-1.5 py-0.5 text-[10px] font-medium align-middle",
important important
? "bg-red-500/15 text-red-700 dark:text-red-300" ? "bg-red-500/15 text-red-700 dark:text-red-300"
: "bg-amber-500/15 text-amber-700 dark:text-amber-300", : "bg-amber-500/15 text-amber-700 dark:text-amber-300",
@@ -121,7 +121,7 @@ export function AboutDataSettings() {
{showGame && <SpamSiegeGame onClose={() => setShowGame(false)} />} {showGame && <SpamSiegeGame onClose={() => setShowGame(false)} />}
<div className="rounded-lg border border-border bg-card p-5 mb-6"> <div className="rounded-lg border border-border bg-card p-5 mb-6">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<button onClick={handleLogoClick} className="flex items-center gap-4 flex-1 text-left focus:outline-none group/about cursor-pointer" aria-label="About"> <button onClick={handleLogoClick} className="flex items-center gap-4 flex-1 text-start focus:outline-none group/about cursor-pointer" aria-label="About">
<div className="shrink-0"> <div className="shrink-0">
<img <img
src={`${getPathPrefix()}/branding/Bulwark_Logo_Color.svg`} src={`${getPathPrefix()}/branding/Bulwark_Logo_Color.svg`}
@@ -69,7 +69,7 @@ function PasswordChangeSection() {
onChange={(e) => setCurrentPassword(e.target.value)} onChange={(e) => setCurrentPassword(e.target.value)}
required required
autoComplete="current-password" autoComplete="current-password"
className="pr-10" className="pe-10"
/> />
<button <button
type="button" type="button"
@@ -90,7 +90,7 @@ function PasswordChangeSection() {
required required
minLength={8} minLength={8}
autoComplete="new-password" autoComplete="new-password"
className="pr-10" className="pe-10"
/> />
<button <button
type="button" type="button"
@@ -120,7 +120,7 @@ function PasswordChangeSection() {
size="sm" size="sm"
disabled={isSaving || !currentPassword || !newPassword || !confirmPassword} disabled={isSaving || !currentPassword || !newPassword || !confirmPassword}
> >
{isSaving ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : null} {isSaving ? <Loader2 className="w-4 h-4 me-2 animate-spin" /> : null}
{t('password.submit')} {t('password.submit')}
</Button> </Button>
</form> </form>
@@ -294,7 +294,7 @@ function TotpSection() {
</SettingItem> </SettingItem>
{setupUrl && ( {setupUrl && (
<div className="ml-4 p-3 bg-muted rounded-md space-y-3"> <div className="ms-4 p-3 bg-muted rounded-md space-y-3">
<p className="text-xs text-muted-foreground">{t('totp.setup_instructions')}</p> <p className="text-xs text-muted-foreground">{t('totp.setup_instructions')}</p>
{qrDataUrl && ( {qrDataUrl && (
<div className="flex justify-center"> <div className="flex justify-center">
@@ -315,7 +315,7 @@ function TotpSection() {
{setupError && <p className="text-xs text-destructive">{setupError}</p>} {setupError && <p className="text-xs text-destructive">{setupError}</p>}
<div className="flex gap-2"> <div className="flex gap-2">
<Button size="sm" onClick={confirmSetup} disabled={isSaving || !password || !otpCode}> <Button size="sm" onClick={confirmSetup} disabled={isSaving || !password || !otpCode}>
{isSaving ? <Loader2 className="w-4 h-4 mr-1 animate-spin" /> : null} {isSaving ? <Loader2 className="w-4 h-4 me-1 animate-spin" /> : null}
{t('totp.confirm')} {t('totp.confirm')}
</Button> </Button>
<Button size="sm" variant="ghost" onClick={cancelSetup}>{t('app_passwords.cancel')}</Button> <Button size="sm" variant="ghost" onClick={cancelSetup}>{t('app_passwords.cancel')}</Button>
@@ -324,7 +324,7 @@ function TotpSection() {
)} )}
{disableOpen && ( {disableOpen && (
<div className="ml-4 p-3 bg-muted rounded-md space-y-2"> <div className="ms-4 p-3 bg-muted rounded-md space-y-2">
<p className="text-xs text-muted-foreground">{t('totp.disable_confirm_prompt')}</p> <p className="text-xs text-muted-foreground">{t('totp.disable_confirm_prompt')}</p>
<Input <Input
type="password" type="password"
@@ -336,7 +336,7 @@ function TotpSection() {
{setupError && <p className="text-xs text-destructive">{setupError}</p>} {setupError && <p className="text-xs text-destructive">{setupError}</p>}
<div className="flex gap-2"> <div className="flex gap-2">
<Button size="sm" variant="destructive" onClick={handleDisable} disabled={isSaving || !password}> <Button size="sm" variant="destructive" onClick={handleDisable} disabled={isSaving || !password}>
{isSaving ? <Loader2 className="w-4 h-4 mr-1 animate-spin" /> : null} {isSaving ? <Loader2 className="w-4 h-4 me-1 animate-spin" /> : null}
{t('totp.disable')} {t('totp.disable')}
</Button> </Button>
<Button size="sm" variant="ghost" onClick={() => { setDisableOpen(false); setPassword(''); setSetupError(null); }}> <Button size="sm" variant="ghost" onClick={() => { setDisableOpen(false); setPassword(''); setSetupError(null); }}>
@@ -470,7 +470,7 @@ function CredentialSection({ icon: Icon, i18nNamespace, entries, onCreate, onRem
<h4 className="text-sm font-medium text-foreground">{tk('title')}</h4> <h4 className="text-sm font-medium text-foreground">{tk('title')}</h4>
</div> </div>
<Button variant="outline" size="sm" onClick={() => setShowAdd(!showAdd)}> <Button variant="outline" size="sm" onClick={() => setShowAdd(!showAdd)}>
<Plus className="w-3 h-3 mr-1" /> <Plus className="w-3 h-3 me-1" />
{t('app_passwords.add')} {t('app_passwords.add')}
</Button> </Button>
</div> </div>
@@ -521,7 +521,7 @@ function CredentialSection({ icon: Icon, i18nNamespace, entries, onCreate, onRem
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<Button type="submit" size="sm" disabled={isSaving || !newDescription.trim()}> <Button type="submit" size="sm" disabled={isSaving || !newDescription.trim()}>
{isSaving ? <Loader2 className="w-4 h-4 mr-1 animate-spin" /> : null} {isSaving ? <Loader2 className="w-4 h-4 me-1 animate-spin" /> : null}
{t('app_passwords.create')} {t('app_passwords.create')}
</Button> </Button>
<Button type="button" variant="ghost" size="sm" onClick={() => setShowAdd(false)}> <Button type="button" variant="ghost" size="sm" onClick={() => setShowAdd(false)}>
@@ -768,9 +768,9 @@ function LinkDeviceSection() {
<Button variant="outline" size="sm" onClick={() => void generate()} disabled={loading}> <Button variant="outline" size="sm" onClick={() => void generate()} disabled={loading}>
{loading ? ( {loading ? (
<Loader2 className="w-3 h-3 mr-1 animate-spin" /> <Loader2 className="w-3 h-3 me-1 animate-spin" />
) : ( ) : (
<QrCode className="w-3 h-3 mr-1" /> <QrCode className="w-3 h-3 me-1" />
)} )}
{hasGenerated ? t('link_device.regenerate') : t('link_device.generate')} {hasGenerated ? t('link_device.regenerate') : t('link_device.generate')}
</Button> </Button>
+3 -3
View File
@@ -223,7 +223,7 @@ export function AccountSettings() {
onClick={handleAddAccount} onClick={handleAddAccount}
className="w-full" className="w-full"
> >
<Plus className="w-4 h-4 mr-2" /> <Plus className="w-4 h-4 me-2" />
{t('accounts.add')} {t('accounts.add')}
</Button> </Button>
)} )}
@@ -245,7 +245,7 @@ export function AccountSettings() {
onClick={() => handleManageShared(acc)} onClick={() => handleManageShared(acc)}
disabled={!editable} disabled={!editable}
className={cn( className={cn(
'flex items-center gap-3 w-full p-3 border border-border rounded-lg text-left transition-colors', 'flex items-center gap-3 w-full p-3 border border-border rounded-lg text-start transition-colors',
editable ? 'hover:bg-muted/50 cursor-pointer' : 'opacity-60 cursor-not-allowed', editable ? 'hover:bg-muted/50 cursor-pointer' : 'opacity-60 cursor-not-allowed',
)} )}
> >
@@ -359,7 +359,7 @@ function AccountRow({
onClick={onSwitch} onClick={onSwitch}
disabled={isActive} disabled={isActive}
className={cn( className={cn(
'min-w-0 flex-1 text-left', 'min-w-0 flex-1 text-start',
!isActive && 'cursor-pointer' !isActive && 'cursor-pointer'
)} )}
title={isActive ? labels.active : labels.switchTo} title={isActive ? labels.active : labels.switchTo}
+1 -1
View File
@@ -141,7 +141,7 @@ export function AppearanceSettings() {
onClick={() => { resetTourCompletion(); startTour(); }} onClick={() => { resetTourCompletion(); startTour(); }}
className="text-xs h-7" className="text-xs h-7"
> >
<PlayCircle className="w-3.5 h-3.5 mr-1" /> <PlayCircle className="w-3.5 h-3.5 me-1" />
{tTour('restart_button')} {tTour('restart_button')}
</Button> </Button>
</SettingItem> </SettingItem>
+9 -1
View File
@@ -23,6 +23,7 @@ export function ComposingSettings() {
const { const {
autoSelectReplyIdentity, autoSelectReplyIdentity,
plainTextMode, plainTextMode,
rtlEditingSupport,
attachmentReminderEnabled, attachmentReminderEnabled,
attachmentReminderKeywords, attachmentReminderKeywords,
sendDelaySeconds, sendDelaySeconds,
@@ -52,6 +53,13 @@ export function ComposingSettings() {
/> />
</SettingItem> </SettingItem>
<SettingItem label={t('rtl_editing.label')} description={t('rtl_editing.description')}>
<ToggleSwitch
checked={rtlEditingSupport}
onChange={(checked) => updateSetting('rtlEditingSupport', checked)}
/>
</SettingItem>
<SettingItem label={t('send_delay.label')} description={t('send_delay.description')}> <SettingItem label={t('send_delay.label')} description={t('send_delay.description')}>
<div className="flex flex-col items-end gap-1"> <div className="flex flex-col items-end gap-1">
<Select <Select
@@ -65,7 +73,7 @@ export function ComposingSettings() {
]} ]}
/> />
{sendDelaySeconds > 0 && !delayedSendSupported && ( {sendDelaySeconds > 0 && !delayedSendSupported && (
<p className="max-w-64 text-right text-xs text-amber-600 dark:text-amber-400">{t('send_delay.unsupported')}</p> <p className="max-w-64 text-end text-xs text-amber-600 dark:text-amber-400">{t('send_delay.unsupported')}</p>
)} )}
</div> </div>
</SettingItem> </SettingItem>
+2 -2
View File
@@ -73,7 +73,7 @@ export function ContactsSettings() {
description={tSettings("import_description")} description={tSettings("import_description")}
> >
<Button variant="outline" size="sm" onClick={() => setShowImport(true)}> <Button variant="outline" size="sm" onClick={() => setShowImport(true)}>
<Upload className="w-4 h-4 mr-2" /> <Upload className="w-4 h-4 me-2" />
{t("import.title")} {t("import.title")}
</Button> </Button>
</SettingItem> </SettingItem>
@@ -88,7 +88,7 @@ export function ContactsSettings() {
onClick={handleExport} onClick={handleExport}
disabled={individuals.length === 0} disabled={individuals.length === 0}
> >
<Download className="w-4 h-4 mr-2" /> <Download className="w-4 h-4 me-2" />
{t("export.title")} {t("export.title")}
</Button> </Button>
</SettingItem> </SettingItem>
+1 -1
View File
@@ -25,7 +25,7 @@ export function DebugSettings() {
</SettingItem> </SettingItem>
{debugMode && ( {debugMode && (
<div className="ml-4 border-l-2 border-muted pl-4 space-y-1"> <div className="ms-4 border-s-2 border-muted ps-4 space-y-1">
<p className="text-xs text-muted-foreground mb-2">{t('debug_categories.description')}</p> <p className="text-xs text-muted-foreground mb-2">{t('debug_categories.description')}</p>
{ALL_DEBUG_CATEGORIES.map((cat) => ( {ALL_DEBUG_CATEGORIES.map((cat) => (
<SettingItem <SettingItem
+5 -5
View File
@@ -84,8 +84,8 @@ function FilesSettingsPreview({ settings }: { settings: FilesSettings }) {
<div className="flex-1 min-w-0 overflow-hidden"> <div className="flex-1 min-w-0 overflow-hidden">
<div className="flex items-center gap-3 px-2 py-1 text-[10px] font-medium text-muted-foreground border-b border-border bg-muted/50"> <div className="flex items-center gap-3 px-2 py-1 text-[10px] font-medium text-muted-foreground border-b border-border bg-muted/50">
<span className="flex-1 min-w-0">Name</span> <span className="flex-1 min-w-0">Name</span>
<span className="w-14 text-right">Size</span> <span className="w-14 text-end">Size</span>
<span className="w-16 text-right">Modified</span> <span className="w-16 text-end">Modified</span>
</div> </div>
{sortedFiles.map((file) => ( {sortedFiles.map((file) => (
<div <div
@@ -103,10 +103,10 @@ function FilesSettingsPreview({ settings }: { settings: FilesSettings }) {
<span className={cn("flex-1 min-w-0 truncate text-[11px]", file.isFolder && "font-medium")}> <span className={cn("flex-1 min-w-0 truncate text-[11px]", file.isFolder && "font-medium")}>
{file.name} {file.name}
</span> </span>
<span className="w-14 text-right text-[10px] text-muted-foreground tabular-nums"> <span className="w-14 text-end text-[10px] text-muted-foreground tabular-nums">
{formatSize(file.size)} {formatSize(file.size)}
</span> </span>
<span className="w-16 text-right text-[10px] text-muted-foreground tabular-nums"> <span className="w-16 text-end text-[10px] text-muted-foreground tabular-nums">
{file.modified.slice(5)} {file.modified.slice(5)}
</span> </span>
</div> </div>
@@ -142,7 +142,7 @@ function FilesSettingsPreview({ settings }: { settings: FilesSettings }) {
); );
const sidebar = settings.folderLayout === "sidebar" && ( const sidebar = settings.folderLayout === "sidebar" && (
<div className="w-24 border-r border-border bg-muted/30 py-1.5 flex-shrink-0"> <div className="w-24 border-e border-border bg-muted/30 py-1.5 flex-shrink-0">
<div className="flex items-center gap-1 px-2 py-0.5 text-[10px] font-medium text-foreground"> <div className="flex items-center gap-1 px-2 py-0.5 text-[10px] font-medium text-foreground">
<Home className="w-3 h-3 flex-shrink-0" /> <Home className="w-3 h-3 flex-shrink-0" />
<span className="truncate">Files</span> <span className="truncate">Files</span>
+3 -3
View File
@@ -487,7 +487,7 @@ export function FilterSettings() {
try { localStorage.setItem('settings-active-tab', 'vacation'); } catch { /* ignore */ } try { localStorage.setItem('settings-active-tab', 'vacation'); } catch { /* ignore */ }
window.dispatchEvent(new CustomEvent('settings-tab-change', { detail: 'vacation' })); window.dispatchEvent(new CustomEvent('settings-tab-change', { detail: 'vacation' }));
}} }}
className="flex items-center gap-3 w-full p-3 rounded-md border border-green-200 dark:border-green-800 bg-green-50 dark:bg-green-900/20 hover:bg-green-100 dark:hover:bg-green-900/30 transition-colors text-left" className="flex items-center gap-3 w-full p-3 rounded-md border border-green-200 dark:border-green-800 bg-green-50 dark:bg-green-900/20 hover:bg-green-100 dark:hover:bg-green-900/30 transition-colors text-start"
> >
<div className="flex items-center justify-center w-8 h-8 rounded-full bg-green-100 dark:bg-green-900/40"> <div className="flex items-center justify-center w-8 h-8 rounded-full bg-green-100 dark:bg-green-900/40">
<PalmtreeIcon className="w-4 h-4 text-green-600 dark:text-green-400" /> <PalmtreeIcon className="w-4 h-4 text-green-600 dark:text-green-400" />
@@ -661,7 +661,7 @@ export function FilterSettings() {
setShowRuleModal(true); setShowRuleModal(true);
}} }}
> >
<Plus className="w-4 h-4 mr-1" /> <Plus className="w-4 h-4 me-1" />
{t("add_rule")} {t("add_rule")}
</Button> </Button>
)} )}
@@ -670,7 +670,7 @@ export function FilterSettings() {
size="sm" size="sm"
onClick={() => setShowSieveEditor(true)} onClick={() => setShowSieveEditor(true)}
> >
<Code className="w-4 h-4 mr-1" /> <Code className="w-4 h-4 me-1" />
{t("raw_editor")} {t("raw_editor")}
</Button> </Button>
</div> </div>
+1 -1
View File
@@ -57,7 +57,7 @@ export function LanguageSettings() {
{ value: 'full', label: t('date_format.full') }, { value: 'full', label: t('date_format.full') },
]} ]}
/> />
<div className="text-xs text-muted-foreground text-right space-y-0.5 font-mono"> <div className="text-xs text-muted-foreground text-end space-y-0.5 font-mono">
<div> <div>
<span className="opacity-70">{t('date_format.preview_today')} </span> <span className="opacity-70">{t('date_format.preview_today')} </span>
<span className="text-foreground/90">{preview.today}</span> <span className="text-foreground/90">{preview.today}</span>
+6 -6
View File
@@ -38,11 +38,11 @@ function MailLayoutPreview({
<div className="mt-3 overflow-hidden rounded-lg border border-border bg-muted/20"> <div className="mt-3 overflow-hidden rounded-lg border border-border bg-muted/20">
<div className="flex h-28"> <div className="flex h-28">
<div className="w-11 border-r border-border bg-muted/40" /> <div className="w-11 border-e border-border bg-muted/40" />
{value === 'split' && ( {value === 'split' && (
<> <>
<div className="w-28 border-r border-border bg-background"> <div className="w-28 border-e border-border bg-background">
{MAIL_LAYOUT_PREVIEW_ROWS.map((row) => ( {MAIL_LAYOUT_PREVIEW_ROWS.map((row) => (
<div <div
key={row.subject} key={row.subject}
@@ -245,7 +245,7 @@ export function LayoutSettings() {
)} )}
{enableUnifiedMailbox && hasGroupInboxes && !isSettingHidden('includeGroupInUnified') && ( {enableUnifiedMailbox && hasGroupInboxes && !isSettingHidden('includeGroupInUnified') && (
<div className="ml-4 border-l-2 border-border pl-4 -mt-2"> <div className="ms-4 border-s-2 border-border ps-4 -mt-2">
<SettingItem <SettingItem
label={t('unified_mailbox.include_group.label')} label={t('unified_mailbox.include_group.label')}
description={t('unified_mailbox.include_group.description')} description={t('unified_mailbox.include_group.description')}
@@ -260,7 +260,7 @@ export function LayoutSettings() {
)} )}
{enableUnifiedMailbox && crossViews.some(c => c.allowed) && ( {enableUnifiedMailbox && crossViews.some(c => c.allowed) && (
<div className="ml-4 border-l-2 border-border pl-4 -mt-2 space-y-2"> <div className="ms-4 border-s-2 border-border ps-4 -mt-2 space-y-2">
{crossViews.map(({ setting, value, allowed, labelKey, descKey }) => ( {crossViews.map(({ setting, value, allowed, labelKey, descKey }) => (
allowed && !isSettingHidden(setting) && ( allowed && !isSettingHidden(setting) && (
<SettingItem <SettingItem
@@ -293,7 +293,7 @@ export function LayoutSettings() {
)} )}
{allMailViewAllowed && enableAllMailView && ( {allMailViewAllowed && enableAllMailView && (
<div className="ml-4 border-l-2 border-border pl-4 -mt-2 space-y-2"> <div className="ms-4 border-s-2 border-border ps-4 -mt-2 space-y-2">
<div> <div>
<div className="text-sm font-medium text-foreground">{t('all_mail.folders_label')}</div> <div className="text-sm font-medium text-foreground">{t('all_mail.folders_label')}</div>
<div className="text-xs text-muted-foreground">{t('all_mail.folders_description')}</div> <div className="text-xs text-muted-foreground">{t('all_mail.folders_description')}</div>
@@ -312,7 +312,7 @@ export function LayoutSettings() {
key={mb.id} key={mb.id}
type="button" type="button"
onClick={() => toggleAllMailFolder(mb.id)} onClick={() => toggleAllMailFolder(mb.id)}
className="w-full flex items-center gap-2.5 py-1.5 px-2 rounded-md hover:bg-muted/50 text-left" className="w-full flex items-center gap-2.5 py-1.5 px-2 rounded-md hover:bg-muted/50 text-start"
role="checkbox" role="checkbox"
aria-checked={checked} aria-checked={checked}
> >
+2 -2
View File
@@ -35,7 +35,7 @@ export function SettingItem({ label, description, children, locked }: SettingIte
data-search-label={label} data-search-label={label}
className={cn("flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between sm:gap-4 py-3 border-b border-border last:border-0", locked && "opacity-60")} className={cn("flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between sm:gap-4 py-3 border-b border-border last:border-0", locked && "opacity-60")}
> >
<div className="flex-1 min-w-0 sm:pr-4"> <div className="flex-1 min-w-0 sm:pe-4">
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<label className="text-sm font-medium text-foreground">{label}</label> <label className="text-sm font-medium text-foreground">{label}</label>
{locked && <Lock className="w-3 h-3 text-muted-foreground" aria-label="Managed by administrator" />} {locked && <Lock className="w-3 h-3 text-muted-foreground" aria-label="Managed by administrator" />}
@@ -72,7 +72,7 @@ export function ToggleSwitch({ checked, onChange, disabled }: ToggleSwitchProps)
<span <span
className={cn( className={cn(
'inline-block h-4 w-4 transform rounded-full bg-background transition-transform duration-150', 'inline-block h-4 w-4 transform rounded-full bg-background transition-transform duration-150',
checked ? 'translate-x-6' : 'translate-x-1' checked ? 'ltr:translate-x-6 rtl:-translate-x-6' : 'ltr:translate-x-1 rtl:-translate-x-1'
)} )}
/> />
</button> </button>
@@ -284,7 +284,7 @@ export function ShareCollectionDialog({
value={preset} value={preset}
onChange={(e) => handleSetRights(principalId, e.target.value as RolePreset)} onChange={(e) => handleSetRights(principalId, e.target.value as RolePreset)}
disabled={savingId === principalId} disabled={savingId === principalId}
className="appearance-none rounded-md border border-input bg-background pl-3 pr-8 py-1.5 text-xs focus:outline-none focus:ring-2 focus:ring-ring disabled:opacity-50" className="appearance-none rounded-md border border-input bg-background ps-3 pe-8 py-1.5 text-xs focus:outline-none focus:ring-2 focus:ring-ring disabled:opacity-50"
> >
{presetOptions.map((p) => ( {presetOptions.map((p) => (
<option key={p} value={p}>{t(`preset.${p}`)}</option> <option key={p} value={p}>{t(`preset.${p}`)}</option>
@@ -318,7 +318,7 @@ export function ShareCollectionDialog({
onClick={() => setShowAdd(true)} onClick={() => setShowAdd(true)}
className="w-full" className="w-full"
> >
<UserPlus className="w-4 h-4 mr-2" /> <UserPlus className="w-4 h-4 me-2" />
{t("add_person")} {t("add_person")}
</Button> </Button>
)} )}
@@ -336,7 +336,7 @@ export function ShareCollectionDialog({
<div className="max-h-48 overflow-y-auto -mx-1"> <div className="max-h-48 overflow-y-auto -mx-1">
{loadingPrincipals && ( {loadingPrincipals && (
<div className="flex items-center justify-center py-4 text-muted-foreground"> <div className="flex items-center justify-center py-4 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_principals")} {t("loading_principals")}
</div> </div>
)} )}
@@ -350,7 +350,7 @@ export function ShareCollectionDialog({
key={p.id} key={p.id}
onClick={() => handleAdd(p)} onClick={() => handleAdd(p)}
disabled={savingId === p.id} disabled={savingId === p.id}
className="w-full text-left px-3 py-2 rounded-md hover:bg-muted disabled:opacity-50 transition-colors" className="w-full text-start px-3 py-2 rounded-md hover:bg-muted disabled:opacity-50 transition-colors"
> >
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Avatar <Avatar
@@ -330,7 +330,7 @@ export function SidebarAppsSettings() {
onClick={() => setShowAddForm(true)} onClick={() => setShowAddForm(true)}
className="w-full" className="w-full"
> >
<Plus className="w-4 h-4 mr-2" /> <Plus className="w-4 h-4 me-2" />
{tApps("add_new")} {tApps("add_new")}
</Button> </Button>
)} )}
+1 -1
View File
@@ -304,7 +304,7 @@ export function SpamSiegeGame({ onClose }: { onClose: () => void }) {
Close Close
</Button> </Button>
<Button size="sm" onClick={startGame}> <Button size="sm" onClick={startGame}>
<RotateCcw className="w-3.5 h-3.5 mr-1.5" /> <RotateCcw className="w-3.5 h-3.5 me-1.5" />
Again Again
</Button> </Button>
</div> </div>
+2 -2
View File
@@ -104,7 +104,7 @@ export function TemplateSettings() {
onClick={handleExport} onClick={handleExport}
disabled={templates.length === 0} disabled={templates.length === 0}
> >
<Download className="w-4 h-4 mr-1" /> <Download className="w-4 h-4 me-1" />
{t('export')} {t('export')}
</Button> </Button>
<div> <div>
@@ -120,7 +120,7 @@ export function TemplateSettings() {
size="sm" size="sm"
onClick={() => fileInputRef.current?.click()} onClick={() => fileInputRef.current?.click()}
> >
<Upload className="w-4 h-4 mr-1" /> <Upload className="w-4 h-4 me-1" />
{t('import')} {t('import')}
</Button> </Button>
</div> </div>
+1 -1
View File
@@ -139,7 +139,7 @@ function ThemeCard({ name, author, preview, css, isDark, isDefaultTheme, isActiv
onClick={onActivate} onClick={onActivate}
disabled={disabled} disabled={disabled}
className={cn( className={cn(
'flex flex-col items-center p-3 rounded-xl border-2 transition-all text-left w-full disabled:cursor-not-allowed disabled:opacity-60', 'flex flex-col items-center p-3 rounded-xl border-2 transition-all text-start w-full disabled:cursor-not-allowed disabled:opacity-60',
isActive isActive
? 'border-primary bg-primary/5 ring-1 ring-primary/20' ? 'border-primary bg-primary/5 ring-1 ring-primary/20'
: 'border-border hover:border-primary/40 bg-card', : 'border-border hover:border-primary/40 bg-card',
+1 -1
View File
@@ -258,7 +258,7 @@ export function VacationSettings() {
> >
{isSaving ? ( {isSaving ? (
<> <>
<Loader2 className="w-4 h-4 mr-2 animate-spin" /> <Loader2 className="w-4 h-4 me-2 animate-spin" />
{t('saving')} {t('saving')}
</> </>
) : ( ) : (
+4 -4
View File
@@ -142,7 +142,7 @@ export function TemplateForm({ template, initialData, onSave, onCancel }: Templa
className="h-6 px-2 text-xs" className="h-6 px-2 text-xs"
onClick={() => setShowPlaceholderMenu(showPlaceholderMenu === 'subject' ? null : 'subject')} onClick={() => setShowPlaceholderMenu(showPlaceholderMenu === 'subject' ? null : 'subject')}
> >
<Plus className="w-3 h-3 mr-1" /> <Plus className="w-3 h-3 me-1" />
{t('placeholder')} {t('placeholder')}
</Button> </Button>
{showPlaceholderMenu === 'subject' && ( {showPlaceholderMenu === 'subject' && (
@@ -172,7 +172,7 @@ export function TemplateForm({ template, initialData, onSave, onCancel }: Templa
className="h-6 px-2 text-xs" className="h-6 px-2 text-xs"
onClick={() => setShowPlaceholderMenu(showPlaceholderMenu === 'body' ? null : 'body')} onClick={() => setShowPlaceholderMenu(showPlaceholderMenu === 'body' ? null : 'body')}
> >
<Plus className="w-3 h-3 mr-1" /> <Plus className="w-3 h-3 me-1" />
{t('placeholder')} {t('placeholder')}
</Button> </Button>
{showPlaceholderMenu === 'body' && ( {showPlaceholderMenu === 'body' && (
@@ -281,11 +281,11 @@ function PlaceholderDropdown({
<button <button
key={p} key={p}
type="button" type="button"
className="w-full text-left px-3 py-1.5 text-sm rounded hover:bg-muted transition-colors" className="w-full text-start px-3 py-1.5 text-sm rounded hover:bg-muted transition-colors"
onClick={() => onSelect(p)} onClick={() => onSelect(p)}
> >
<span className="font-mono text-xs text-primary">{`{{${p}}}`}</span> <span className="font-mono text-xs text-primary">{`{{${p}}}`}</span>
<span className="ml-2 text-muted-foreground">{t(`placeholders.${p}`)}</span> <span className="ms-2 text-muted-foreground">{t(`placeholders.${p}`)}</span>
</button> </button>
))} ))}
</div> </div>
@@ -104,7 +104,7 @@ export function TemplateManagerModal({ isOpen, onClose }: TemplateManagerModalPr
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
placeholder={t('search_placeholder')} placeholder={t('search_placeholder')}
className="pl-9" className="ps-9"
/> />
</div> </div>
<Button <Button
+2 -2
View File
@@ -76,7 +76,7 @@ export function TemplatePicker({ isOpen, onClose, onSelect }: TemplatePickerProp
key={template.id} key={template.id}
type="button" type="button"
onClick={() => handleSelectTemplate(template)} onClick={() => handleSelectTemplate(template)}
className="w-full text-left p-3 rounded-md hover:bg-muted transition-colors group" className="w-full text-start p-3 rounded-md hover:bg-muted transition-colors group"
> >
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{template.isFavorite && ( {template.isFavorite && (
@@ -155,7 +155,7 @@ export function TemplatePicker({ isOpen, onClose, onSelect }: TemplatePickerProp
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
placeholder={t('search_placeholder')} placeholder={t('search_placeholder')}
className="pl-9 h-9" className="ps-9 h-9"
autoFocus autoFocus
/> />
</div> </div>
+1 -1
View File
@@ -204,7 +204,7 @@ export function TrustedSendersModal({ isOpen, onClose }: TrustedSendersModalProp
placeholder={t("search_placeholder")} placeholder={t("search_placeholder")}
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
className="w-full pl-9 pr-3 py-2 text-sm bg-muted border border-border rounded-md focus:outline-none focus:ring-2 focus:ring-primary/50" className="w-full ps-9 pe-3 py-2 text-sm bg-muted border border-border rounded-md focus:outline-none focus:ring-2 focus:ring-primary/50"
/> />
</div> </div>
</div> </div>
+2 -2
View File
@@ -123,7 +123,7 @@ export function ContextMenuItem({
role="menuitem" role="menuitem"
disabled={disabled} disabled={disabled}
className={cn( className={cn(
"w-full px-3 py-1.5 text-sm text-left flex items-center gap-2", "w-full px-3 py-1.5 text-sm text-start flex items-center gap-2",
"transition-colors duration-150", "transition-colors duration-150",
"focus:outline-none focus:bg-muted", "focus:outline-none focus:bg-muted",
disabled && "opacity-50 cursor-not-allowed", disabled && "opacity-50 cursor-not-allowed",
@@ -139,7 +139,7 @@ export function ContextMenuItem({
{Icon && <Icon className="w-4 h-4 flex-shrink-0" />} {Icon && <Icon className="w-4 h-4 flex-shrink-0" />}
<span className="flex-1">{label}</span> <span className="flex-1">{label}</span>
{shortcut && ( {shortcut && (
<span className="text-xs text-muted-foreground ml-auto">{shortcut}</span> <span className="text-xs text-muted-foreground ms-auto">{shortcut}</span>
)} )}
</button> </button>
); );

Some files were not shown because too many files have changed in this diff Show More