A note's colour is its first tag's colour, at a heavier weight
CI & Build / Build now, or wait for Android? (push) Successful in 3s
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 6s
CI & Build / Python tests (push) Successful in 14s
CI & Build / integration (push) Successful in 20s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 2m41s
Android / Kotlin + Rust (APK) (push) Failing after 3m53s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 5m7s
Desktop (Tauri) / Update manifest (push) Successful in 5s
CI & Build / Build now, or wait for Android? (push) Successful in 3s
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 6s
CI & Build / Python tests (push) Successful in 14s
CI & Build / integration (push) Successful in 20s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 2m41s
Android / Kotlin + Rust (APK) (push) Failing after 3m53s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 5m7s
Desktop (Tauri) / Update manifest (push) Successful in 5s
Four #todo notes on the operator's board in four different colours, because the tint was derived per-note-id and ignored tags entirely. Now a tagged note wears its first tag's colour, so notes that share a tag share a look. TWO WEIGHTS, NOT ONE RAMP. The operator, seeing step 1: "the tints look the same as the chosen colors". They did — there was only one ramp. `strong` is not a second decision, it IS whether the colour was chosen: a tag (or, until step 5, the picker) means somebody said what this note is, while a derived tint only means the board should not be a wall of white. The two weights move in OPPOSITE directions per theme, because that is where each has headroom. The operator asked whether the tint could go lighter instead of the tagged end going darker; in dark mode that is the better half of the answer, so the derived end drops to a quarter opacity — closer to the board, which gives the light body text MORE contrast rather than less. Light mode has nowhere to go below `-50` without being white again, so there the gap opens by deepening the chosen end to `-100`. No hex was transcribed for any of it. `-100` is already in NoteTint.kt as every hue's `lightChipBackground`, and the dark weights are the existing `-950` fill re-alphaed, so the only two numbers that have to agree by hand are the alphas. Copying ten more Tailwind values from memory is exactly how this mirror would have drifted. `default` is marked not tintable — it is the ABSENCE of a colour, there is no emphatic version of it, and re-alphaing its opaque neutral fill would have made every draft card translucent. Borders untouched: the fill is the signal, moving both muddies the edge. Resolution order is explicit pick, then first tag, then the id hash. First tag because it is the one you control by typing; manual labels count the same as #tags because nobody can tell which kind they made by looking. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -114,10 +114,14 @@ fun resolvedLabelColor(
|
||||
fun resolvedNoteColor(
|
||||
id: String,
|
||||
color: String,
|
||||
labelColor: String,
|
||||
known: Set<String>,
|
||||
): String =
|
||||
when {
|
||||
color.isNotEmpty() && color != "default" && color in known -> color
|
||||
// The note's FIRST tag. Passed in already resolved, so this does not need to
|
||||
// know that a colourless tag derives one from its name.
|
||||
labelColor.isNotEmpty() -> labelColor
|
||||
// A draft carries DRAFT_ID (""), so there is no identity to derive from yet.
|
||||
// Staying white until the note exists costs one colour change at save time;
|
||||
// hashing the empty string instead would give EVERY draft the same tint and
|
||||
@@ -125,3 +129,18 @@ fun resolvedNoteColor(
|
||||
id.isEmpty() -> "default"
|
||||
else -> derivedTint(id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a note's colour was CHOSEN rather than derived — which is what decides
|
||||
* between the two weights the card is drawn at.
|
||||
*
|
||||
* A tag (or, until step 5, the picker) means somebody said what this note is. A
|
||||
* derived tint only means the board should not be a wall of white. Drawing both at
|
||||
* the same weight is what prompted the operator's "the tints look the same as the
|
||||
* chosen colors" — the tint was never meant to shout as loudly as a decision.
|
||||
*/
|
||||
fun noteColorIsChosen(
|
||||
color: String,
|
||||
labelColor: String,
|
||||
known: Set<String>,
|
||||
): Boolean = labelColor.isNotEmpty() || (color.isNotEmpty() && color != "default" && color in known)
|
||||
|
||||
@@ -37,7 +37,8 @@ fun NoteCard(
|
||||
onToggleItem: (Int, Boolean) -> Unit,
|
||||
) {
|
||||
val dark = isSystemInDarkTheme()
|
||||
val tint = noteTintFor(note.id, note.color)
|
||||
val tint = noteTintFor(note)
|
||||
val strong = noteIsStrong(note)
|
||||
|
||||
Column(
|
||||
modifier =
|
||||
@@ -47,7 +48,7 @@ fun NoteCard(
|
||||
// rounded corners instead of a rectangle overhanging them.
|
||||
.clip(RoundedCornerShape(CARD_RADIUS))
|
||||
.clickable(onClickLabel = stringResource(R.string.board_open_note), onClick = onOpen)
|
||||
.background(tint.background(dark))
|
||||
.background(tint.cardBackground(dark, strong))
|
||||
.border(1.dp, tint.border(dark), RoundedCornerShape(CARD_RADIUS))
|
||||
.padding(12.dp),
|
||||
) {
|
||||
|
||||
@@ -63,7 +63,8 @@ fun NoteEditorScreen(
|
||||
onAction: (EditorAction) -> Unit,
|
||||
) {
|
||||
val dark = isSystemInDarkTheme()
|
||||
val tint = noteTintFor(note.id, note.color)
|
||||
val tint = noteTintFor(note)
|
||||
val strong = noteIsStrong(note)
|
||||
|
||||
// Keyed by the SESSION, not by note.id: the editor is reused across notes, so it
|
||||
// needs a key — but a draft's id changes the moment it is first saved, and
|
||||
@@ -155,7 +156,7 @@ fun NoteEditorScreen(
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
shape = RoundedCornerShape(topStart = SHEET_CORNER, topEnd = SHEET_CORNER),
|
||||
color = tint.background(dark),
|
||||
color = tint.cardBackground(dark, strong),
|
||||
// Both content colours are spelled out for the reason the toolbar had to
|
||||
// be: Surface and Scaffold each default theirs to contentColorFor(their
|
||||
// container), which returns Unspecified for anything that is not a
|
||||
@@ -166,7 +167,7 @@ fun NoteEditorScreen(
|
||||
contentColor = MaterialTheme.colorScheme.onSurface,
|
||||
) {
|
||||
Scaffold(
|
||||
containerColor = tint.background(dark),
|
||||
containerColor = tint.cardBackground(dark, strong),
|
||||
contentColor = MaterialTheme.colorScheme.onSurface,
|
||||
topBar = {
|
||||
EditorTopBar(
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.fabledsword.thoughtsync.ui
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import com.fabledsword.thoughtsync.core.Note
|
||||
|
||||
/**
|
||||
* The note colour palette, matching `frontend/src/notes/colors.ts` VALUE FOR VALUE.
|
||||
@@ -30,9 +31,44 @@ data class NoteTint(
|
||||
val lightChipForeground: Color,
|
||||
val darkChipBackground: Color,
|
||||
val darkChipForeground: Color,
|
||||
/**
|
||||
* False only for `default`, which is the ABSENCE of a colour rather than one of
|
||||
* them. Everything else has two weights (see [cardBackground]); `default` has one,
|
||||
* because there is no such thing as an emphatic lack of colour — and re-alphaing
|
||||
* its opaque neutral fill would make a draft card translucent.
|
||||
*/
|
||||
val tintable: Boolean = true,
|
||||
) {
|
||||
fun background(dark: Boolean): Color = if (dark) darkBackground else lightBackground
|
||||
|
||||
/**
|
||||
* A NOTE card's fill, at one of two weights.
|
||||
*
|
||||
* `strong` means the colour was CHOSEN — by a tag, or (until step 5) by the
|
||||
* picker. Subdued means it was derived from the note's id purely so the board is
|
||||
* not a wall of white. Drawing those at the same weight is what prompted the
|
||||
* operator's "the tints look the same as the chosen colors".
|
||||
*
|
||||
* The two weights move in OPPOSITE directions per theme, because that is where
|
||||
* each has headroom. Light cannot go lighter than `-50` without being white again,
|
||||
* so the gap opens by deepening the chosen end to `-100` — which is exactly
|
||||
* [lightChipBackground], already in this table, so no new hex is transcribed.
|
||||
* Dark CAN go lighter, so the derived end drops to a quarter opacity: closer to
|
||||
* the board, which gives the light body text MORE contrast, not less.
|
||||
*
|
||||
* Borders are untouched. The fill is the signal; moving both muddies the edge.
|
||||
*/
|
||||
fun cardBackground(
|
||||
dark: Boolean,
|
||||
strong: Boolean,
|
||||
): Color =
|
||||
when {
|
||||
!tintable -> background(dark)
|
||||
dark -> darkBackground.copy(alpha = if (strong) STRONG_DARK_ALPHA else SUBDUED_DARK_ALPHA)
|
||||
strong -> lightChipBackground
|
||||
else -> lightBackground
|
||||
}
|
||||
|
||||
fun border(dark: Boolean): Color = if (dark) darkBorder else lightBorder
|
||||
|
||||
fun chipBackground(dark: Boolean): Color = if (dark) darkChipBackground else lightChipBackground
|
||||
@@ -40,6 +76,12 @@ data class NoteTint(
|
||||
fun chipForeground(dark: Boolean): Color = if (dark) darkChipForeground else lightChipForeground
|
||||
}
|
||||
|
||||
// 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.
|
||||
private const val SUBDUED_DARK_ALPHA = 0.25f
|
||||
private const val STRONG_DARK_ALPHA = 0.70f
|
||||
|
||||
/** Keyed by the core's colour vocabulary. Order matches the web's picker. */
|
||||
val NOTE_TINTS: Map<String, NoteTint> =
|
||||
mapOf(
|
||||
@@ -54,6 +96,7 @@ val NOTE_TINTS: Map<String, NoteTint> =
|
||||
lightChipForeground = Color(0xFF525252),
|
||||
darkChipBackground = Color(0x1AFFFFFF),
|
||||
darkChipForeground = Color(0xFFD4D4D4),
|
||||
tintable = false,
|
||||
),
|
||||
"red" to
|
||||
NoteTint(
|
||||
@@ -185,10 +228,31 @@ fun noteTint(key: String): NoteTint = NOTE_TINTS[key] ?: NOTE_TINTS.getValue("de
|
||||
*/
|
||||
@Composable
|
||||
@ReadOnlyComposable
|
||||
fun noteTintFor(
|
||||
id: String,
|
||||
color: String,
|
||||
): NoteTint = noteTint(resolvedNoteColor(id, color, NOTE_TINTS.keys))
|
||||
fun noteTintFor(note: Note): NoteTint =
|
||||
noteTint(resolvedNoteColor(note.id, note.color, firstLabelColor(note), NOTE_TINTS.keys))
|
||||
|
||||
/**
|
||||
* Whether this note's card is drawn at the CHOSEN weight — see [NoteTint.cardBackground].
|
||||
*
|
||||
* Not `@Composable`: the card needs it alongside `isSystemInDarkTheme()`, and keeping
|
||||
* it an ordinary function means it can be read anywhere the note is.
|
||||
*/
|
||||
fun noteIsStrong(note: Note): Boolean =
|
||||
noteColorIsChosen(note.color, firstLabelColor(note), NOTE_TINTS.keys)
|
||||
|
||||
/**
|
||||
* The colour of the note's FIRST label, already resolved, or "" when it has none.
|
||||
*
|
||||
* First rather than any other: it is the one the person controls by typing, where
|
||||
* alphabetical or most-used would move a note's colour when an unrelated tag was
|
||||
* added somewhere else. Manual labels count the same as `#tags` — someone looking at
|
||||
* a chip cannot tell which kind they made, and two identically-tagged notes in
|
||||
* different colours for an invisible reason is worse than the rule being loose.
|
||||
*/
|
||||
private fun firstLabelColor(note: Note): String =
|
||||
note.labels.firstOrNull()
|
||||
?.let { resolvedLabelColor(it.name, it.color, NOTE_TINTS.keys) }
|
||||
?: ""
|
||||
|
||||
/**
|
||||
* The tint for a LABEL, derived from its name when nobody has picked one.
|
||||
|
||||
@@ -64,14 +64,14 @@ class DerivedTintTest {
|
||||
@Test
|
||||
fun `an explicitly picked colour still wins`() {
|
||||
val known = DERIVED_TINT_KEYS.toSet() + "default"
|
||||
assertEquals("teal", resolvedNoteColor("any-id", "teal", known))
|
||||
assertEquals("teal", resolvedNoteColor("any-id", "teal", "", known))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `an unknown colour key falls back to the derived tint`() {
|
||||
val known = DERIVED_TINT_KEYS.toSet() + "default"
|
||||
val id = "00000000-0000-0000-0000-000000000000"
|
||||
assertEquals("purple", resolvedNoteColor(id, "chartreuse", known))
|
||||
assertEquals("purple", resolvedNoteColor(id, "chartreuse", "", known))
|
||||
}
|
||||
|
||||
/** `default` is not a choice, it is the absence of one — so a note stored as
|
||||
@@ -81,16 +81,52 @@ class DerivedTintTest {
|
||||
fun `a default colour is treated as no colour`() {
|
||||
val known = DERIVED_TINT_KEYS.toSet() + "default"
|
||||
val id = "11111111-1111-1111-1111-111111111111"
|
||||
assertEquals("blue", resolvedNoteColor(id, "default", known))
|
||||
assertNotEquals("default", resolvedNoteColor(id, "default", known))
|
||||
assertEquals("blue", resolvedNoteColor(id, "default", "", known))
|
||||
assertNotEquals("default", resolvedNoteColor(id, "default", "", known))
|
||||
}
|
||||
|
||||
/** A draft has no id yet. It must not be hashed — see the comment in DerivedTint. */
|
||||
@Test
|
||||
fun `a draft stays default until it has an id`() {
|
||||
val known = DERIVED_TINT_KEYS.toSet() + "default"
|
||||
assertEquals("default", resolvedNoteColor("", "", known))
|
||||
assertEquals("default", resolvedNoteColor("", "default", known))
|
||||
assertEquals("default", resolvedNoteColor("", "", "", known))
|
||||
assertEquals("default", resolvedNoteColor("", "default", "", known))
|
||||
}
|
||||
|
||||
/** The point of the whole milestone: notes sharing a tag share a colour, however
|
||||
* different their ids. Four `#todo` notes in four colours is what started this. */
|
||||
@Test
|
||||
fun `notes sharing a tag share a colour whatever their ids`() {
|
||||
val known = DERIVED_TINT_KEYS.toSet() + "default"
|
||||
val todo = resolvedLabelColor("todo", "default", known)
|
||||
val a = resolvedNoteColor("11111111-1111-1111-1111-111111111111", "default", todo, known)
|
||||
val b = resolvedNoteColor("6ba7b810-9dad-11d1-80b4-00c04fd430c8", "default", todo, known)
|
||||
assertEquals(todo, a)
|
||||
assertEquals(a, b)
|
||||
// ...and it is NOT what either id would have derived on its own.
|
||||
assertNotEquals(derivedTint("11111111-1111-1111-1111-111111111111"), a)
|
||||
}
|
||||
|
||||
/** Until step 5 removes the picker, a hand-picked colour outranks the tag. */
|
||||
@Test
|
||||
fun `an explicit note colour still beats its tag`() {
|
||||
val known = DERIVED_TINT_KEYS.toSet() + "default"
|
||||
val todo = resolvedLabelColor("todo", "default", known)
|
||||
assertNotEquals("teal", todo)
|
||||
assertEquals("teal", resolvedNoteColor("any-id", "teal", todo, known))
|
||||
}
|
||||
|
||||
/** Strength is not a second decision — it IS whether the colour was chosen. */
|
||||
@Test
|
||||
fun `only a chosen colour is drawn strongly`() {
|
||||
val known = DERIVED_TINT_KEYS.toSet() + "default"
|
||||
val todo = resolvedLabelColor("todo", "default", known)
|
||||
assertEquals(true, noteColorIsChosen("default", todo, known))
|
||||
assertEquals(true, noteColorIsChosen("teal", "", known))
|
||||
assertEquals(false, noteColorIsChosen("default", "", known))
|
||||
assertEquals(false, noteColorIsChosen("", "", known))
|
||||
// An unrecognised key is not a choice — it is data we could not read.
|
||||
assertEquals(false, noteColorIsChosen("chartreuse", "", known))
|
||||
}
|
||||
|
||||
/** A tag with no colour of its own derives one from its NAME, which is what makes
|
||||
|
||||
@@ -3,12 +3,11 @@ import { computed, onBeforeUnmount, ref, watch } from "vue";
|
||||
import { useNotesStore } from "../stores/notes";
|
||||
import {
|
||||
LABEL_CHIP_CLASSES,
|
||||
NOTE_CARD_CLASSES,
|
||||
NOTE_COLOR_KEYS,
|
||||
NOTE_COLOR_LABELS,
|
||||
NOTE_SWATCH_CLASSES,
|
||||
noteCardClasses,
|
||||
resolveLabelColor,
|
||||
resolveNoteColor,
|
||||
type NoteColor,
|
||||
} from "../notes/colors";
|
||||
import type { Note } from "../stores/notes";
|
||||
@@ -191,12 +190,6 @@ async function snoozeReminder(minutes: number): Promise<void> {
|
||||
emit("reminder-changed");
|
||||
}
|
||||
|
||||
// Takes the note, not its colour: a note with no colour of its own gets one
|
||||
// derived from its id, so the card can no longer be painted from a single field.
|
||||
function cardClass(note: Note): string {
|
||||
return NOTE_CARD_CLASSES[resolveNoteColor(note)] ?? NOTE_CARD_CLASSES.default;
|
||||
}
|
||||
|
||||
// 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 {
|
||||
@@ -231,7 +224,7 @@ onBeforeUnmount(() => document.removeEventListener("mousedown", onDocMousedown))
|
||||
ref="root"
|
||||
class="group relative mb-4 break-inside-avoid rounded-xl border p-3 shadow-sm transition hover:shadow-md"
|
||||
:class="[
|
||||
cardClass(note),
|
||||
noteCardClasses(note),
|
||||
dragging ? 'opacity-40' : '',
|
||||
dragOver
|
||||
? 'scale-[1.02] shadow-lg ring-2 ring-brand ring-offset-2 ring-offset-white dark:ring-offset-neutral-950'
|
||||
|
||||
@@ -19,15 +19,15 @@ export type NoteColor = (typeof NOTE_COLOR_KEYS)[number];
|
||||
|
||||
export const NOTE_CARD_CLASSES: Record<NoteColor, string> = {
|
||||
default: "bg-white border-neutral-200 dark:bg-neutral-900 dark:border-neutral-700",
|
||||
red: "bg-red-50 border-red-200 dark:bg-red-950/40 dark:border-red-900",
|
||||
orange: "bg-orange-50 border-orange-200 dark:bg-orange-950/40 dark:border-orange-900",
|
||||
yellow: "bg-amber-50 border-amber-200 dark:bg-amber-950/40 dark:border-amber-900",
|
||||
green: "bg-green-50 border-green-200 dark:bg-green-950/40 dark:border-green-900",
|
||||
teal: "bg-teal-50 border-teal-200 dark:bg-teal-950/40 dark:border-teal-900",
|
||||
blue: "bg-blue-50 border-blue-200 dark:bg-blue-950/40 dark:border-blue-900",
|
||||
purple: "bg-purple-50 border-purple-200 dark:bg-purple-950/40 dark:border-purple-900",
|
||||
pink: "bg-pink-50 border-pink-200 dark:bg-pink-950/40 dark:border-pink-900",
|
||||
gray: "bg-neutral-100 border-neutral-300 dark:bg-neutral-800 dark:border-neutral-700",
|
||||
red: "bg-red-50 border-red-200 dark:bg-red-950/25 dark:border-red-900",
|
||||
orange: "bg-orange-50 border-orange-200 dark:bg-orange-950/25 dark:border-orange-900",
|
||||
yellow: "bg-amber-50 border-amber-200 dark:bg-amber-950/25 dark:border-amber-900",
|
||||
green: "bg-green-50 border-green-200 dark:bg-green-950/25 dark:border-green-900",
|
||||
teal: "bg-teal-50 border-teal-200 dark:bg-teal-950/25 dark:border-teal-900",
|
||||
blue: "bg-blue-50 border-blue-200 dark:bg-blue-950/25 dark:border-blue-900",
|
||||
purple: "bg-purple-50 border-purple-200 dark:bg-purple-950/25 dark:border-purple-900",
|
||||
pink: "bg-pink-50 border-pink-200 dark:bg-pink-950/25 dark:border-pink-900",
|
||||
gray: "bg-neutral-100 border-neutral-300 dark:bg-neutral-800/25 dark:border-neutral-700",
|
||||
};
|
||||
|
||||
export const NOTE_SWATCH_CLASSES: Record<NoteColor, string> = {
|
||||
@@ -143,23 +143,6 @@ export function derivedTint(id: string): NoteColor {
|
||||
return DERIVED_TINT_KEYS[tintHash(id) % DERIVED_TINT_KEYS.length];
|
||||
}
|
||||
|
||||
/**
|
||||
* The colour to actually paint a note.
|
||||
*
|
||||
* An explicitly-picked colour still wins — the picker is on its way out
|
||||
* (milestone 309 step 5) but it has not gone yet, and a note the operator
|
||||
* coloured by hand changing under them would read as data loss.
|
||||
*/
|
||||
export function resolveNoteColor(note: { id: string; color?: string | null }): NoteColor {
|
||||
const picked = note.color as NoteColor | undefined | null;
|
||||
if (picked && picked !== "default" && picked in NOTE_CARD_CLASSES) return picked;
|
||||
// No id yet means an unsaved draft: nothing to derive from. Staying white until the
|
||||
// note exists costs one colour change at save time; hashing the empty string would
|
||||
// give every draft the same tint and then change it anyway.
|
||||
if (!note.id) return "default";
|
||||
return derivedTint(note.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* The colour to paint a LABEL — its chip, and (step 3) every note carrying it.
|
||||
*
|
||||
@@ -208,3 +191,73 @@ export function resolveLabelColor(label: { name: string; color?: string | null }
|
||||
// Note `work`/`ideas` and `home`/`reading` collide. Nine keys makes that unavoidable
|
||||
// and it is not a bug: colour hints that two notes are related, it never claims they
|
||||
// carry the same tag. The chip's text is what says which tag it is.
|
||||
|
||||
// The FULL-strength ramp: what a note wears when its colour was CHOSEN — by a tag, or
|
||||
// (until step 5) by the picker. One Tailwind step deeper in light mode, and a much
|
||||
// heavier fill in dark.
|
||||
//
|
||||
// The two ramps move in opposite directions per theme, because that is where each has
|
||||
// headroom. Light mode cannot go lighter than `-50` without being white again, so the
|
||||
// gap is opened by deepening the tagged end to `-100`. Dark mode CAN go lighter, so
|
||||
// the untagged end drops to `/25` — closer to the board, which also gives the light
|
||||
// body text MORE contrast rather than less.
|
||||
//
|
||||
// Borders stay put. The fill is the signal; moving both just muddies the edge.
|
||||
export const NOTE_CARD_CLASSES_STRONG: Record<NoteColor, string> = {
|
||||
default: "bg-white border-neutral-200 dark:bg-neutral-900 dark:border-neutral-700",
|
||||
red: "bg-red-100 border-red-200 dark:bg-red-950/70 dark:border-red-900",
|
||||
orange: "bg-orange-100 border-orange-200 dark:bg-orange-950/70 dark:border-orange-900",
|
||||
yellow: "bg-amber-100 border-amber-200 dark:bg-amber-950/70 dark:border-amber-900",
|
||||
green: "bg-green-100 border-green-200 dark:bg-green-950/70 dark:border-green-900",
|
||||
teal: "bg-teal-100 border-teal-200 dark:bg-teal-950/70 dark:border-teal-900",
|
||||
blue: "bg-blue-100 border-blue-200 dark:bg-blue-950/70 dark:border-blue-900",
|
||||
purple: "bg-purple-100 border-purple-200 dark:bg-purple-950/70 dark:border-purple-900",
|
||||
pink: "bg-pink-100 border-pink-200 dark:bg-pink-950/70 dark:border-pink-900",
|
||||
gray: "bg-neutral-200 border-neutral-300 dark:bg-neutral-800/70 dark:border-neutral-700",
|
||||
};
|
||||
|
||||
/**
|
||||
* The colour a note wears AND how strongly, in one answer.
|
||||
*
|
||||
* `strong` is not a second decision — it IS whether the colour was chosen. A tag (or,
|
||||
* until step 5, the picker) means somebody said what this note is; a derived tint only
|
||||
* means the board should not be a wall of white. Rendering those two at the same
|
||||
* weight is what made the operator ask "the tints look the same as the chosen colors".
|
||||
*
|
||||
* Resolution order, and why: an explicit pick beats a tag because it is the more
|
||||
* specific statement and the picker still exists. The FIRST label wins among tags —
|
||||
* it is the one the person controls by typing, where alphabetical or most-used would
|
||||
* move a note's colour when an unrelated tag was added somewhere else.
|
||||
*
|
||||
* Manual labels count the same as `#tags`. Someone looking at a chip cannot tell which
|
||||
* kind they made, and two identically-tagged notes in different colours for an
|
||||
* invisible reason is worse than the rule being slightly loose.
|
||||
*/
|
||||
export function resolveNoteTint(note: {
|
||||
id: string;
|
||||
color?: string | null;
|
||||
labels?: { name: string; color: string }[];
|
||||
}): { color: NoteColor; strong: boolean } {
|
||||
const picked = note.color as NoteColor | undefined | null;
|
||||
if (picked && picked !== "default" && picked in NOTE_CARD_CLASSES) {
|
||||
return { color: picked, strong: true };
|
||||
}
|
||||
const first = note.labels?.[0];
|
||||
if (first) return { color: resolveLabelColor(first), strong: true };
|
||||
// No id yet means an unsaved draft: nothing to derive from. Staying white until the
|
||||
// note exists costs one colour change at save time; hashing the empty string would
|
||||
// give every draft the same tint and then change it anyway.
|
||||
if (!note.id) return { color: "default", strong: false };
|
||||
return { color: derivedTint(note.id), strong: false };
|
||||
}
|
||||
|
||||
/** The card classes for a note — both ramps behind one call. */
|
||||
export function noteCardClasses(note: {
|
||||
id: string;
|
||||
color?: string | null;
|
||||
labels?: { name: string; color: string }[];
|
||||
}): string {
|
||||
const { color, strong } = resolveNoteTint(note);
|
||||
const ramp = strong ? NOTE_CARD_CLASSES_STRONG : NOTE_CARD_CLASSES;
|
||||
return ramp[color] ?? NOTE_CARD_CLASSES.default;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user