feat: add hover actions display mode and corner selection options in settings #58
This commit is contained in:
@@ -54,6 +54,13 @@ const ACTION_CONFIG: Record<HoverAction, {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const CORNER_CLASSES = {
|
||||||
|
'top-right': 'top-1 right-1',
|
||||||
|
'top-left': 'top-1 left-1',
|
||||||
|
'bottom-right': 'bottom-1 right-1',
|
||||||
|
'bottom-left': 'bottom-1 left-1',
|
||||||
|
} as const;
|
||||||
|
|
||||||
export function EmailHoverActions({
|
export function EmailHoverActions({
|
||||||
email,
|
email,
|
||||||
onToggleStar,
|
onToggleStar,
|
||||||
@@ -64,6 +71,8 @@ export function EmailHoverActions({
|
|||||||
onMarkAsSpam,
|
onMarkAsSpam,
|
||||||
}: EmailHoverActionsProps) {
|
}: EmailHoverActionsProps) {
|
||||||
const hoverActions = useSettingsStore((state) => state.hoverActions);
|
const hoverActions = useSettingsStore((state) => state.hoverActions);
|
||||||
|
const hoverActionsMode = useSettingsStore((state) => state.hoverActionsMode);
|
||||||
|
const hoverActionsCorner = useSettingsStore((state) => state.hoverActionsCorner);
|
||||||
const t = useTranslations("settings.email_behavior.hover_actions");
|
const t = useTranslations("settings.email_behavior.hover_actions");
|
||||||
|
|
||||||
const isUnread = !email.keywords?.$seen;
|
const isUnread = !email.keywords?.$seen;
|
||||||
@@ -96,42 +105,59 @@ export function EmailHoverActions({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const actionButtons = hoverActions.map((actionId) => {
|
||||||
|
const config = ACTION_CONFIG[actionId];
|
||||||
|
if (!config) return null;
|
||||||
|
const Icon = config.icon;
|
||||||
|
|
||||||
|
const DisplayIcon = actionId === "markRead"
|
||||||
|
? (isUnread ? MailOpen : Mail)
|
||||||
|
: actionId === "star" && isStarred
|
||||||
|
? Star
|
||||||
|
: Icon;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={actionId}
|
||||||
|
onClick={(e) => handleAction(e, actionId)}
|
||||||
|
title={t(config.titleKey)}
|
||||||
|
className={cn(
|
||||||
|
"p-1.5 rounded-md transition-colors duration-100 text-muted-foreground hover:bg-black/5 dark:hover:bg-white/10",
|
||||||
|
config.className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<DisplayIcon
|
||||||
|
className={cn(
|
||||||
|
"w-4 h-4",
|
||||||
|
actionId === "star" && isStarred && "fill-amber-400 text-amber-400",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (hoverActionsMode === 'floating') {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"absolute z-10 hidden group-hover:flex items-center",
|
||||||
|
CORNER_CLASSES[hoverActionsCorner],
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-0.5 bg-muted rounded-lg px-1.5 py-0.5 shadow-md border border-border">
|
||||||
|
{actionButtons}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="absolute right-0 top-0 bottom-0 z-10 hidden group-hover:flex items-center"
|
className="absolute right-0 top-0 bottom-0 z-10 hidden group-hover:flex items-center"
|
||||||
>
|
>
|
||||||
<div className="w-8 h-full bg-gradient-to-r from-transparent to-muted" />
|
<div className="w-8 h-full bg-gradient-to-r from-transparent to-muted" />
|
||||||
<div className="flex items-center gap-0.5 h-full bg-muted pr-3 pl-0.5">
|
<div className="flex items-center gap-0.5 h-full bg-muted pr-3 pl-0.5">
|
||||||
{hoverActions.map((actionId) => {
|
{actionButtons}
|
||||||
const config = ACTION_CONFIG[actionId];
|
|
||||||
if (!config) return null;
|
|
||||||
const Icon = config.icon;
|
|
||||||
|
|
||||||
const DisplayIcon = actionId === "markRead"
|
|
||||||
? (isUnread ? MailOpen : Mail)
|
|
||||||
: actionId === "star" && isStarred
|
|
||||||
? Star
|
|
||||||
: Icon;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={actionId}
|
|
||||||
onClick={(e) => handleAction(e, actionId)}
|
|
||||||
title={t(config.titleKey)}
|
|
||||||
className={cn(
|
|
||||||
"p-1.5 rounded-md transition-colors duration-100 text-muted-foreground hover:bg-black/5 dark:hover:bg-white/10",
|
|
||||||
config.className,
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<DisplayIcon
|
|
||||||
className={cn(
|
|
||||||
"w-4 h-4",
|
|
||||||
actionId === "star" && isStarred && "fill-amber-400 text-amber-400",
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useState, useCallback } from 'react';
|
|||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
import { useConfig } from '@/hooks/use-config';
|
import { useConfig } from '@/hooks/use-config';
|
||||||
import { useSettingsStore } from '@/stores/settings-store';
|
import { useSettingsStore } from '@/stores/settings-store';
|
||||||
import type { ArchiveMode, HoverAction } from '@/stores/settings-store';
|
import type { ArchiveMode, HoverAction, HoverActionsMode, HoverActionsCorner } from '@/stores/settings-store';
|
||||||
import { ALL_HOVER_ACTIONS } from '@/stores/settings-store';
|
import { ALL_HOVER_ACTIONS } from '@/stores/settings-store';
|
||||||
import { useAuthStore } from '@/stores/auth-store';
|
import { useAuthStore } from '@/stores/auth-store';
|
||||||
import { useEmailStore } from '@/stores/email-store';
|
import { useEmailStore } from '@/stores/email-store';
|
||||||
@@ -47,6 +47,8 @@ export function EmailSettings() {
|
|||||||
emailAlwaysLightMode,
|
emailAlwaysLightMode,
|
||||||
archiveMode,
|
archiveMode,
|
||||||
hoverActions,
|
hoverActions,
|
||||||
|
hoverActionsMode,
|
||||||
|
hoverActionsCorner,
|
||||||
trustedSenders,
|
trustedSenders,
|
||||||
updateSetting,
|
updateSetting,
|
||||||
} = useSettingsStore();
|
} = useSettingsStore();
|
||||||
@@ -251,6 +253,52 @@ export function EmailSettings() {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Hover Actions Display Mode */}
|
||||||
|
<div className="pt-2 space-y-2">
|
||||||
|
<label className="text-xs font-medium text-foreground">{t('hover_actions.mode_label')}</label>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{(['inline', 'floating'] as const).map((mode) => (
|
||||||
|
<button
|
||||||
|
key={mode}
|
||||||
|
type="button"
|
||||||
|
onClick={() => updateSetting('hoverActionsMode', mode)}
|
||||||
|
className={cn(
|
||||||
|
'px-3 py-1.5 text-xs rounded-md transition-colors duration-150',
|
||||||
|
hoverActionsMode === mode
|
||||||
|
? 'bg-primary text-primary-foreground font-medium'
|
||||||
|
: 'bg-muted hover:bg-accent text-foreground'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{t(`hover_actions.mode_${mode}`)}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Corner Selection (only when floating) */}
|
||||||
|
{hoverActionsMode === 'floating' && (
|
||||||
|
<div className="pt-1 space-y-2">
|
||||||
|
<label className="text-xs font-medium text-foreground">{t('hover_actions.corner_label')}</label>
|
||||||
|
<div className="grid grid-cols-2 gap-2 w-48">
|
||||||
|
{(['top-left', 'top-right', 'bottom-left', 'bottom-right'] as const).map((corner) => (
|
||||||
|
<button
|
||||||
|
key={corner}
|
||||||
|
type="button"
|
||||||
|
onClick={() => updateSetting('hoverActionsCorner', corner)}
|
||||||
|
className={cn(
|
||||||
|
'px-2 py-1.5 text-xs rounded-md transition-colors duration-150 text-center',
|
||||||
|
hoverActionsCorner === corner
|
||||||
|
? 'bg-primary text-primary-foreground font-medium'
|
||||||
|
: 'bg-muted hover:bg-accent text-foreground'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{t(`hover_actions.corner_${corner}`)}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -861,7 +861,15 @@
|
|||||||
"archive": "Archivieren",
|
"archive": "Archivieren",
|
||||||
"tag": "Schlagwort",
|
"tag": "Schlagwort",
|
||||||
"spam": "Als Spam markieren",
|
"spam": "Als Spam markieren",
|
||||||
"none_selected": "Keine Aktionen ausgewählt"
|
"none_selected": "Keine Aktionen ausgewählt",
|
||||||
|
"mode_label": "Anzeigemodus",
|
||||||
|
"mode_inline": "Eingebettet",
|
||||||
|
"mode_floating": "Schwebend",
|
||||||
|
"corner_label": "Schwebende Position",
|
||||||
|
"corner_top-left": "Oben links",
|
||||||
|
"corner_top-right": "Oben rechts",
|
||||||
|
"corner_bottom-left": "Unten links",
|
||||||
|
"corner_bottom-right": "Unten rechts"
|
||||||
},
|
},
|
||||||
"default_mail_program": {
|
"default_mail_program": {
|
||||||
"label": "Standard-E-Mail-Programm",
|
"label": "Standard-E-Mail-Programm",
|
||||||
|
|||||||
@@ -861,7 +861,15 @@
|
|||||||
"archive": "Archive",
|
"archive": "Archive",
|
||||||
"tag": "Tag",
|
"tag": "Tag",
|
||||||
"spam": "Mark as Spam",
|
"spam": "Mark as Spam",
|
||||||
"none_selected": "No actions selected"
|
"none_selected": "No actions selected",
|
||||||
|
"mode_label": "Display Mode",
|
||||||
|
"mode_inline": "Inline",
|
||||||
|
"mode_floating": "Floating",
|
||||||
|
"corner_label": "Floating Position",
|
||||||
|
"corner_top-left": "Top Left",
|
||||||
|
"corner_top-right": "Top Right",
|
||||||
|
"corner_bottom-left": "Bottom Left",
|
||||||
|
"corner_bottom-right": "Bottom Right"
|
||||||
},
|
},
|
||||||
"default_mail_program": {
|
"default_mail_program": {
|
||||||
"label": "Default Mail Program",
|
"label": "Default Mail Program",
|
||||||
|
|||||||
@@ -861,7 +861,15 @@
|
|||||||
"archive": "Archivar",
|
"archive": "Archivar",
|
||||||
"tag": "Etiqueta",
|
"tag": "Etiqueta",
|
||||||
"spam": "Marcar como spam",
|
"spam": "Marcar como spam",
|
||||||
"none_selected": "No hay acciones seleccionadas"
|
"none_selected": "No hay acciones seleccionadas",
|
||||||
|
"mode_label": "Modo de visualización",
|
||||||
|
"mode_inline": "En línea",
|
||||||
|
"mode_floating": "Flotante",
|
||||||
|
"corner_label": "Posición flotante",
|
||||||
|
"corner_top-left": "Arriba izquierda",
|
||||||
|
"corner_top-right": "Arriba derecha",
|
||||||
|
"corner_bottom-left": "Abajo izquierda",
|
||||||
|
"corner_bottom-right": "Abajo derecha"
|
||||||
},
|
},
|
||||||
"default_mail_program": {
|
"default_mail_program": {
|
||||||
"label": "Programa de correo predeterminado",
|
"label": "Programa de correo predeterminado",
|
||||||
|
|||||||
@@ -861,7 +861,15 @@
|
|||||||
"archive": "Archiver",
|
"archive": "Archiver",
|
||||||
"tag": "Étiquette",
|
"tag": "Étiquette",
|
||||||
"spam": "Marquer comme spam",
|
"spam": "Marquer comme spam",
|
||||||
"none_selected": "Aucune action sélectionnée"
|
"none_selected": "Aucune action sélectionnée",
|
||||||
|
"mode_label": "Mode d'affichage",
|
||||||
|
"mode_inline": "En ligne",
|
||||||
|
"mode_floating": "Flottant",
|
||||||
|
"corner_label": "Position flottante",
|
||||||
|
"corner_top-left": "Haut gauche",
|
||||||
|
"corner_top-right": "Haut droit",
|
||||||
|
"corner_bottom-left": "Bas gauche",
|
||||||
|
"corner_bottom-right": "Bas droit"
|
||||||
},
|
},
|
||||||
"default_mail_program": {
|
"default_mail_program": {
|
||||||
"label": "Programme de messagerie par défaut",
|
"label": "Programme de messagerie par défaut",
|
||||||
|
|||||||
@@ -861,7 +861,15 @@
|
|||||||
"archive": "Archivia",
|
"archive": "Archivia",
|
||||||
"tag": "Etichetta",
|
"tag": "Etichetta",
|
||||||
"spam": "Segna come spam",
|
"spam": "Segna come spam",
|
||||||
"none_selected": "Nessuna azione selezionata"
|
"none_selected": "Nessuna azione selezionata",
|
||||||
|
"mode_label": "Modalità di visualizzazione",
|
||||||
|
"mode_inline": "In linea",
|
||||||
|
"mode_floating": "Fluttuante",
|
||||||
|
"corner_label": "Posizione fluttuante",
|
||||||
|
"corner_top-left": "In alto a sinistra",
|
||||||
|
"corner_top-right": "In alto a destra",
|
||||||
|
"corner_bottom-left": "In basso a sinistra",
|
||||||
|
"corner_bottom-right": "In basso a destra"
|
||||||
},
|
},
|
||||||
"default_mail_program": {
|
"default_mail_program": {
|
||||||
"label": "Programma di posta predefinito",
|
"label": "Programma di posta predefinito",
|
||||||
|
|||||||
@@ -861,7 +861,15 @@
|
|||||||
"archive": "アーカイブ",
|
"archive": "アーカイブ",
|
||||||
"tag": "タグ",
|
"tag": "タグ",
|
||||||
"spam": "スパムとしてマーク",
|
"spam": "スパムとしてマーク",
|
||||||
"none_selected": "アクションが選択されていません"
|
"none_selected": "アクションが選択されていません",
|
||||||
|
"mode_label": "表示モード",
|
||||||
|
"mode_inline": "インライン",
|
||||||
|
"mode_floating": "フローティング",
|
||||||
|
"corner_label": "フローティング位置",
|
||||||
|
"corner_top-left": "左上",
|
||||||
|
"corner_top-right": "右上",
|
||||||
|
"corner_bottom-left": "左下",
|
||||||
|
"corner_bottom-right": "右下"
|
||||||
},
|
},
|
||||||
"default_mail_program": {
|
"default_mail_program": {
|
||||||
"label": "既定のメールプログラム",
|
"label": "既定のメールプログラム",
|
||||||
|
|||||||
@@ -861,7 +861,15 @@
|
|||||||
"archive": "Archiveren",
|
"archive": "Archiveren",
|
||||||
"tag": "Label",
|
"tag": "Label",
|
||||||
"spam": "Markeer als spam",
|
"spam": "Markeer als spam",
|
||||||
"none_selected": "Geen acties geselecteerd"
|
"none_selected": "Geen acties geselecteerd",
|
||||||
|
"mode_label": "Weergavemodus",
|
||||||
|
"mode_inline": "Inline",
|
||||||
|
"mode_floating": "Zwevend",
|
||||||
|
"corner_label": "Zwevende positie",
|
||||||
|
"corner_top-left": "Linksboven",
|
||||||
|
"corner_top-right": "Rechtsboven",
|
||||||
|
"corner_bottom-left": "Linksonder",
|
||||||
|
"corner_bottom-right": "Rechtsonder"
|
||||||
},
|
},
|
||||||
"default_mail_program": {
|
"default_mail_program": {
|
||||||
"label": "Standaard e-mailprogramma",
|
"label": "Standaard e-mailprogramma",
|
||||||
|
|||||||
@@ -861,7 +861,15 @@
|
|||||||
"archive": "Arquivar",
|
"archive": "Arquivar",
|
||||||
"tag": "Etiqueta",
|
"tag": "Etiqueta",
|
||||||
"spam": "Marcar como spam",
|
"spam": "Marcar como spam",
|
||||||
"none_selected": "Nenhuma ação selecionada"
|
"none_selected": "Nenhuma ação selecionada",
|
||||||
|
"mode_label": "Modo de exibição",
|
||||||
|
"mode_inline": "Em linha",
|
||||||
|
"mode_floating": "Flutuante",
|
||||||
|
"corner_label": "Posição flutuante",
|
||||||
|
"corner_top-left": "Superior esquerdo",
|
||||||
|
"corner_top-right": "Superior direito",
|
||||||
|
"corner_bottom-left": "Inferior esquerdo",
|
||||||
|
"corner_bottom-right": "Inferior direito"
|
||||||
},
|
},
|
||||||
"default_mail_program": {
|
"default_mail_program": {
|
||||||
"label": "Programa de e-mail padrão",
|
"label": "Programa de e-mail padrão",
|
||||||
|
|||||||
@@ -861,7 +861,15 @@
|
|||||||
"archive": "Архивировать",
|
"archive": "Архивировать",
|
||||||
"tag": "Тег",
|
"tag": "Тег",
|
||||||
"spam": "Отметить как спам",
|
"spam": "Отметить как спам",
|
||||||
"none_selected": "Действия не выбраны"
|
"none_selected": "Действия не выбраны",
|
||||||
|
"mode_label": "Режим отображения",
|
||||||
|
"mode_inline": "Встроенный",
|
||||||
|
"mode_floating": "Плавающий",
|
||||||
|
"corner_label": "Позиция плавающих действий",
|
||||||
|
"corner_top-left": "Вверху слева",
|
||||||
|
"corner_top-right": "Вверху справа",
|
||||||
|
"corner_bottom-left": "Внизу слева",
|
||||||
|
"corner_bottom-right": "Внизу справа"
|
||||||
},
|
},
|
||||||
"default_mail_program": {
|
"default_mail_program": {
|
||||||
"label": "Почтовая программа по умолчанию",
|
"label": "Почтовая программа по умолчанию",
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ export type ToolbarPosition = 'top' | 'below-subject';
|
|||||||
export type ArchiveMode = 'single' | 'year' | 'month';
|
export type ArchiveMode = 'single' | 'year' | 'month';
|
||||||
|
|
||||||
export type HoverAction = 'delete' | 'star' | 'markRead' | 'archive' | 'tag' | 'spam';
|
export type HoverAction = 'delete' | 'star' | 'markRead' | 'archive' | 'tag' | 'spam';
|
||||||
|
export type HoverActionsMode = 'inline' | 'floating';
|
||||||
|
export type HoverActionsCorner = 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left';
|
||||||
|
|
||||||
export const ALL_HOVER_ACTIONS: { id: HoverAction; labelKey: string }[] = [
|
export const ALL_HOVER_ACTIONS: { id: HoverAction; labelKey: string }[] = [
|
||||||
{ id: 'delete', labelKey: 'delete' },
|
{ id: 'delete', labelKey: 'delete' },
|
||||||
@@ -110,6 +112,8 @@ interface SettingsState {
|
|||||||
emailAlwaysLightMode: boolean; // Always render email content in light mode
|
emailAlwaysLightMode: boolean; // Always render email content in light mode
|
||||||
archiveMode: ArchiveMode; // How to organize archived emails: single folder, by year, or by year+month
|
archiveMode: ArchiveMode; // How to organize archived emails: single folder, by year, or by year+month
|
||||||
hoverActions: HoverAction[]; // Quick actions shown on hover in mail list
|
hoverActions: HoverAction[]; // Quick actions shown on hover in mail list
|
||||||
|
hoverActionsMode: HoverActionsMode; // Display mode: inline (current) or floating corner
|
||||||
|
hoverActionsCorner: HoverActionsCorner; // Corner for floating mode
|
||||||
|
|
||||||
// Composer
|
// Composer
|
||||||
autoSaveDraftInterval: number; // milliseconds
|
autoSaveDraftInterval: number; // milliseconds
|
||||||
@@ -226,6 +230,8 @@ const DEFAULT_SETTINGS = {
|
|||||||
emailAlwaysLightMode: false,
|
emailAlwaysLightMode: false,
|
||||||
archiveMode: 'single' as ArchiveMode,
|
archiveMode: 'single' as ArchiveMode,
|
||||||
hoverActions: ['delete', 'star', 'markRead', 'archive'] as HoverAction[],
|
hoverActions: ['delete', 'star', 'markRead', 'archive'] as HoverAction[],
|
||||||
|
hoverActionsMode: 'inline' as HoverActionsMode,
|
||||||
|
hoverActionsCorner: 'top-right' as HoverActionsCorner,
|
||||||
|
|
||||||
// Composer
|
// Composer
|
||||||
autoSaveDraftInterval: 60000, // 1 minute
|
autoSaveDraftInterval: 60000, // 1 minute
|
||||||
@@ -331,6 +337,8 @@ export const useSettingsStore = create<SettingsState>()(
|
|||||||
attachmentPosition: state.attachmentPosition,
|
attachmentPosition: state.attachmentPosition,
|
||||||
archiveMode: state.archiveMode,
|
archiveMode: state.archiveMode,
|
||||||
hoverActions: state.hoverActions,
|
hoverActions: state.hoverActions,
|
||||||
|
hoverActionsMode: state.hoverActionsMode,
|
||||||
|
hoverActionsCorner: state.hoverActionsCorner,
|
||||||
disableThreading: state.disableThreading,
|
disableThreading: state.disableThreading,
|
||||||
trustedSenders: state.trustedSenders,
|
trustedSenders: state.trustedSenders,
|
||||||
autoSaveDraftInterval: state.autoSaveDraftInterval,
|
autoSaveDraftInterval: state.autoSaveDraftInterval,
|
||||||
|
|||||||
Reference in New Issue
Block a user