aa6023e421
* feat(web): per-task color indicator in sidebar (localStorage) Adds a color marker to tasks in the left task list for better visual identification. Right-click a task → Color submenu picks one of seven colors (or None to clear). Color renders as a 3px bar on the left edge of the row and persists in localStorage. * fix(web): address review feedback on task color indicator - Filter storage events to TASK_COLORS_STORAGE_KEY so cross-tab writes to unrelated keys no longer wake every TaskItem subscriber. - Cache the parsed color map at module level, invalidated on writes and cross-tab storage events. Avoids re-parsing localStorage on every getSnapshot call (N tasks × N renders). - Drop redundant typeof string check on Object.entries keys. - Dim the colored bar to opacity-60 when not selected so colored rows remain distinguishable from colored-and-selected rows at the left edge. * refactor(web): remove test-only export from task-colors Drop the __resetTaskColorsCacheForTests escape hatch from the production module. Tests now invalidate the cache via a synthesized cross-tab `storage` event, exercising the same listener path the production code uses for real cross-tab updates. --------- Co-authored-by: Kandev Agent <agent@kandev.dev>
81 lines
2.3 KiB
TypeScript
81 lines
2.3 KiB
TypeScript
import { getLocalStorage, setLocalStorage } from "./local-storage";
|
|
|
|
export const TASK_COLORS = ["red", "orange", "yellow", "green", "blue", "purple", "pink"] as const;
|
|
|
|
export type TaskColor = (typeof TASK_COLORS)[number];
|
|
|
|
export const TASK_COLOR_BAR_CLASS: Record<TaskColor, string> = {
|
|
red: "bg-red-500",
|
|
orange: "bg-orange-500",
|
|
yellow: "bg-yellow-500",
|
|
green: "bg-green-500",
|
|
blue: "bg-blue-500",
|
|
purple: "bg-purple-500",
|
|
pink: "bg-pink-500",
|
|
};
|
|
|
|
export const TASK_COLOR_LABEL: Record<TaskColor, string> = {
|
|
red: "Red",
|
|
orange: "Orange",
|
|
yellow: "Yellow",
|
|
green: "Green",
|
|
blue: "Blue",
|
|
purple: "Purple",
|
|
pink: "Pink",
|
|
};
|
|
|
|
export const TASK_COLORS_STORAGE_KEY = "kandev.taskColors";
|
|
export const TASK_COLORS_CHANGED_EVENT = "kandev:task-colors-changed";
|
|
|
|
function isTaskColor(value: unknown): value is TaskColor {
|
|
return typeof value === "string" && (TASK_COLORS as readonly string[]).includes(value);
|
|
}
|
|
|
|
let cachedMap: Record<string, TaskColor> | null = null;
|
|
|
|
if (typeof window !== "undefined") {
|
|
window.addEventListener("storage", (e) => {
|
|
if (e.key === null || e.key === TASK_COLORS_STORAGE_KEY) cachedMap = null;
|
|
});
|
|
}
|
|
|
|
function readAll(): Record<string, TaskColor> {
|
|
if (cachedMap) return cachedMap;
|
|
const raw = getLocalStorage<Record<string, string>>(TASK_COLORS_STORAGE_KEY, {});
|
|
const out: Record<string, TaskColor> = {};
|
|
if (raw && typeof raw === "object") {
|
|
for (const [taskId, color] of Object.entries(raw)) {
|
|
if (isTaskColor(color)) out[taskId] = color;
|
|
}
|
|
}
|
|
cachedMap = out;
|
|
return out;
|
|
}
|
|
|
|
function writeAll(map: Record<string, TaskColor>): void {
|
|
cachedMap = map;
|
|
setLocalStorage(TASK_COLORS_STORAGE_KEY, map);
|
|
if (typeof window !== "undefined") {
|
|
window.dispatchEvent(new CustomEvent(TASK_COLORS_CHANGED_EVENT));
|
|
}
|
|
}
|
|
|
|
export function getTaskColor(taskId: string): TaskColor | null {
|
|
if (!taskId) return null;
|
|
return readAll()[taskId] ?? null;
|
|
}
|
|
|
|
export function setTaskColor(taskId: string, color: TaskColor | null): void {
|
|
if (!taskId) return;
|
|
const all = readAll();
|
|
if (color === null) {
|
|
if (!(taskId in all)) return;
|
|
const next = { ...all };
|
|
delete next[taskId];
|
|
writeAll(next);
|
|
} else {
|
|
if (all[taskId] === color) return;
|
|
writeAll({ ...all, [taskId]: color });
|
|
}
|
|
}
|