Checklists in the body, colour from tags, and commit-derived CalVer #4
@@ -114,10 +114,14 @@ fun resolvedLabelColor(
|
|||||||
fun resolvedNoteColor(
|
fun resolvedNoteColor(
|
||||||
id: String,
|
id: String,
|
||||||
color: String,
|
color: String,
|
||||||
|
labelColor: String,
|
||||||
known: Set<String>,
|
known: Set<String>,
|
||||||
): String =
|
): String =
|
||||||
when {
|
when {
|
||||||
color.isNotEmpty() && color != "default" && color in known -> color
|
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.
|
// 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;
|
// 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
|
// hashing the empty string instead would give EVERY draft the same tint and
|
||||||
@@ -125,3 +129,18 @@ fun resolvedNoteColor(
|
|||||||
id.isEmpty() -> "default"
|
id.isEmpty() -> "default"
|
||||||
else -> derivedTint(id)
|
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,
|
onToggleItem: (Int, Boolean) -> Unit,
|
||||||
) {
|
) {
|
||||||
val dark = isSystemInDarkTheme()
|
val dark = isSystemInDarkTheme()
|
||||||
val tint = noteTintFor(note.id, note.color)
|
val tint = noteTintFor(note)
|
||||||
|
val strong = noteIsStrong(note)
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier =
|
modifier =
|
||||||
@@ -47,7 +48,7 @@ fun NoteCard(
|
|||||||
// rounded corners instead of a rectangle overhanging them.
|
// rounded corners instead of a rectangle overhanging them.
|
||||||
.clip(RoundedCornerShape(CARD_RADIUS))
|
.clip(RoundedCornerShape(CARD_RADIUS))
|
||||||
.clickable(onClickLabel = stringResource(R.string.board_open_note), onClick = onOpen)
|
.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))
|
.border(1.dp, tint.border(dark), RoundedCornerShape(CARD_RADIUS))
|
||||||
.padding(12.dp),
|
.padding(12.dp),
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -63,7 +63,8 @@ fun NoteEditorScreen(
|
|||||||
onAction: (EditorAction) -> Unit,
|
onAction: (EditorAction) -> Unit,
|
||||||
) {
|
) {
|
||||||
val dark = isSystemInDarkTheme()
|
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
|
// 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
|
// needs a key — but a draft's id changes the moment it is first saved, and
|
||||||
@@ -155,7 +156,7 @@ fun NoteEditorScreen(
|
|||||||
Surface(
|
Surface(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
shape = RoundedCornerShape(topStart = SHEET_CORNER, topEnd = SHEET_CORNER),
|
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
|
// Both content colours are spelled out for the reason the toolbar had to
|
||||||
// be: Surface and Scaffold each default theirs to contentColorFor(their
|
// be: Surface and Scaffold each default theirs to contentColorFor(their
|
||||||
// container), which returns Unspecified for anything that is not a
|
// container), which returns Unspecified for anything that is not a
|
||||||
@@ -166,7 +167,7 @@ fun NoteEditorScreen(
|
|||||||
contentColor = MaterialTheme.colorScheme.onSurface,
|
contentColor = MaterialTheme.colorScheme.onSurface,
|
||||||
) {
|
) {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
containerColor = tint.background(dark),
|
containerColor = tint.cardBackground(dark, strong),
|
||||||
contentColor = MaterialTheme.colorScheme.onSurface,
|
contentColor = MaterialTheme.colorScheme.onSurface,
|
||||||
topBar = {
|
topBar = {
|
||||||
EditorTopBar(
|
EditorTopBar(
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.fabledsword.thoughtsync.ui
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.ReadOnlyComposable
|
import androidx.compose.runtime.ReadOnlyComposable
|
||||||
import androidx.compose.ui.graphics.Color
|
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.
|
* The note colour palette, matching `frontend/src/notes/colors.ts` VALUE FOR VALUE.
|
||||||
@@ -30,9 +31,44 @@ data class NoteTint(
|
|||||||
val lightChipForeground: Color,
|
val lightChipForeground: Color,
|
||||||
val darkChipBackground: Color,
|
val darkChipBackground: Color,
|
||||||
val darkChipForeground: 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
|
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 border(dark: Boolean): Color = if (dark) darkBorder else lightBorder
|
||||||
|
|
||||||
fun chipBackground(dark: Boolean): Color = if (dark) darkChipBackground else lightChipBackground
|
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
|
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. */
|
/** Keyed by the core's colour vocabulary. Order matches the web's picker. */
|
||||||
val NOTE_TINTS: Map<String, NoteTint> =
|
val NOTE_TINTS: Map<String, NoteTint> =
|
||||||
mapOf(
|
mapOf(
|
||||||
@@ -54,6 +96,7 @@ val NOTE_TINTS: Map<String, NoteTint> =
|
|||||||
lightChipForeground = Color(0xFF525252),
|
lightChipForeground = Color(0xFF525252),
|
||||||
darkChipBackground = Color(0x1AFFFFFF),
|
darkChipBackground = Color(0x1AFFFFFF),
|
||||||
darkChipForeground = Color(0xFFD4D4D4),
|
darkChipForeground = Color(0xFFD4D4D4),
|
||||||
|
tintable = false,
|
||||||
),
|
),
|
||||||
"red" to
|
"red" to
|
||||||
NoteTint(
|
NoteTint(
|
||||||
@@ -185,10 +228,31 @@ fun noteTint(key: String): NoteTint = NOTE_TINTS[key] ?: NOTE_TINTS.getValue("de
|
|||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
@ReadOnlyComposable
|
@ReadOnlyComposable
|
||||||
fun noteTintFor(
|
fun noteTintFor(note: Note): NoteTint =
|
||||||
id: String,
|
noteTint(resolvedNoteColor(note.id, note.color, firstLabelColor(note), NOTE_TINTS.keys))
|
||||||
color: String,
|
|
||||||
): NoteTint = noteTint(resolvedNoteColor(id, color, 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.
|
* The tint for a LABEL, derived from its name when nobody has picked one.
|
||||||
|
|||||||
@@ -64,14 +64,14 @@ class DerivedTintTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `an explicitly picked colour still wins`() {
|
fun `an explicitly picked colour still wins`() {
|
||||||
val known = DERIVED_TINT_KEYS.toSet() + "default"
|
val known = DERIVED_TINT_KEYS.toSet() + "default"
|
||||||
assertEquals("teal", resolvedNoteColor("any-id", "teal", known))
|
assertEquals("teal", resolvedNoteColor("any-id", "teal", "", known))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `an unknown colour key falls back to the derived tint`() {
|
fun `an unknown colour key falls back to the derived tint`() {
|
||||||
val known = DERIVED_TINT_KEYS.toSet() + "default"
|
val known = DERIVED_TINT_KEYS.toSet() + "default"
|
||||||
val id = "00000000-0000-0000-0000-000000000000"
|
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
|
/** `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`() {
|
fun `a default colour is treated as no colour`() {
|
||||||
val known = DERIVED_TINT_KEYS.toSet() + "default"
|
val known = DERIVED_TINT_KEYS.toSet() + "default"
|
||||||
val id = "11111111-1111-1111-1111-111111111111"
|
val id = "11111111-1111-1111-1111-111111111111"
|
||||||
assertEquals("blue", resolvedNoteColor(id, "default", known))
|
assertEquals("blue", resolvedNoteColor(id, "default", "", known))
|
||||||
assertNotEquals("default", 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. */
|
/** A draft has no id yet. It must not be hashed — see the comment in DerivedTint. */
|
||||||
@Test
|
@Test
|
||||||
fun `a draft stays default until it has an id`() {
|
fun `a draft stays default until it has an id`() {
|
||||||
val known = DERIVED_TINT_KEYS.toSet() + "default"
|
val known = DERIVED_TINT_KEYS.toSet() + "default"
|
||||||
assertEquals("default", resolvedNoteColor("", "", known))
|
assertEquals("default", resolvedNoteColor("", "", "", known))
|
||||||
assertEquals("default", resolvedNoteColor("", "default", 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
|
/** 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 { useNotesStore } from "../stores/notes";
|
||||||
import {
|
import {
|
||||||
LABEL_CHIP_CLASSES,
|
LABEL_CHIP_CLASSES,
|
||||||
NOTE_CARD_CLASSES,
|
|
||||||
NOTE_COLOR_KEYS,
|
NOTE_COLOR_KEYS,
|
||||||
NOTE_COLOR_LABELS,
|
NOTE_COLOR_LABELS,
|
||||||
NOTE_SWATCH_CLASSES,
|
NOTE_SWATCH_CLASSES,
|
||||||
|
noteCardClasses,
|
||||||
resolveLabelColor,
|
resolveLabelColor,
|
||||||
resolveNoteColor,
|
|
||||||
type NoteColor,
|
type NoteColor,
|
||||||
} from "../notes/colors";
|
} from "../notes/colors";
|
||||||
import type { Note } from "../stores/notes";
|
import type { Note } from "../stores/notes";
|
||||||
@@ -191,12 +190,6 @@ async function snoozeReminder(minutes: number): Promise<void> {
|
|||||||
emit("reminder-changed");
|
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
|
// 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.
|
// name, so chips carry the tag's identity rather than all being the same grey.
|
||||||
function labelChip(label: { name: string; color: string }): string {
|
function labelChip(label: { name: string; color: string }): string {
|
||||||
@@ -231,7 +224,7 @@ onBeforeUnmount(() => document.removeEventListener("mousedown", onDocMousedown))
|
|||||||
ref="root"
|
ref="root"
|
||||||
class="group relative mb-4 break-inside-avoid rounded-xl border p-3 shadow-sm transition hover:shadow-md"
|
class="group relative mb-4 break-inside-avoid rounded-xl border p-3 shadow-sm transition hover:shadow-md"
|
||||||
:class="[
|
:class="[
|
||||||
cardClass(note),
|
noteCardClasses(note),
|
||||||
dragging ? 'opacity-40' : '',
|
dragging ? 'opacity-40' : '',
|
||||||
dragOver
|
dragOver
|
||||||
? 'scale-[1.02] shadow-lg ring-2 ring-brand ring-offset-2 ring-offset-white dark:ring-offset-neutral-950'
|
? '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> = {
|
export const NOTE_CARD_CLASSES: Record<NoteColor, string> = {
|
||||||
default: "bg-white border-neutral-200 dark:bg-neutral-900 dark:border-neutral-700",
|
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",
|
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/40 dark:border-orange-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/40 dark:border-amber-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/40 dark:border-green-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/40 dark:border-teal-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/40 dark:border-blue-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/40 dark:border-purple-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/40 dark:border-pink-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 dark:border-neutral-700",
|
gray: "bg-neutral-100 border-neutral-300 dark:bg-neutral-800/25 dark:border-neutral-700",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const NOTE_SWATCH_CLASSES: Record<NoteColor, string> = {
|
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];
|
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.
|
* 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
|
// 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
|
// 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.
|
// 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