Task #442: Checklist checkbox color matches the note's color
- artifacts/tx-os/src/lib/notes-api.ts: extended NOTE_COLORS so each entry carries an `accent` Tailwind class alongside `bg`/`ring` (default→amber, red→red, orange→orange, yellow→yellow, green→green, teal→teal, blue→sky, purple→purple, pink→pink, gray→slate; all *-500 for visibility in light + dark). Added a `colorAccent(id)` helper that resolves to a literal accent-* class, falling back to the default amber for unknown/null ids. The class strings are written out as literals so Tailwind v4's source scanner emits all of them into the generated CSS — no dynamic class names, no safelist needed. - artifacts/tx-os/src/pages/notes.tsx: ChecklistView gained an optional `noteColor?: string | null` prop and now resolves its checkbox accent via `colorAccent(noteColor)` instead of the hard-coded `accent-amber-500`. Updated all four call sites (NoteCard, received list, sent list, thread dialog) to pass `note.color` / `n.color` / `thread.color`. - artifacts/tx-os/src/components/notes/incoming-note-popup.tsx: PopupChecklistPreview gained the same optional `noteColor` prop and uses `colorAccent` for the checkbox className. Call site now passes `current.color` from the popup queue. - Unchecked boxes still render with the neutral browser default; only the checked accent (the filled tint) follows the note color. The default (white/card) note keeps amber for visibility against the neutral surface in both themes. - Verified: `pnpm exec tsc --noEmit` is clean and the `tests/notes-popup-on-receive.spec.mjs` regression suite (incl. the Task #433 checklist popup case) still passes 3/3. - Pre-existing failing `test` workflow (api-server tsc errors in executive-meetings.ts) is unrelated and out of scope.
This commit is contained in:
@@ -12,6 +12,7 @@ import { Reply, Check, ExternalLink, X, GripVertical } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
colorBg,
|
||||
colorAccent,
|
||||
useMarkNoteRead,
|
||||
useToggleChecklistItem,
|
||||
userDisplayName,
|
||||
@@ -113,10 +114,15 @@ function clampToViewport(pos: StoredPos): StoredPos {
|
||||
function PopupChecklistPreview({
|
||||
items,
|
||||
noteId,
|
||||
noteColor,
|
||||
}: {
|
||||
items: ChecklistItem[];
|
||||
noteId: number;
|
||||
// Match the checked-checkbox accent to the parent note's color so the
|
||||
// checkbox tint belongs to the card (pink note → pink check, etc.).
|
||||
noteColor?: string | null;
|
||||
}) {
|
||||
const accent = colorAccent(noteColor);
|
||||
const toggle = useToggleChecklistItem();
|
||||
// The server intentionally does NOT echo `note_checklist_changed`
|
||||
// back to the actor, so the popup's queued snapshot would stay stale
|
||||
@@ -205,7 +211,7 @@ function PopupChecklistPreview({
|
||||
},
|
||||
);
|
||||
}}
|
||||
className="mt-0.5 h-4 w-4 rounded border-black/30 accent-amber-500"
|
||||
className={`mt-0.5 h-4 w-4 rounded border-black/30 ${accent}`}
|
||||
data-testid={`incoming-note-popup-checklist-check-${it.id}`}
|
||||
/>
|
||||
<span
|
||||
@@ -526,6 +532,7 @@ export function IncomingNotePopup() {
|
||||
<PopupChecklistPreview
|
||||
items={checklistItems}
|
||||
noteId={current.noteId}
|
||||
noteColor={current.color}
|
||||
/>
|
||||
) : (
|
||||
bodyContent && (
|
||||
|
||||
@@ -530,23 +530,40 @@ export function useDeleteLabel() {
|
||||
});
|
||||
}
|
||||
|
||||
export const NOTE_COLORS: { id: string; bg: string; ring: string }[] = [
|
||||
{ id: "default", bg: "bg-white dark:bg-card", ring: "ring-slate-300" },
|
||||
{ id: "red", bg: "bg-red-100 dark:bg-red-900/40", ring: "ring-red-400" },
|
||||
{ id: "orange", bg: "bg-orange-100 dark:bg-orange-900/40", ring: "ring-orange-400" },
|
||||
{ id: "yellow", bg: "bg-yellow-100 dark:bg-yellow-900/40", ring: "ring-yellow-400" },
|
||||
{ id: "green", bg: "bg-green-100 dark:bg-green-900/40", ring: "ring-green-400" },
|
||||
{ id: "teal", bg: "bg-teal-100 dark:bg-teal-900/40", ring: "ring-teal-400" },
|
||||
{ id: "blue", bg: "bg-sky-100 dark:bg-sky-900/40", ring: "ring-sky-400" },
|
||||
{ id: "purple", bg: "bg-purple-100 dark:bg-purple-900/40", ring: "ring-purple-400" },
|
||||
{ id: "pink", bg: "bg-pink-100 dark:bg-pink-900/40", ring: "ring-pink-400" },
|
||||
{ id: "gray", bg: "bg-slate-200 dark:bg-slate-800/60", ring: "ring-slate-400" },
|
||||
export const NOTE_COLORS: {
|
||||
id: string;
|
||||
bg: string;
|
||||
ring: string;
|
||||
accent: string;
|
||||
}[] = [
|
||||
// Tailwind v4 scans these literal class strings at build time, so each
|
||||
// accent-* utility below ends up in the generated CSS even though we
|
||||
// pick the right one at runtime via colorAccent(). For the "default"
|
||||
// (white/card) note we keep amber so the checked box stays visible
|
||||
// against a neutral surface in both light and dark mode; every other
|
||||
// color points at its own family's *-500 accent so the checked tint
|
||||
// belongs to the card instead of clashing with it.
|
||||
{ id: "default", bg: "bg-white dark:bg-card", ring: "ring-slate-300", accent: "accent-amber-500" },
|
||||
{ id: "red", bg: "bg-red-100 dark:bg-red-900/40", ring: "ring-red-400", accent: "accent-red-500" },
|
||||
{ id: "orange", bg: "bg-orange-100 dark:bg-orange-900/40", ring: "ring-orange-400", accent: "accent-orange-500" },
|
||||
{ id: "yellow", bg: "bg-yellow-100 dark:bg-yellow-900/40", ring: "ring-yellow-400", accent: "accent-yellow-500" },
|
||||
{ id: "green", bg: "bg-green-100 dark:bg-green-900/40", ring: "ring-green-400", accent: "accent-green-500" },
|
||||
{ id: "teal", bg: "bg-teal-100 dark:bg-teal-900/40", ring: "ring-teal-400", accent: "accent-teal-500" },
|
||||
{ id: "blue", bg: "bg-sky-100 dark:bg-sky-900/40", ring: "ring-sky-400", accent: "accent-sky-500" },
|
||||
{ id: "purple", bg: "bg-purple-100 dark:bg-purple-900/40", ring: "ring-purple-400", accent: "accent-purple-500" },
|
||||
{ id: "pink", bg: "bg-pink-100 dark:bg-pink-900/40", ring: "ring-pink-400", accent: "accent-pink-500" },
|
||||
{ id: "gray", bg: "bg-slate-200 dark:bg-slate-800/60", ring: "ring-slate-400", accent: "accent-slate-500" },
|
||||
];
|
||||
|
||||
export function colorBg(id: string): string {
|
||||
return NOTE_COLORS.find((c) => c.id === id)?.bg ?? NOTE_COLORS[0].bg;
|
||||
}
|
||||
|
||||
export function colorAccent(id: string | null | undefined): string {
|
||||
if (!id) return NOTE_COLORS[0].accent;
|
||||
return NOTE_COLORS.find((c) => c.id === id)?.accent ?? NOTE_COLORS[0].accent;
|
||||
}
|
||||
|
||||
export function userDisplayName(
|
||||
u: UserSummary | null | undefined,
|
||||
lang: string,
|
||||
|
||||
@@ -74,6 +74,7 @@ import {
|
||||
type UserSummary,
|
||||
NOTE_COLORS,
|
||||
colorBg,
|
||||
colorAccent,
|
||||
useArchiveReceivedNote,
|
||||
useCreateLabel,
|
||||
useCreateNote,
|
||||
@@ -773,6 +774,7 @@ function NoteCard({
|
||||
items={note.items ?? []}
|
||||
testIdPrefix={`note-card-checklist-${note.id}`}
|
||||
className="mt-1"
|
||||
noteColor={note.color}
|
||||
onToggle={(itemId, done) => {
|
||||
const next = (note.items ?? []).map((it) =>
|
||||
it.id === itemId ? { ...it, done } : it,
|
||||
@@ -1448,6 +1450,7 @@ function ReceivedList({
|
||||
items={n.items ?? []}
|
||||
testIdPrefix={`received-note-checklist-${n.id}`}
|
||||
className="mt-1"
|
||||
noteColor={n.color}
|
||||
/>
|
||||
) : (
|
||||
n.content && (
|
||||
@@ -1507,6 +1510,7 @@ function SentList({
|
||||
items={n.items ?? []}
|
||||
testIdPrefix={`sent-note-checklist-${n.id}`}
|
||||
className="mt-1"
|
||||
noteColor={n.color}
|
||||
/>
|
||||
) : (
|
||||
n.content && (
|
||||
@@ -1884,6 +1888,7 @@ function ThreadDialog({
|
||||
<ChecklistView
|
||||
items={thread.items ?? []}
|
||||
testIdPrefix={`thread-checklist-${thread.id}`}
|
||||
noteColor={thread.color}
|
||||
// Task #438: gate toggles to match server auth — only
|
||||
// the owner or an active (non-archived) recipient can
|
||||
// collaboratively check items. Admins viewing a note
|
||||
@@ -2246,13 +2251,19 @@ function ChecklistView({
|
||||
onToggle,
|
||||
testIdPrefix,
|
||||
className = "",
|
||||
noteColor,
|
||||
}: {
|
||||
items: ChecklistItem[];
|
||||
onToggle?: (id: string, done: boolean) => void;
|
||||
testIdPrefix: string;
|
||||
className?: string;
|
||||
// Optional: when provided, the checked-checkbox accent matches the
|
||||
// parent note's color (pink note → pink check, etc.). Falls back to
|
||||
// amber when omitted or unknown — see colorAccent().
|
||||
noteColor?: string | null;
|
||||
}) {
|
||||
if (items.length === 0) return null;
|
||||
const accent = colorAccent(noteColor);
|
||||
return (
|
||||
<div
|
||||
className={`space-y-1 ${className}`}
|
||||
@@ -2274,7 +2285,7 @@ function ChecklistView({
|
||||
checked={it.done}
|
||||
disabled={!onToggle}
|
||||
onChange={(e) => onToggle?.(it.id, e.target.checked)}
|
||||
className="mt-0.5 h-4 w-4 rounded border-black/30 accent-amber-500 disabled:cursor-default"
|
||||
className={`mt-0.5 h-4 w-4 rounded border-black/30 ${accent} disabled:cursor-default`}
|
||||
data-testid={`${testIdPrefix}-check-${it.id}`}
|
||||
/>
|
||||
<span
|
||||
|
||||
Reference in New Issue
Block a user