diff --git a/android/app/src/main/java/com/fabledsword/thoughtsync/ui/EditorChrome.kt b/android/app/src/main/java/com/fabledsword/thoughtsync/ui/EditorChrome.kt index efff405..6241950 100644 --- a/android/app/src/main/java/com/fabledsword/thoughtsync/ui/EditorChrome.kt +++ b/android/app/src/main/java/com/fabledsword/thoughtsync/ui/EditorChrome.kt @@ -343,6 +343,7 @@ fun EditorLabelRow( Modifier .clip(CircleShape) .background(tint.chipBackground(dark)) + .border(1.dp, tint.chipBorder(dark), CircleShape) .padding(horizontal = 10.dp, vertical = 4.dp), ) if (label.viaTag) { diff --git a/android/app/src/main/java/com/fabledsword/thoughtsync/ui/NoteCard.kt b/android/app/src/main/java/com/fabledsword/thoughtsync/ui/NoteCard.kt index e0a03e4..af7077d 100644 --- a/android/app/src/main/java/com/fabledsword/thoughtsync/ui/NoteCard.kt +++ b/android/app/src/main/java/com/fabledsword/thoughtsync/ui/NoteCard.kt @@ -194,6 +194,7 @@ private fun LabelChips(labels: List) { Modifier .clip(RoundedCornerShape(CHIP_RADIUS)) .background(tint.chipBackground(dark)) + .border(1.dp, tint.chipBorder(dark), RoundedCornerShape(CHIP_RADIUS)) .padding(horizontal = 6.dp, vertical = 2.dp), ) } diff --git a/android/app/src/main/java/com/fabledsword/thoughtsync/ui/NoteTint.kt b/android/app/src/main/java/com/fabledsword/thoughtsync/ui/NoteTint.kt index f9cafcc..e324ee1 100644 --- a/android/app/src/main/java/com/fabledsword/thoughtsync/ui/NoteTint.kt +++ b/android/app/src/main/java/com/fabledsword/thoughtsync/ui/NoteTint.kt @@ -74,8 +74,35 @@ data class NoteTint( fun chipBackground(dark: Boolean): Color = if (dark) darkChipBackground else lightChipBackground fun chipForeground(dark: Boolean): Color = if (dark) darkChipForeground else lightChipForeground + + /** + * A hairline edge for a chip, in its own text colour at low alpha. + * + * Needed because a chip's fill can no longer be trusted to differ from what is + * behind it. A tagged note takes its FIRST tag's colour, and that card is drawn + * at exactly [lightChipBackground] — the same value the chip uses — so in light + * mode the pill was measured at a contrast ratio of 1.00 against the card it had + * itself coloured. Perfectly invisible: the tag name read as loose text. + * + * An edge rather than shifting the fill, because the fill can collide with any + * card colour and chasing that needs the chip to know what it is sitting on. A + * border in the chip's own foreground always reads, against any background, and + * needs no plumbing. + */ + fun chipBorder(dark: Boolean): Color = chipForeground(dark).copy(alpha = CHIP_EDGE_ALPHA) } +// How strongly a chip's edge is drawn, as a fraction of its own text colour. +// +// 0.60 measured, not guessed: against the worst case — a chip on a card of its own +// colour — this is a 2.32:1 boundary, where 0.30 gave only 1.49:1 and was effectively +// no edge at all. It does NOT reach the 3:1 of WCAG 1.4.11, which needs 0.80 and +// draws a hard outline rather than a hairline. 1.4.11 governs boundaries that carry +// REQUIRED information, and a chip's information is its text — which passes AA at +// 8:1 or better on every card in the palette. The edge restores the pill's shape; +// it does not carry the meaning. Raise to 0.80 if that judgment is ever overruled. +private const val CHIP_EDGE_ALPHA = 0.60f + // The two dark-theme weights, as fractions. Mirror `dark:bg-{hue}-950/25` and `/70` // in colors.ts — these are the only two numbers that have to agree by hand, since the // light weights are both already columns of the table below. diff --git a/frontend/src/notes/colors.ts b/frontend/src/notes/colors.ts index 87f77a4..26514e6 100644 --- a/frontend/src/notes/colors.ts +++ b/frontend/src/notes/colors.ts @@ -43,18 +43,26 @@ export const NOTE_SWATCH_CLASSES: Record = { gray: "bg-neutral-400 dark:bg-neutral-500", }; -// Label chip tints (bg + readable text), keyed by the same color vocabulary. +// Label chip tints (bg + readable text + a hairline edge), keyed by the same color +// vocabulary. +// +// The RING is not decoration. A tagged note takes its first tag's colour and is drawn +// at that hue's `-100` — exactly what the chip uses as its fill — so in light mode the +// chip measured a contrast ratio of 1.00 against the card it had itself coloured. +// Perfectly invisible; the tag name read as loose text. An edge holds the pill's shape +// against ANY background, where shifting the fill only moves which card it collides +// with. export const LABEL_CHIP_CLASSES: Record = { - default: "bg-black/5 text-neutral-600 dark:bg-white/10 dark:text-neutral-300", - red: "bg-red-100 text-red-700 dark:bg-red-950/50 dark:text-red-300", - orange: "bg-orange-100 text-orange-700 dark:bg-orange-950/50 dark:text-orange-300", - yellow: "bg-amber-100 text-amber-800 dark:bg-amber-950/50 dark:text-amber-300", - green: "bg-green-100 text-green-700 dark:bg-green-950/50 dark:text-green-300", - teal: "bg-teal-100 text-teal-700 dark:bg-teal-950/50 dark:text-teal-300", - blue: "bg-blue-100 text-blue-700 dark:bg-blue-950/50 dark:text-blue-300", - purple: "bg-purple-100 text-purple-700 dark:bg-purple-950/50 dark:text-purple-300", - pink: "bg-pink-100 text-pink-700 dark:bg-pink-950/50 dark:text-pink-300", - gray: "bg-neutral-200 text-neutral-700 dark:bg-neutral-700 dark:text-neutral-200", + default: "bg-black/5 text-neutral-600 dark:bg-white/10 dark:text-neutral-300 ring-1 ring-inset ring-black/10 dark:ring-white/15", + red: "bg-red-100 text-red-700 dark:bg-red-950/50 dark:text-red-300 ring-1 ring-inset ring-red-700/60 dark:ring-red-300/60", + orange: "bg-orange-100 text-orange-700 dark:bg-orange-950/50 dark:text-orange-300 ring-1 ring-inset ring-orange-700/60 dark:ring-orange-300/60", + yellow: "bg-amber-100 text-amber-800 dark:bg-amber-950/50 dark:text-amber-300 ring-1 ring-inset ring-amber-700/60 dark:ring-amber-300/60", + green: "bg-green-100 text-green-700 dark:bg-green-950/50 dark:text-green-300 ring-1 ring-inset ring-green-700/60 dark:ring-green-300/60", + teal: "bg-teal-100 text-teal-700 dark:bg-teal-950/50 dark:text-teal-300 ring-1 ring-inset ring-teal-700/60 dark:ring-teal-300/60", + blue: "bg-blue-100 text-blue-700 dark:bg-blue-950/50 dark:text-blue-300 ring-1 ring-inset ring-blue-700/60 dark:ring-blue-300/60", + purple: "bg-purple-100 text-purple-700 dark:bg-purple-950/50 dark:text-purple-300 ring-1 ring-inset ring-purple-700/60 dark:ring-purple-300/60", + pink: "bg-pink-100 text-pink-700 dark:bg-pink-950/50 dark:text-pink-300 ring-1 ring-inset ring-pink-700/60 dark:ring-pink-300/60", + gray: "bg-neutral-200 text-neutral-700 dark:bg-neutral-700 dark:text-neutral-200 ring-1 ring-inset ring-neutral-700/60 dark:ring-neutral-200/60", }; // Solid fills for graph nodes (SVG needs concrete colors, not Tailwind bg classes).