feat(i18n): Hebrew locale + full RTL support
This commit is contained in:
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
@@ -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>
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
@@ -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
@@ -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);
|
||||||
|
}
|
||||||
|
|||||||
@@ -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)' }}
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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")}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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")}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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)}
|
||||||
|
|||||||
@@ -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",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
@@ -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"
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -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" />}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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[] = [];
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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"
|
||||||
)} />
|
)} />
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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")}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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>}
|
||||||
|
|||||||
@@ -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"
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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"
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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')}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
Reference in New Issue
Block a user