Files
ihasmail/web/src/views/settings/LabelsSettings.tsx
T
jcoffey-dev 645b8b510f ihasmail 2.0: rebuild as Stalwart-first JMAP webmail
Replace the FastAPI/HTMX prototype with a Node/Hono session proxy and a
React 19/Vite SPA. Mail (conversation view, search operators, labels,
sanitised HTML, privacy image proxy, invites, undo send, templates),
calendar (month/week/day/agenda, invites, free/busy, categories,
context menus), contacts (JSContact, groups, vCard), files, Sieve filter
builder (incl. filter-from-message with retroactive apply), vacation,
identities with default + Reply-To, PWA/mobile layout, push via SSE,
in-memory mock Stalwart for dev, Docker + CI.
2026-08-23 01:07:13 -07:00

45 lines
2.5 KiB
TypeScript

import { useState } from "react";
import { Plus, Trash2 } from "lucide-react";
import { useSettings } from "@/store/settings";
import { CALENDAR_COLORS, ColorSwatches } from "@/ui/misc";
import { promptDialog } from "@/ui/dialog";
export function LabelsSettings() {
const labels = useSettings((s) => s.settings.labels);
const update = useSettings((s) => s.update);
const [editing, setEditing] = useState<string | null>(null);
const add = async () => {
const name = await promptDialog({ title: "New label", placeholder: "Label name" });
if (!name?.trim()) return;
const keyword = name.trim().toLowerCase().replace(/[^a-z0-9_.-]+/g, "_").replace(/^_+|_+$/g, "") || `label${Date.now()}`;
if (labels.some((l) => l.keyword === keyword)) return;
update({ labels: [...labels, { keyword, name: name.trim(), color: CALENDAR_COLORS[labels.length % CALENDAR_COLORS.length]! }] });
};
return (
<div>
<h1>Labels</h1>
<p className="lead">Labels are IMAP keywords stored on your messages, so they sync to other clients. Names and colours are kept in this browser.</p>
{labels.map((l) => (
<div key={l.keyword} className="card">
<div className="card-head">
<span className="label-dot" style={{ background: l.color, width: 14, height: 14 }} />
{editing === l.keyword ? (
<input className="input sm" autoFocus defaultValue={l.name} onBlur={(e) => { update({ labels: labels.map((x) => (x.keyword === l.keyword ? { ...x, name: e.target.value || x.name } : x)) }); setEditing(null); }} onKeyDown={(e) => { if (e.key === "Enter") (e.target as HTMLInputElement).blur(); }} style={{ width: 240 }} />
) : (
<h3 style={{ cursor: "text" }} onClick={() => setEditing(l.keyword)}>{l.name} <span className="hint" style={{ fontWeight: 400 }}>({l.keyword})</span></h3>
)}
<button className="icon-btn sm danger" aria-label="Delete label" onClick={() => update({ labels: labels.filter((x) => x.keyword !== l.keyword) })}><Trash2 size={16} /></button>
</div>
<div style={{ marginTop: 8 }}>
<ColorSwatches value={l.color} onChange={(c) => update({ labels: labels.map((x) => (x.keyword === l.keyword ? { ...x, color: c } : x)) })} />
</div>
</div>
))}
<button className="btn" onClick={() => void add()}><Plus size={16} /> New label</button>
<p className="hint mt-8">Tip: press <kbd className="kbd">l</kbd> on a conversation to apply labels. Search with <code>label:name</code>.</p>
</div>
);
}