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:
Riyadh
2026-05-07 15:08:58 +00:00
parent 28b8e9e4fa
commit 469be5d07f
3 changed files with 48 additions and 13 deletions
@@ -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 && (
+28 -11
View File
@@ -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,
+12 -1
View File
@@ -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