Task #468: Match incoming-note popup colors to the note's own color
User reported that the floating "new note" popup had a fixed amber/
orange ring, avatar ring, ping pulse and shadow tint, which clashed
visually whenever the underlying note used a different color (e.g. a
pink note appeared inside an orange popup).
Changes:
- artifacts/tx-os/src/lib/notes-api.ts:
- Extended the NOTE_COLORS array with four new fields per color:
`ringStrong` (popup outer ring), `avatarBg` (avatar circle bg+text),
`avatarRing` (2px avatar ring), and `ping` (animated pulse).
- Each entry uses literal Tailwind utility strings so v4's build-time
scanner picks them up — same pattern already used for `accent-*`.
- Default + gray colors map to neutral slate variants so they stay
legible on the white card surface.
- Added small helpers: `colorRingStrong`, `colorAvatarBg`,
`colorAvatarRing`, `colorPing` (each falls back to the default
entry for unknown ids, matching `colorAccent`'s behavior).
- artifacts/tx-os/src/components/notes/incoming-note-popup.tsx:
- Imported the new helpers and resolved them once per render from
`current.color` (works for both note and reply popup variants since
both expose the same `color` field on the payload).
- Replaced every `amber-*` class on the outer ring, the avatar
fallback bg/text, the avatar image ring, and the animated ping
with the resolved color classes.
- Dropped the colored shadow tint (`shadow-amber-500/30`) — the
card now uses a neutral `shadow-2xl` so the only colored chrome
is the ring matching the note.
Validation:
- `pnpm --filter @workspace/tx-os exec tsc --noEmit` passes.
- No remaining `amber` literal in the popup file (only an
explanatory comment mentions the word).
- Pre-existing `test` workflow failures (executive-meetings PDF,
notes-share, groups-crud) are unchanged and out of scope.
This commit is contained in:
@@ -13,6 +13,10 @@ import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
colorBg,
|
||||
colorAccent,
|
||||
colorRingStrong,
|
||||
colorAvatarBg,
|
||||
colorAvatarRing,
|
||||
colorPing,
|
||||
useMarkNoteRead,
|
||||
useToggleChecklistItem,
|
||||
userDisplayName,
|
||||
@@ -342,6 +346,16 @@ export function IncomingNotePopup() {
|
||||
|
||||
if (!current) return null;
|
||||
|
||||
// Task #468: derive every accent class from the note's own color so
|
||||
// the popup looks like an extension of the note (pink note → pink
|
||||
// ring + pink ping + pink avatar tint, etc.) instead of the previous
|
||||
// fixed amber chrome that clashed with non-amber notes.
|
||||
const noteColor = current.color;
|
||||
const ringClass = colorRingStrong(noteColor);
|
||||
const avatarBgClass = colorAvatarBg(noteColor);
|
||||
const avatarRingClass = colorAvatarRing(noteColor);
|
||||
const pingClass = colorPing(noteColor);
|
||||
|
||||
const acknowledge = () => {
|
||||
// Only the recipient (note variant) marks-as-read. The reply
|
||||
// variant goes to the *original sender* (note owner), who can't
|
||||
@@ -464,10 +478,10 @@ export function IncomingNotePopup() {
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={`rounded-2xl ring-4 ring-amber-400/70 shadow-2xl shadow-amber-500/30 overflow-hidden transition-transform duration-200 ${
|
||||
current.color === "default"
|
||||
className={`rounded-2xl ring-4 ${ringClass} shadow-2xl overflow-hidden transition-transform duration-200 ${
|
||||
noteColor === "default"
|
||||
? "bg-background"
|
||||
: colorBg(current.color)
|
||||
: colorBg(noteColor)
|
||||
} ${enterAnim ? "scale-100" : "scale-95"}`}
|
||||
data-popup-color={current.color}
|
||||
>
|
||||
@@ -488,17 +502,17 @@ export function IncomingNotePopup() {
|
||||
/>
|
||||
<span className="relative shrink-0">
|
||||
<span
|
||||
className="absolute inset-0 rounded-full bg-amber-400/60 animate-ping"
|
||||
className={`absolute inset-0 rounded-full ${pingClass} animate-ping`}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{senderForHeader?.avatarUrl ? (
|
||||
<img
|
||||
src={senderForHeader.avatarUrl}
|
||||
alt=""
|
||||
className="relative h-8 w-8 rounded-full object-cover ring-2 ring-amber-400"
|
||||
className={`relative h-8 w-8 rounded-full object-cover ring-2 ${avatarRingClass}`}
|
||||
/>
|
||||
) : (
|
||||
<div className="relative h-8 w-8 rounded-full bg-amber-200 text-amber-900 text-sm font-semibold flex items-center justify-center ring-2 ring-amber-400">
|
||||
<div className={`relative h-8 w-8 rounded-full ${avatarBgClass} text-sm font-semibold flex items-center justify-center ring-2 ${avatarRingClass}`}>
|
||||
{avatarInitial(senderForHeader, i18n.language)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -736,6 +736,16 @@ export const NOTE_COLORS: {
|
||||
bg: string;
|
||||
ring: string;
|
||||
accent: string;
|
||||
// Task #468: each color also carries the literal class strings used by
|
||||
// the incoming-note popup so the popup's outer ring, the avatar's
|
||||
// background + ring, and the animated ping pulse all match the note's
|
||||
// own color family instead of being a fixed amber tint. Keeping these
|
||||
// as literal strings (not interpolated) preserves Tailwind v4's
|
||||
// build-time scanning, same as the existing `accent-*` pattern above.
|
||||
ringStrong: string; // ring color for the popup card outline (4px ring)
|
||||
avatarBg: string; // avatar background + text when there's no photo
|
||||
avatarRing: string; // 2px ring around the avatar circle
|
||||
ping: string; // animated pulse behind the avatar
|
||||
}[] = [
|
||||
// Tailwind v4 scans these literal class strings at build time, so each
|
||||
// accent-* utility below ends up in the generated CSS even though we
|
||||
@@ -745,16 +755,16 @@ export const NOTE_COLORS: {
|
||||
// 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. colorAccent() falls back to this default for unknown ids.
|
||||
{ id: "default", bg: "bg-white dark:bg-card", ring: "ring-slate-300", accent: "accent-slate-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" },
|
||||
{ id: "default", bg: "bg-white dark:bg-card", ring: "ring-slate-300", accent: "accent-slate-500", ringStrong: "ring-slate-400/70", avatarBg: "bg-slate-200 text-slate-900", avatarRing: "ring-slate-400", ping: "bg-slate-400/60" },
|
||||
{ id: "red", bg: "bg-red-100 dark:bg-red-900/40", ring: "ring-red-400", accent: "accent-red-500", ringStrong: "ring-red-400/70", avatarBg: "bg-red-200 text-red-900", avatarRing: "ring-red-400", ping: "bg-red-400/60" },
|
||||
{ id: "orange", bg: "bg-orange-100 dark:bg-orange-900/40", ring: "ring-orange-400", accent: "accent-orange-500", ringStrong: "ring-orange-400/70", avatarBg: "bg-orange-200 text-orange-900", avatarRing: "ring-orange-400", ping: "bg-orange-400/60" },
|
||||
{ id: "yellow", bg: "bg-yellow-100 dark:bg-yellow-900/40", ring: "ring-yellow-400", accent: "accent-yellow-500", ringStrong: "ring-yellow-400/70", avatarBg: "bg-yellow-200 text-yellow-900", avatarRing: "ring-yellow-400", ping: "bg-yellow-400/60" },
|
||||
{ id: "green", bg: "bg-green-100 dark:bg-green-900/40", ring: "ring-green-400", accent: "accent-green-500", ringStrong: "ring-green-400/70", avatarBg: "bg-green-200 text-green-900", avatarRing: "ring-green-400", ping: "bg-green-400/60" },
|
||||
{ id: "teal", bg: "bg-teal-100 dark:bg-teal-900/40", ring: "ring-teal-400", accent: "accent-teal-500", ringStrong: "ring-teal-400/70", avatarBg: "bg-teal-200 text-teal-900", avatarRing: "ring-teal-400", ping: "bg-teal-400/60" },
|
||||
{ id: "blue", bg: "bg-sky-100 dark:bg-sky-900/40", ring: "ring-sky-400", accent: "accent-sky-500", ringStrong: "ring-sky-400/70", avatarBg: "bg-sky-200 text-sky-900", avatarRing: "ring-sky-400", ping: "bg-sky-400/60" },
|
||||
{ id: "purple", bg: "bg-purple-100 dark:bg-purple-900/40", ring: "ring-purple-400", accent: "accent-purple-500", ringStrong: "ring-purple-400/70", avatarBg: "bg-purple-200 text-purple-900", avatarRing: "ring-purple-400", ping: "bg-purple-400/60" },
|
||||
{ id: "pink", bg: "bg-pink-100 dark:bg-pink-900/40", ring: "ring-pink-400", accent: "accent-pink-500", ringStrong: "ring-pink-400/70", avatarBg: "bg-pink-200 text-pink-900", avatarRing: "ring-pink-400", ping: "bg-pink-400/60" },
|
||||
{ id: "gray", bg: "bg-slate-200 dark:bg-slate-800/60", ring: "ring-slate-400", accent: "accent-slate-500", ringStrong: "ring-slate-400/70", avatarBg: "bg-slate-300 text-slate-900", avatarRing: "ring-slate-400", ping: "bg-slate-400/60" },
|
||||
];
|
||||
|
||||
export function colorBg(id: string): string {
|
||||
@@ -766,6 +776,27 @@ export function colorAccent(id: string | null | undefined): string {
|
||||
return NOTE_COLORS.find((c) => c.id === id)?.accent ?? NOTE_COLORS[0].accent;
|
||||
}
|
||||
|
||||
function colorEntry(id: string | null | undefined) {
|
||||
if (!id) return NOTE_COLORS[0];
|
||||
return NOTE_COLORS.find((c) => c.id === id) ?? NOTE_COLORS[0];
|
||||
}
|
||||
|
||||
export function colorRingStrong(id: string | null | undefined): string {
|
||||
return colorEntry(id).ringStrong;
|
||||
}
|
||||
|
||||
export function colorAvatarBg(id: string | null | undefined): string {
|
||||
return colorEntry(id).avatarBg;
|
||||
}
|
||||
|
||||
export function colorAvatarRing(id: string | null | undefined): string {
|
||||
return colorEntry(id).avatarRing;
|
||||
}
|
||||
|
||||
export function colorPing(id: string | null | undefined): string {
|
||||
return colorEntry(id).ping;
|
||||
}
|
||||
|
||||
export function userDisplayName(
|
||||
u: UserSummary | null | undefined,
|
||||
lang: string,
|
||||
|
||||
Reference in New Issue
Block a user