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 5865bfe..5248d1a 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 @@ -331,7 +331,7 @@ fun EditorLabelRow( // "from tag" hint beside one is what says why it has no cross. text = "#${label.name}", style = MaterialTheme.typography.labelLarge, - color = tint.chipForeground(dark), + color = tint.tagInk(dark), modifier = Modifier .clip(CircleShape) 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 e254a56..46eab4f 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 @@ -375,7 +375,7 @@ private fun LabelChips(labels: List) { // also keeps a lifted chip reading as the `#todo` somebody wrote. text = "#${label.name}", style = MaterialTheme.typography.labelSmall, - color = tint.chipForeground(dark), + color = tint.tagInk(dark), maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = 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 8af03b4..bae671a 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 @@ -33,24 +33,33 @@ data class NoteTint( val darkBackground: Color, val darkBorder: Color, val lightChipBackground: Color, - val lightChipForeground: Color, val darkChipBackground: Color, + /** + * The REMINDER pill's ink, and nothing else's — see [chipForeground]. + * + * Only two of these ten are ever read (`red` when a reminder has passed, `default` + * otherwise). They stay a per-hue column because they are transcribed from the + * web's literals rather than derived from anything here. + */ + val lightChipForeground: Color, val darkChipForeground: Color, /** - * The colour a `#tag` is drawn in when it is left in the prose — see [tagInk]. + * THE INK A TAG IS DRAWN IN — inline in the prose AND as a chip's text — see + * [tagInk]. * - * A SEPARATE value from [lightChipForeground] rather than a reuse of it, and the - * difference is not cosmetic. A chip brings its own `-100` fill, so its text only - * ever has to read against that one colour. Inline text sits on whatever the card - * happens to be, which includes the gray-tagged card at `neutral-200` — and there - * the chip's `-700` measured 3.98 (green), 4.11 (orange) and 4.34 (teal), all - * under the 4.5 body text needs. One step deeper puts every hue between 5.63 and - * 12.01 in light and 7.20 and 10.84 in dark, across every fill that existed then, - * so it was one rule rather than three exceptions. + * These were two columns until M315, and the split was real while it lasted: a chip + * brought its own `-100` fill and could afford `-700`, while inline text sat on + * whatever the card was, which included a gray-tagged card at `neutral-200` where + * `-700` measured 3.98 (green), 4.11 (orange) and 4.34 (teal), all under the 4.5 + * body text needs. One step deeper cleared every fill at once. * - * THE CONSTRAINT IS GONE as of M315 — one card fill per theme means this is solving - * for two backgrounds instead of twenty. Left here unchanged because it still - * clears 4.5 on both; re-measuring against one known background is #3149. + * The twenty card fills that split was solving for are gone, so both jobs take this + * one value. The direction is deliberate: since M311 a tag whose text is in the body + * is drawn where it was typed and NOT repeated as a chip, so the inline token is the + * common case and collapsing onto ITS column leaves what is seen most exactly as it + * was. The chip is strictly better for the move — on its own fill it goes from + * 4.52-8.23 to 6.37-12.01 in light. Dark needed no decision: the two columns already + * held the same value for all ten hues. */ val lightTagInk: Color, val darkTagInk: Color, @@ -66,10 +75,19 @@ data class NoteTint( fun chipBackground(dark: Boolean): Color = if (dark) darkChipBackground else lightChipBackground + /** + * The REMINDER pill's ink. NOT a tag's — a tag takes [tagInk] wherever it is drawn. + * + * Kept apart from [tagInk] because the reminder pill is not a tag: it borrows the + * chip's shape and its `red-100`/`black-5` fills, and its `red-700`/`neutral-600` + * text is transcribed from NoteCard.vue's literal classes. The two happened to be + * one value; making the tag ink one step deeper (M315) is where they parted, and + * moving the reminder with it would have silently broken that mirror instead. + */ fun chipForeground(dark: Boolean): Color = if (dark) darkChipForeground else lightChipForeground /** - * The colour for a `#tag` still sitting in the note's own words. + * The colour a `#tag` is drawn in — in the note's own words, or as a chip. * * A tag whose text is in the body is no longer repeated as a chip (the card was * printing every tag twice — once where it was typed, once at the top). It is @@ -79,32 +97,37 @@ data class NoteTint( fun tagInk(dark: Boolean): Color = if (dark) darkTagInk else lightTagInk /** - * A hairline edge for a chip, in its own text colour at low alpha. + * A hairline edge for a chip, in its own ink 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. + * THE EDGE IS THE PILL. Against the one card surface a chip's fill measures + * 1.02-1.26 in light and 1.02-1.73 in dark — very nearly nothing, and dark red at + * 1.02 is literally invisible. Without this the tag name would read as loose text. + * The fill only tints a shape the edge is drawing. * - * 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. + * An edge rather than a heavier fill, because a fill loud enough to hold its own + * shape would be the loudest thing on a board whose whole point is now that the tag + * is the one coloured thing on it. */ - fun chipBorder(dark: Boolean): Color = chipForeground(dark).copy(alpha = CHIP_EDGE_ALPHA) + fun chipBorder(dark: Boolean): Color = tagInk(dark).copy(alpha = CHIP_EDGE_ALPHA) } -// How strongly a chip's edge is drawn, as a fraction of its own text colour. +// How strongly a chip's edge is drawn, as a fraction of its own ink. // -// 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 +// SOLVED FOR, NOT GUESSED — and re-solved once the answer became solvable. 0.60 was +// picked against the worst case of the time: a chip on a card of its OWN colour, back +// when a note took its first tag's fill. It gave 2.32:1 there, missed the 3:1 of WCAG +// 1.4.11, and the comment here reasoned its way out of that on the grounds that a +// chip's information is its text. +// +// M315 removed that worst case. The edge is now the ink at alpha over a KNOWN fill, so +// the smallest alpha clearing 3:1 for all ten hues is arithmetic rather than judgment: +// 0.60 gives 2.75-3.82 in light and misses for six of the ten, 0.65 gives 3.03-4.36 and +// misses for none. Dark runs 4.52-5.76. The old comment named 0.80 as the fallback if +// the judgment were ever overruled; it is not needed, and it draws a hard outline where +// a hairline does the job. +// +// Mirrored on the web as the `/65` in LABEL_CHIP_SHELL's ring. +private const val CHIP_EDGE_ALPHA = 0.65f /** Keyed by the core's colour vocabulary. Order matches the web's picker. */ val NOTE_TINTS: Map = diff --git a/frontend/src/components/NoteCard.vue b/frontend/src/components/NoteCard.vue index 0c32cee..b5a50d9 100644 --- a/frontend/src/components/NoteCard.vue +++ b/frontend/src/components/NoteCard.vue @@ -2,12 +2,11 @@ import { computed, onBeforeUnmount, ref, watch } from "vue"; import { useNotesStore } from "../stores/notes"; import { - LABEL_CHIP_CLASSES, NOTE_CARD_SURFACE, NOTE_COLOR_KEYS, NOTE_COLOR_LABELS, NOTE_SWATCH_CLASSES, - resolveLabelColor, + labelChipClasses, type NoteColor, } from "../notes/colors"; import type { Note } from "../stores/notes"; @@ -190,12 +189,6 @@ async function snoozeReminder(minutes: number): Promise { emit("reminder-changed"); } -// Takes the label, not its colour: a tag nobody has coloured derives one from its -// name, so chips carry the tag's identity rather than all being the same grey. -function labelChip(label: { name: string; color: string }): string { - return LABEL_CHIP_CLASSES[resolveLabelColor(label)] ?? LABEL_CHIP_CLASSES.default; -} - // Only the tags the BODY is not already showing. `via_tag` means exactly "backed by // text still in the note" since M311, so a chip for one printed the same tag twice — // once where it was typed, once in this row — and the loud copy was the duplicate. A @@ -300,7 +293,7 @@ onBeforeUnmount(() => document.removeEventListener("mousedown", onDocMousedown)) v-for="lb in chipLabels" :key="lb.id" class="rounded-full px-2 py-0.5 text-xs" - :class="labelChip(lb)" + :class="labelChipClasses(lb)" >#{{ lb.name }} diff --git a/frontend/src/components/NoteEditor.vue b/frontend/src/components/NoteEditor.vue index 25ba158..d3e7095 100644 --- a/frontend/src/components/NoteEditor.vue +++ b/frontend/src/components/NoteEditor.vue @@ -9,7 +9,7 @@ import { fromLocalInput, toLocalInput } from "../notes/datetime"; import { takeMorphOrigin } from "../composables/useEditorMorph"; import { prefersReducedMotion } from "../composables/useReducedMotion"; import type { Note, NoteLabel, NoteRevision } from "../stores/notes"; -import { LABEL_CHIP_CLASSES, resolveLabelColor, type NoteColor } from "../notes/colors"; +import { labelChipClasses, type NoteColor } from "../notes/colors"; import { afterEnter, type EditorBlock, @@ -376,13 +376,6 @@ async function onLabelsChange(next: NoteLabel[]) { async function removeLabel(id: string) { await onLabelsChange(labelList.value.filter((lb) => lb.id !== id)); } -// Takes the label, not its colour — a tag nobody has coloured derives one from its -// name. Must match NoteCard's chip exactly: the same tag either side of opening a -// note changing colour would be worse than both being grey. -function labelChip(label: { name: string; color: string }): string { - return LABEL_CHIP_CLASSES[resolveLabelColor(label)] ?? LABEL_CHIP_CLASSES.default; -} - // ---- add a checklist ---- // // Appends an empty item and puts the caret in it. Unlike every other toolbar button @@ -630,7 +623,7 @@ function revPreview(rev: NoteRevision): string { v-for="lb in labelList" :key="lb.id" class="inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs" - :class="labelChip(lb)" + :class="labelChipClasses(lb)" >