Right-click a folder and pick one of the twelve colours the calendar already uses, or clear it again. The colour tints the folder's icon; the label keeps the sidebar's own contrast, which a dozen arbitrary colours would not reliably give it. Kept by mailbox id rather than by name, so a folder renamed or dragged somewhere else keeps its colour. Stored in settings, which live in this browser -- JMAP has nowhere on a Mailbox to put a colour, and every other colour in the app, labels and event categories included, already works this way. Worth knowing it does not follow you to another device. The cascade needed care: .nav-item svg sets the colour on the icon itself, so a colour inherited from a wrapper does nothing. Checking getComputedStyle on the wrapper said the icon was purple while the pixels stayed grey; the rule now targets the svg, and the check now reads the pixels.
53 lines
2.0 KiB
TypeScript
53 lines
2.0 KiB
TypeScript
import type { Id, Mailbox } from "@/jmap/types";
|
|
|
|
/**
|
|
* A folder can be moved unless the server gave it a role. Inbox, Sent, Trash and
|
|
* the rest are structural, and the server refuses to reparent them anyway --
|
|
* better not to offer the drag at all.
|
|
*/
|
|
export function movable(m: Mailbox): boolean {
|
|
return !m.role || m.role === "subscribed";
|
|
}
|
|
|
|
/** Every folder beneath this one, so a folder cannot be dropped inside itself. */
|
|
export function descendantIds(mailboxes: Record<Id, Mailbox>, id: Id): Set<Id> {
|
|
const out = new Set<Id>();
|
|
const all = Object.values(mailboxes);
|
|
let frontier = new Set<Id>([id]);
|
|
// Depth is bounded by the server's own mailbox depth limit; the guard is only
|
|
// here so a cycle in the data cannot spin forever.
|
|
for (let depth = 0; depth < 20 && frontier.size; depth++) {
|
|
const next = new Set<Id>();
|
|
for (const m of all) {
|
|
if (m.parentId && frontier.has(m.parentId) && !out.has(m.id)) {
|
|
out.add(m.id);
|
|
next.add(m.id);
|
|
}
|
|
}
|
|
frontier = next;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/**
|
|
* Whether `draggedId` may be dropped on `targetId`, where null means the root.
|
|
*
|
|
* Four ways it cannot: the folder is not movable at all, it is being dropped on
|
|
* itself, into its own subtree — which would orphan the branch — or onto the
|
|
* parent it already has, which would be a no-op dressed up as a move.
|
|
*/
|
|
export function canDropFolder(mailboxes: Record<Id, Mailbox>, draggedId: Id, targetId: Id | null): boolean {
|
|
const dragged = mailboxes[draggedId];
|
|
if (!dragged || !movable(dragged)) return false;
|
|
if (targetId === null) return dragged.parentId != null;
|
|
if (targetId === draggedId) return false;
|
|
if (dragged.parentId === targetId) return false;
|
|
if (!mailboxes[targetId]) return false;
|
|
return !descendantIds(mailboxes, draggedId).has(targetId);
|
|
}
|
|
|
|
/** The colour chosen for a folder, if any. Ids are used, so a rename keeps it. */
|
|
export function folderColor(colors: Record<string, string>, id: Id): string | null {
|
|
return colors[id] ?? null;
|
|
}
|