void;
}
-// Color tag mapping - using lighter backgrounds for better readability
-const colorTags = {
- red: "bg-red-50 dark:bg-red-950/30",
- orange: "bg-orange-50 dark:bg-orange-950/30",
- yellow: "bg-yellow-50 dark:bg-yellow-950/30",
- green: "bg-green-50 dark:bg-green-950/30",
- blue: "bg-blue-50 dark:bg-blue-950/30",
- purple: "bg-purple-50 dark:bg-purple-950/30",
- pink: "bg-pink-50 dark:bg-pink-950/30",
-} as const;
-
-const getEmailColor = (keywords: Record
| undefined) => {
- if (!keywords) return null;
- for (const key of Object.keys(keywords)) {
- if (key.startsWith("$color:") && keywords[key] === true) {
- const color = key.replace("$color:", "");
- return colorTags[color as keyof typeof colorTags] || null;
- }
- }
- return null;
-};
-
export function EmailListItem({ email, selected, onClick, onContextMenu }: EmailListItemProps) {
const t = useTranslations('email_viewer');
const { selectedEmailIds, toggleEmailSelection, selectRangeEmails, selectedMailbox } = useEmailStore();
const showPreview = useSettingsStore((state) => state.showPreview);
+ const emailKeywords = useSettingsStore((state) => state.emailKeywords);
const { identities } = useAuthStore();
const isChecked = selectedEmailIds.has(email.id);
const isUnread = !email.keywords?.$seen;
const isStarred = email.keywords?.$flagged;
const isImportant = email.keywords?.["$important"];
const sender = email.from?.[0];
- const colorTag = getEmailColor(email.keywords);
+
+ // Resolve color tag using keyword definitions from settings
+ const colorTagId = getEmailColorTag(email.keywords);
+ const keywordDef = colorTagId ? emailKeywords.find(k => k.id === colorTagId) : null;
+ const colorTag = keywordDef ? KEYWORD_PALETTE[keywordDef.color]?.bg ?? null : null;
// Drag and drop functionality
const { dragHandlers, isDragging } = useEmailDrag({
diff --git a/components/email/email-viewer.tsx b/components/email/email-viewer.tsx
index 2e409274..e7c2cf89 100644
--- a/components/email/email-viewer.tsx
+++ b/components/email/email-viewer.tsx
@@ -50,7 +50,7 @@ import {
Keyboard,
} from "lucide-react";
import { useTranslations } from "next-intl";
-import { useSettingsStore } from "@/stores/settings-store";
+import { useSettingsStore, KEYWORD_PALETTE } from "@/stores/settings-store";
import { useUIStore } from "@/stores/ui-store";
import { useDeviceDetection } from "@/hooks/use-media-query";
import { useAuthStore } from "@/stores/auth-store";
@@ -113,8 +113,10 @@ const getFileIcon = (name?: string, type?: string) => {
const getCurrentColor = (keywords: Record | undefined) => {
if (!keywords) return null;
for (const key of Object.keys(keywords)) {
- if (key.startsWith("$color:") && keywords[key] === true) {
- return key.replace("$color:", "");
+ if ((key.startsWith("$label:") || key.startsWith("$color:")) && keywords[key] === true) {
+ return key.startsWith("$label:")
+ ? key.slice("$label:".length)
+ : key.slice("$color:".length);
}
}
return null;
@@ -184,20 +186,17 @@ export function EmailViewer({
const externalContentPolicy = useSettingsStore((state) => state.externalContentPolicy);
const addTrustedSender = useSettingsStore((state) => state.addTrustedSender);
const isSenderTrusted = useSettingsStore((state) => state.isSenderTrusted);
+ const emailKeywords = useSettingsStore((state) => state.emailKeywords);
// Detect if current mailbox is Junk folder
const isInJunkFolder = currentMailboxRole === 'junk';
- // Color options for email tags (using translations)
- const colorOptions = [
- { name: t("color_tag.red"), value: "red", color: "bg-red-500" },
- { name: t("color_tag.orange"), value: "orange", color: "bg-orange-500" },
- { name: t("color_tag.yellow"), value: "yellow", color: "bg-yellow-500" },
- { name: t("color_tag.green"), value: "green", color: "bg-green-500" },
- { name: t("color_tag.blue"), value: "blue", color: "bg-blue-500" },
- { name: t("color_tag.purple"), value: "purple", color: "bg-purple-500" },
- { name: t("color_tag.pink"), value: "pink", color: "bg-pink-500" },
- ];
+ // Color options for email tags (from user-defined keyword settings)
+ const colorOptions = emailKeywords.map((kw) => ({
+ name: kw.label,
+ value: kw.id,
+ color: KEYWORD_PALETTE[kw.color]?.dot || 'bg-gray-500',
+ }));
// Tablet list visibility
const { isTablet } = useDeviceDetection();
@@ -804,17 +803,13 @@ export function EmailViewer({
className="h-8 w-8 rounded hover:bg-muted flex items-center justify-center"
title={t('set_color')}
>
-
+ {(() => {
+ const kw = currentColor ? emailKeywords.find(k => k.id === currentColor) : null;
+ const dotClass = kw ? KEYWORD_PALETTE[kw.color]?.dot : null;
+ return dotClass
+ ?
+ : ;
+ })()}
{/* Colors appear on hover */}
diff --git a/components/email/thread-list-item.tsx b/components/email/thread-list-item.tsx
index d5ee48e7..a504f118 100644
--- a/components/email/thread-list-item.tsx
+++ b/components/email/thread-list-item.tsx
@@ -6,10 +6,10 @@ import { Email, ThreadGroup } from "@/lib/jmap/types";
import { cn } from "@/lib/utils";
import { Avatar } from "@/components/ui/avatar";
import { Paperclip, Star, Circle, ChevronRight, ChevronDown, Loader2, MessageSquare } from "lucide-react";
-import { useSettingsStore } from "@/stores/settings-store";
+import { useSettingsStore, KEYWORD_PALETTE } from "@/stores/settings-store";
import { useUIStore } from "@/stores/ui-store";
import { useEmailStore } from "@/stores/email-store";
-import { getThreadColorTag } from "@/lib/thread-utils";
+import { getThreadColorTag, getEmailColorTag } from "@/lib/thread-utils";
import { useEmailDrag } from "@/hooks/use-email-drag";
import { ThreadEmailItem } from "./thread-email-item";
import { useTranslations } from "next-intl";
@@ -26,16 +26,6 @@ interface ThreadListItemProps {
onOpenConversation?: (thread: ThreadGroup) => void;
}
-const colorTags = {
- red: "bg-red-50 dark:bg-red-950/30",
- orange: "bg-orange-50 dark:bg-orange-950/30",
- yellow: "bg-yellow-50 dark:bg-yellow-950/30",
- green: "bg-green-50 dark:bg-green-950/30",
- blue: "bg-blue-50 dark:bg-blue-950/30",
- purple: "bg-purple-50 dark:bg-purple-950/30",
- pink: "bg-pink-50 dark:bg-pink-950/30",
-} as const;
-
interface SingleEmailItemProps {
email: Email;
selected: boolean;
@@ -51,8 +41,17 @@ const SingleEmailItem = React.forwardRef(
const isStarred = email.keywords?.$flagged;
const sender = email.from?.[0];
const { selectedMailbox, selectedEmailIds, toggleEmailSelection, selectRangeEmails } = useEmailStore();
+ const emailKeywords = useSettingsStore((state) => state.emailKeywords);
const isChecked = selectedEmailIds.has(email.id);
+ // Resolve color from keyword definitions if not passed directly
+ const resolvedColorTag = (() => {
+ if (colorTag) return colorTag;
+ const tagId = getEmailColorTag(email.keywords);
+ const kw = tagId ? emailKeywords.find(k => k.id === tagId) : null;
+ return kw ? KEYWORD_PALETTE[kw.color]?.bg ?? null : null;
+ })();
+
const { dragHandlers, isDragging } = useEmailDrag({
email,
sourceMailboxId: selectedMailbox,
@@ -80,15 +79,15 @@ const SingleEmailItem = React.forwardRef(
{...dragHandlers}
className={cn(
"relative group cursor-pointer transition-all duration-200 border-b border-border",
- colorTag ? colorTag : (
+ resolvedColorTag ? resolvedColorTag : (
selected
? "bg-accent"
: "bg-background"
),
- selected && !colorTag && "shadow-sm",
- !colorTag && !selected && "hover:bg-muted hover:shadow-sm",
- colorTag && "hover:brightness-95 dark:hover:brightness-110",
- isUnread && !colorTag && "bg-accent/30",
+ selected && !resolvedColorTag && "shadow-sm",
+ !resolvedColorTag && !selected && "hover:bg-muted hover:shadow-sm",
+ resolvedColorTag && "hover:brightness-95 dark:hover:brightness-110",
+ isUnread && !resolvedColorTag && "bg-accent/30",
isChecked && "ring-2 ring-primary/20 bg-accent/40",
isDragging && "opacity-50 scale-[0.98] ring-2 ring-primary/30"
)}
@@ -192,7 +191,9 @@ export const ThreadListItem = React.forwardRef state.emailKeywords);
+ const keywordDef = threadColor ? emailKeywordDefs.find(k => k.id === threadColor) : null;
+ const colorTag = keywordDef ? KEYWORD_PALETTE[keywordDef.color]?.bg ?? null : null;
const isSelected = selectedEmailId === latestEmail.id ||
thread.emails.some(e => e.id === selectedEmailId);
diff --git a/components/settings/keyword-settings.tsx b/components/settings/keyword-settings.tsx
new file mode 100644
index 00000000..e9b27827
--- /dev/null
+++ b/components/settings/keyword-settings.tsx
@@ -0,0 +1,244 @@
+"use client";
+
+import { useState } from "react";
+import { useTranslations } from "next-intl";
+import { useSettingsStore, KEYWORD_PALETTE, DEFAULT_KEYWORDS, type KeywordDefinition } from "@/stores/settings-store";
+import { SettingsSection } from "./settings-section";
+import { Plus, Pencil, Trash2, GripVertical, Check, X, RotateCcw } from "lucide-react";
+import { cn } from "@/lib/utils";
+
+const PALETTE_KEYS = Object.keys(KEYWORD_PALETTE);
+
+function KeywordColorPicker({
+ value,
+ onChange,
+}: {
+ value: string;
+ onChange: (color: string) => void;
+}) {
+ return (
+
+ {PALETTE_KEYS.map((colorKey) => (
+
+ );
+}
+
+function KeywordRow({
+ keyword,
+ onEdit,
+ onDelete,
+}: {
+ keyword: KeywordDefinition;
+ onEdit: () => void;
+ onDelete: () => void;
+}) {
+ const t = useTranslations("settings.keywords");
+ const palette = KEYWORD_PALETTE[keyword.color];
+
+ return (
+
+
+
+
{keyword.label}
+
{"$label:" + keyword.id}
+
+
+ );
+}
+
+function KeywordEditForm({
+ initial,
+ existingIds,
+ onSave,
+ onCancel,
+}: {
+ initial?: KeywordDefinition;
+ existingIds: string[];
+ onSave: (keyword: KeywordDefinition) => void;
+ onCancel: () => void;
+}) {
+ const t = useTranslations("settings.keywords");
+ const [label, setLabel] = useState(initial?.label || "");
+ const [color, setColor] = useState(initial?.color || "blue");
+ const isEditing = !!initial;
+
+ const normalizedId = isEditing
+ ? initial.id
+ : label
+ .trim()
+ .toLowerCase()
+ .replace(/[^a-z0-9_-]/g, "-")
+ .replace(/-+/g, "-")
+ .replace(/^-|-$/g, "");
+
+ const isDuplicate = !isEditing && normalizedId.length > 0 && existingIds.includes(normalizedId);
+ const isValid = normalizedId.length > 0 && label.trim().length > 0 && !isDuplicate;
+
+ const handleSave = () => {
+ if (!isValid) return;
+ onSave({ id: normalizedId, label: label.trim(), color });
+ };
+
+ return (
+
+
+
+
setLabel(e.target.value)}
+ className="w-full px-2.5 py-1.5 text-sm rounded-md border border-border bg-background focus:outline-none focus:ring-2 focus:ring-ring"
+ placeholder={t("label_placeholder")}
+ autoFocus
+ maxLength={30}
+ onKeyDown={(e) => e.key === "Enter" && handleSave()}
+ />
+ {isDuplicate && (
+
{t("id_exists")}
+ )}
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export function KeywordSettings() {
+ const t = useTranslations("settings.keywords");
+ const { emailKeywords, addKeyword, updateKeyword, removeKeyword, reorderKeywords } =
+ useSettingsStore();
+ const [editingId, setEditingId] = useState(null);
+ const [isAdding, setIsAdding] = useState(false);
+
+ const existingIds = emailKeywords.map((k) => k.id);
+
+ const handleAdd = (keyword: KeywordDefinition) => {
+ addKeyword(keyword);
+ setIsAdding(false);
+ };
+
+ const handleEdit = (keyword: KeywordDefinition) => {
+ updateKeyword(keyword.id, { label: keyword.label, color: keyword.color });
+ setEditingId(null);
+ };
+
+ const handleDelete = (id: string) => {
+ removeKeyword(id);
+ };
+
+ const handleResetDefaults = () => {
+ reorderKeywords(DEFAULT_KEYWORDS);
+ };
+
+ return (
+
+
+ {emailKeywords.map((keyword) =>
+ editingId === keyword.id ? (
+
id !== keyword.id)}
+ onSave={handleEdit}
+ onCancel={() => setEditingId(null)}
+ />
+ ) : (
+ {
+ setEditingId(keyword.id);
+ setIsAdding(false);
+ }}
+ onDelete={() => handleDelete(keyword.id)}
+ />
+ )
+ )}
+
+ {isAdding ? (
+ setIsAdding(false)}
+ />
+ ) : (
+
+
+
+
+ )}
+
+
+ );
+}
diff --git a/lib/__tests__/thread-utils.test.ts b/lib/__tests__/thread-utils.test.ts
index f27931c5..2c9c7dfd 100644
--- a/lib/__tests__/thread-utils.test.ts
+++ b/lib/__tests__/thread-utils.test.ts
@@ -210,7 +210,11 @@ describe('mergeThreadEmails', () => {
});
describe('getEmailColorTag', () => {
- it('returns color from $color: keyword', () => {
+ it('returns label from $label: keyword', () => {
+ expect(getEmailColorTag({ '$label:red': true, $seen: true })).toBe('red');
+ });
+
+ it('returns label from legacy $color: keyword', () => {
expect(getEmailColorTag({ '$color:red': true, $seen: true })).toBe('red');
});
@@ -227,7 +231,7 @@ describe('getThreadColorTag', () => {
it('returns first color found across thread emails', () => {
const emails = [
makeEmail({ id: 'e1', keywords: { $seen: true } }),
- makeEmail({ id: 'e2', keywords: { '$color:blue': true } }),
+ makeEmail({ id: 'e2', keywords: { '$label:blue': true } }),
];
expect(getThreadColorTag(emails)).toBe('blue');
});
diff --git a/lib/thread-utils.ts b/lib/thread-utils.ts
index ebcde2e8..18681278 100644
--- a/lib/thread-utils.ts
+++ b/lib/thread-utils.ts
@@ -136,15 +136,23 @@ export function mergeThreadEmails(
};
}
+/** Active prefix for new keyword tags written to JMAP */
+export const KEYWORD_PREFIX = "$label:";
+/** Legacy prefix still recognised when reading */
+export const KEYWORD_PREFIX_LEGACY = "$color:";
+
/**
- * Gets color tag from email keywords (if any).
+ * Gets label/color tag from email keywords (if any).
+ * Reads both the current $label: prefix and the legacy $color: prefix.
*/
export function getEmailColorTag(keywords: Record | undefined): string | null {
if (!keywords) return null;
for (const key of Object.keys(keywords)) {
- if (key.startsWith("$color:") && keywords[key] === true) {
- return key.replace("$color:", "");
+ if ((key.startsWith(KEYWORD_PREFIX) || key.startsWith(KEYWORD_PREFIX_LEGACY)) && keywords[key] === true) {
+ return key.startsWith(KEYWORD_PREFIX)
+ ? key.slice(KEYWORD_PREFIX.length)
+ : key.slice(KEYWORD_PREFIX_LEGACY.length);
}
}
diff --git a/locales/de/common.json b/locales/de/common.json
index f043e9c2..d1b4748f 100644
--- a/locales/de/common.json
+++ b/locales/de/common.json
@@ -447,7 +447,8 @@
"calendar": "Kalender",
"filters": "Filter",
"templates": "Vorlagen",
- "folders": "Ordner"
+ "folders": "Ordner",
+ "keywords": "Schlüsselwörter"
},
"tab_groups": {
"general": "Allgemein",
@@ -487,6 +488,23 @@
"description": "Weiche Übergänge und Effekte anzeigen"
}
},
+ "keywords": {
+ "title": "E-Mail-Schlüsselwörter",
+ "description": "Definieren Sie Schlüsselwörter (Labels/Tags) zum Organisieren Ihrer E-Mails mit Farben.",
+ "add_keyword": "Schlüsselwort hinzufügen",
+ "reset_defaults": "Auf Standard zurücksetzen",
+ "label_field": "Anzeigename",
+ "label_placeholder": "z.B. Arbeit, Privat, Dringend",
+ "id_field": "Schlüsselwort-ID",
+ "id_placeholder": "z.B. arbeit, privat",
+ "color_field": "Farbe",
+ "id_exists": "Diese Schlüsselwort-ID existiert bereits",
+ "edit": "Schlüsselwort bearbeiten",
+ "delete": "Schlüsselwort löschen",
+ "save": "Speichern",
+ "add": "Hinzufügen",
+ "cancel": "Abbrechen"
+ },
"language_region": {
"title": "Sprache & Region",
"description": "Konfigurieren Sie Sprach- und Regionaleinstellungen",
diff --git a/locales/en/common.json b/locales/en/common.json
index a346d9a8..d7c7b7d7 100644
--- a/locales/en/common.json
+++ b/locales/en/common.json
@@ -459,7 +459,8 @@
"calendar": "Calendar",
"filters": "Filters",
"templates": "Templates",
- "folders": "Folders"
+ "folders": "Folders",
+ "keywords": "Keywords"
},
"tab_groups": {
"general": "General",
@@ -499,6 +500,23 @@
"description": "Show smooth transitions and effects"
}
},
+ "keywords": {
+ "title": "Email Keywords",
+ "description": "Define keywords (labels/tags) to organize your emails with colors. These are stored as JMAP keywords on the server.",
+ "add_keyword": "Add Keyword",
+ "reset_defaults": "Reset to Defaults",
+ "label_field": "Display Name",
+ "label_placeholder": "e.g. Work, Personal, Urgent",
+ "id_field": "Keyword ID",
+ "id_placeholder": "e.g. work, personal",
+ "color_field": "Color",
+ "id_exists": "This keyword ID already exists",
+ "edit": "Edit keyword",
+ "delete": "Delete keyword",
+ "save": "Save",
+ "add": "Add",
+ "cancel": "Cancel"
+ },
"language_region": {
"title": "Language & Region",
"description": "Configure language and regional preferences",
diff --git a/locales/es/common.json b/locales/es/common.json
index ec7368f2..15dbe8b7 100644
--- a/locales/es/common.json
+++ b/locales/es/common.json
@@ -447,7 +447,8 @@
"calendar": "Calendario",
"filters": "Filtros",
"templates": "Plantillas",
- "folders": "Carpetas"
+ "folders": "Carpetas",
+ "keywords": "Palabras clave"
},
"tab_groups": {
"general": "General",
@@ -487,6 +488,23 @@
"description": "Mostrar transiciones y efectos suaves"
}
},
+ "keywords": {
+ "title": "Palabras clave de correo",
+ "description": "Define palabras clave (etiquetas) para organizar tus correos con colores.",
+ "add_keyword": "Añadir palabra clave",
+ "reset_defaults": "Restablecer valores predeterminados",
+ "label_field": "Nombre para mostrar",
+ "label_placeholder": "ej. Trabajo, Personal, Urgente",
+ "id_field": "ID de palabra clave",
+ "id_placeholder": "ej. trabajo, personal",
+ "color_field": "Color",
+ "id_exists": "Esta ID de palabra clave ya existe",
+ "edit": "Editar palabra clave",
+ "delete": "Eliminar palabra clave",
+ "save": "Guardar",
+ "add": "Añadir",
+ "cancel": "Cancelar"
+ },
"language_region": {
"title": "Idioma y Región",
"description": "Configure las preferencias de idioma y región",
diff --git a/locales/fr/common.json b/locales/fr/common.json
index 0c0d02b5..9f9f5400 100644
--- a/locales/fr/common.json
+++ b/locales/fr/common.json
@@ -447,7 +447,8 @@
"calendar": "Calendrier",
"filters": "Filtres",
"templates": "Modèles",
- "folders": "Dossiers"
+ "folders": "Dossiers",
+ "keywords": "Mots-clés"
},
"tab_groups": {
"general": "Général",
@@ -487,6 +488,23 @@
"description": "Afficher les transitions et effets fluides"
}
},
+ "keywords": {
+ "title": "Mots-clés des e-mails",
+ "description": "Définissez des mots-clés (étiquettes) pour organiser vos e-mails avec des couleurs.",
+ "add_keyword": "Ajouter un mot-clé",
+ "reset_defaults": "Réinitialiser par défaut",
+ "label_field": "Nom d'affichage",
+ "label_placeholder": "ex. Travail, Personnel, Urgent",
+ "id_field": "ID du mot-clé",
+ "id_placeholder": "ex. travail, personnel",
+ "color_field": "Couleur",
+ "id_exists": "Cet ID de mot-clé existe déjà",
+ "edit": "Modifier le mot-clé",
+ "delete": "Supprimer le mot-clé",
+ "save": "Enregistrer",
+ "add": "Ajouter",
+ "cancel": "Annuler"
+ },
"language_region": {
"title": "Langue et région",
"description": "Configurez vos préférences linguistiques et régionales",
diff --git a/locales/it/common.json b/locales/it/common.json
index a22ca085..c263c165 100644
--- a/locales/it/common.json
+++ b/locales/it/common.json
@@ -447,7 +447,8 @@
"calendar": "Calendario",
"filters": "Filtri",
"templates": "Modelli",
- "folders": "Cartelle"
+ "folders": "Cartelle",
+ "keywords": "Parole chiave"
},
"tab_groups": {
"general": "Generale",
@@ -487,6 +488,23 @@
"description": "Mostra transizioni ed effetti fluidi"
}
},
+ "keywords": {
+ "title": "Parole chiave e-mail",
+ "description": "Definisci parole chiave (etichette) per organizzare le tue e-mail con colori.",
+ "add_keyword": "Aggiungi parola chiave",
+ "reset_defaults": "Ripristina predefiniti",
+ "label_field": "Nome visualizzato",
+ "label_placeholder": "es. Lavoro, Personale, Urgente",
+ "id_field": "ID parola chiave",
+ "id_placeholder": "es. lavoro, personale",
+ "color_field": "Colore",
+ "id_exists": "Questo ID parola chiave esiste già",
+ "edit": "Modifica parola chiave",
+ "delete": "Elimina parola chiave",
+ "save": "Salva",
+ "add": "Aggiungi",
+ "cancel": "Annulla"
+ },
"language_region": {
"title": "Lingua e regione",
"description": "Configura le preferenze di lingua e regionali",
diff --git a/locales/ja/common.json b/locales/ja/common.json
index 1e0cab76..cefe324e 100644
--- a/locales/ja/common.json
+++ b/locales/ja/common.json
@@ -447,7 +447,8 @@
"calendar": "カレンダー",
"filters": "フィルター",
"templates": "テンプレート",
- "folders": "フォルダー"
+ "folders": "フォルダー",
+ "keywords": "キーワード"
},
"tab_groups": {
"general": "一般",
@@ -487,6 +488,23 @@
"description": "スムーズな遷移とエフェクトを表示"
}
},
+ "keywords": {
+ "title": "メールキーワード",
+ "description": "色でメールを整理するためのキーワード(ラベル/タグ)を定義します。",
+ "add_keyword": "キーワードを追加",
+ "reset_defaults": "デフォルトに戻す",
+ "label_field": "表示名",
+ "label_placeholder": "例:仕事、個人、緊急",
+ "id_field": "キーワードID",
+ "id_placeholder": "例:work、personal",
+ "color_field": "色",
+ "id_exists": "このキーワードIDは既に存在します",
+ "edit": "キーワードを編集",
+ "delete": "キーワードを削除",
+ "save": "保存",
+ "add": "追加",
+ "cancel": "キャンセル"
+ },
"language_region": {
"title": "言語と地域",
"description": "言語と地域の設定を構成",
diff --git a/locales/nl/common.json b/locales/nl/common.json
index c9b0fc1f..d40ec70d 100644
--- a/locales/nl/common.json
+++ b/locales/nl/common.json
@@ -447,7 +447,8 @@
"calendar": "Agenda",
"filters": "Filters",
"templates": "Sjablonen",
- "folders": "Mappen"
+ "folders": "Mappen",
+ "keywords": "Sleutelwoorden"
},
"tab_groups": {
"general": "Algemeen",
@@ -487,6 +488,23 @@
"description": "Vloeiende overgangen en effecten tonen"
}
},
+ "keywords": {
+ "title": "E-mail trefwoorden",
+ "description": "Definieer trefwoorden (labels/tags) om uw e-mails met kleuren te organiseren.",
+ "add_keyword": "Trefwoord toevoegen",
+ "reset_defaults": "Standaardwaarden herstellen",
+ "label_field": "Weergavenaam",
+ "label_placeholder": "bijv. Werk, Persoonlijk, Urgent",
+ "id_field": "Trefwoord-ID",
+ "id_placeholder": "bijv. werk, persoonlijk",
+ "color_field": "Kleur",
+ "id_exists": "Dit trefwoord-ID bestaat al",
+ "edit": "Trefwoord bewerken",
+ "delete": "Trefwoord verwijderen",
+ "save": "Opslaan",
+ "add": "Toevoegen",
+ "cancel": "Annuleren"
+ },
"language_region": {
"title": "Taal & Regio",
"description": "Configureer taal- en regiovoorkeuren",
diff --git a/locales/pt/common.json b/locales/pt/common.json
index 9431fd59..b4086fe3 100644
--- a/locales/pt/common.json
+++ b/locales/pt/common.json
@@ -447,7 +447,8 @@
"calendar": "Calendário",
"filters": "Filtros",
"templates": "Modelos",
- "folders": "Pastas"
+ "folders": "Pastas",
+ "keywords": "Palavras-chave"
},
"tab_groups": {
"general": "Geral",
@@ -487,6 +488,23 @@
"description": "Mostrar transições e efeitos suaves"
}
},
+ "keywords": {
+ "title": "Palavras-chave de e-mail",
+ "description": "Defina palavras-chave (rótulos/tags) para organizar seus e-mails com cores.",
+ "add_keyword": "Adicionar palavra-chave",
+ "reset_defaults": "Restaurar padrões",
+ "label_field": "Nome de exibição",
+ "label_placeholder": "ex. Trabalho, Pessoal, Urgente",
+ "id_field": "ID da palavra-chave",
+ "id_placeholder": "ex. trabalho, pessoal",
+ "color_field": "Cor",
+ "id_exists": "Este ID de palavra-chave já existe",
+ "edit": "Editar palavra-chave",
+ "delete": "Excluir palavra-chave",
+ "save": "Salvar",
+ "add": "Adicionar",
+ "cancel": "Cancelar"
+ },
"language_region": {
"title": "Idioma e Região",
"description": "Configure preferências de idioma e região",
diff --git a/stores/settings-store.ts b/stores/settings-store.ts
index 01a91668..24dcaba4 100644
--- a/stores/settings-store.ts
+++ b/stores/settings-store.ts
@@ -27,6 +27,39 @@ export type TimeFormat = '12h' | '24h';
export type FirstDayOfWeek = 0 | 1; // 0 = Sunday, 1 = Monday
export type ExternalContentPolicy = 'ask' | 'block' | 'allow';
+export interface KeywordDefinition {
+ id: string; // Used as JMAP keyword suffix: $label:
+ label: string; // Display name
+ color: string; // Key from KEYWORD_PALETTE
+}
+
+// Available color palette for keywords
+export const KEYWORD_PALETTE: Record = {
+ red: { dot: 'bg-red-500', bg: 'bg-red-50 dark:bg-red-950/30' },
+ orange: { dot: 'bg-orange-500', bg: 'bg-orange-50 dark:bg-orange-950/30' },
+ yellow: { dot: 'bg-yellow-500', bg: 'bg-yellow-50 dark:bg-yellow-950/30' },
+ green: { dot: 'bg-green-500', bg: 'bg-green-50 dark:bg-green-950/30' },
+ blue: { dot: 'bg-blue-500', bg: 'bg-blue-50 dark:bg-blue-950/30' },
+ purple: { dot: 'bg-purple-500', bg: 'bg-purple-50 dark:bg-purple-950/30' },
+ pink: { dot: 'bg-pink-500', bg: 'bg-pink-50 dark:bg-pink-950/30' },
+ teal: { dot: 'bg-teal-500', bg: 'bg-teal-50 dark:bg-teal-950/30' },
+ cyan: { dot: 'bg-cyan-500', bg: 'bg-cyan-50 dark:bg-cyan-950/30' },
+ indigo: { dot: 'bg-indigo-500', bg: 'bg-indigo-50 dark:bg-indigo-950/30' },
+ amber: { dot: 'bg-amber-500', bg: 'bg-amber-50 dark:bg-amber-950/30' },
+ lime: { dot: 'bg-lime-500', bg: 'bg-lime-50 dark:bg-lime-950/30' },
+ gray: { dot: 'bg-gray-500', bg: 'bg-gray-50 dark:bg-gray-950/30' },
+} as const;
+
+export const DEFAULT_KEYWORDS: KeywordDefinition[] = [
+ { id: 'red', label: 'Red', color: 'red' },
+ { id: 'orange', label: 'Orange', color: 'orange' },
+ { id: 'yellow', label: 'Yellow', color: 'yellow' },
+ { id: 'green', label: 'Green', color: 'green' },
+ { id: 'blue', label: 'Blue', color: 'blue' },
+ { id: 'purple', label: 'Purple', color: 'purple' },
+ { id: 'pink', label: 'Pink', color: 'pink' },
+];
+
interface SettingsState {
// Appearance
fontSize: FontSize;
@@ -64,6 +97,9 @@ interface SettingsState {
// Folders
folderIcons: Record; // mailboxId -> icon name
+ // Keywords (labels/tags)
+ emailKeywords: KeywordDefinition[];
+
// Advanced
debugMode: boolean;
settingsSyncDisabled: boolean;
@@ -86,6 +122,13 @@ interface SettingsState {
removeTrustedSender: (email: string) => void;
isSenderTrusted: (email: string) => boolean;
+ // Keywords
+ addKeyword: (keyword: KeywordDefinition) => void;
+ updateKeyword: (id: string, updates: Partial>) => void;
+ removeKeyword: (id: string) => void;
+ reorderKeywords: (keywords: KeywordDefinition[]) => void;
+ getKeywordById: (id: string) => KeywordDefinition | undefined;
+
// Settings sync
enableSync: (username: string, serverUrl: string) => void;
disableSync: () => void;
@@ -129,6 +172,9 @@ const DEFAULT_SETTINGS = {
// Folders
folderIcons: {} as Record,
+ // Keywords
+ emailKeywords: DEFAULT_KEYWORDS,
+
// Advanced
debugMode: false,
settingsSyncDisabled: false,
@@ -188,6 +234,7 @@ export const useSettingsStore = create()(
calendarNotificationSound: state.calendarNotificationSound,
senderFavicons: state.senderFavicons,
folderIcons: state.folderIcons,
+ emailKeywords: state.emailKeywords,
debugMode: state.debugMode,
settingsSyncDisabled: state.settingsSyncDisabled,
// Cross-store settings
@@ -264,6 +311,33 @@ export const useSettingsStore = create()(
return get().trustedSenders.includes(normalizedEmail);
},
+ // Keyword methods
+ addKeyword: (keyword: KeywordDefinition) => {
+ const current = get().emailKeywords;
+ if (current.some(k => k.id === keyword.id)) return;
+ set({ emailKeywords: [...current, keyword] });
+ },
+
+ updateKeyword: (id: string, updates: Partial>) => {
+ set({
+ emailKeywords: get().emailKeywords.map(k =>
+ k.id === id ? { ...k, ...updates } : k
+ ),
+ });
+ },
+
+ removeKeyword: (id: string) => {
+ set({ emailKeywords: get().emailKeywords.filter(k => k.id !== id) });
+ },
+
+ reorderKeywords: (keywords: KeywordDefinition[]) => {
+ set({ emailKeywords: keywords });
+ },
+
+ getKeywordById: (id: string) => {
+ return get().emailKeywords.find(k => k.id === id);
+ },
+
// Settings sync methods
enableSync: (username: string, serverUrl: string) => {
syncUsername = username;
diff --git a/tailwind.config.ts b/tailwind.config.ts
index d4e90b64..36f2f4e7 100644
--- a/tailwind.config.ts
+++ b/tailwind.config.ts
@@ -6,6 +6,7 @@ export default {
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
"./components/**/*.{js,ts,jsx,tsx,mdx}",
"./app/**/*.{js,ts,jsx,tsx,mdx}",
+ "./stores/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {