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:
@@ -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