Checklists in the body, colour from tags, and commit-derived CalVer #4
@@ -71,6 +71,36 @@ fun derivedTint(id: String): String {
|
|||||||
return DERIVED_TINT_KEYS[index.toInt()]
|
return DERIVED_TINT_KEYS[index.toInt()]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The colour key for a LABEL — its chip, and (step 3) every note carrying it.
|
||||||
|
*
|
||||||
|
* Derived from the tag's NAME when nobody has picked one. Every `#tag` ever typed is
|
||||||
|
* currently `default`: the server mints one as `Label(owner_id=…, name=name)` with no
|
||||||
|
* colour, so tag-driven note colour against that would leave the board exactly as
|
||||||
|
* grey as it was.
|
||||||
|
*
|
||||||
|
* DERIVED RATHER THAN PERSISTED AT MINT TIME, reversing #2965's plan. That plan wanted
|
||||||
|
* a hashed colour written at each of the four places a label can be born — and named
|
||||||
|
* the risk itself: `find_or_create_label` is "easy to miss, and it is the common one",
|
||||||
|
* since most tags are born from typing `#grocery`, not from a management screen.
|
||||||
|
* Deriving has no mint points to miss and no backfill for the tags already out there.
|
||||||
|
* The cost is that renaming a tag recolours it, which is fair: the name IS the tag.
|
||||||
|
*
|
||||||
|
* Lowercased because tags dedupe case-insensitively — `#Todo` renamed to `#todo` is
|
||||||
|
* the same tag and should not change colour. Kotlin's `lowercase()` and the web's
|
||||||
|
* `toLowerCase()` are both locale-independent, so the mirror holds.
|
||||||
|
*/
|
||||||
|
fun resolvedLabelColor(
|
||||||
|
name: String,
|
||||||
|
color: String,
|
||||||
|
known: Set<String>,
|
||||||
|
): String =
|
||||||
|
when {
|
||||||
|
color.isNotEmpty() && color != "default" && color in known -> color
|
||||||
|
name.isEmpty() -> "default"
|
||||||
|
else -> derivedTint(name.lowercase())
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The colour key to actually paint a note with.
|
* The colour key to actually paint a note with.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -330,7 +330,7 @@ fun EditorLabelRow(
|
|||||||
val dark = isSystemInDarkTheme()
|
val dark = isSystemInDarkTheme()
|
||||||
Column(modifier = Modifier.padding(top = 12.dp)) {
|
Column(modifier = Modifier.padding(top = 12.dp)) {
|
||||||
note.labels.forEach { label ->
|
note.labels.forEach { label ->
|
||||||
val tint = noteTint(label.color)
|
val tint = labelTintFor(label.name, label.color)
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = Modifier.padding(vertical = 2.dp),
|
modifier = Modifier.padding(vertical = 2.dp),
|
||||||
|
|||||||
@@ -182,7 +182,7 @@ private fun LabelChips(labels: List<NoteLabel>) {
|
|||||||
// not grow taller than its content. The editor shows the full set.
|
// not grow taller than its content. The editor shows the full set.
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||||
labels.take(MAX_LABEL_CHIPS).forEach { label ->
|
labels.take(MAX_LABEL_CHIPS).forEach { label ->
|
||||||
val tint = noteTint(label.color)
|
val tint = labelTintFor(label.name, label.color)
|
||||||
Text(
|
Text(
|
||||||
text = label.name,
|
text = label.name,
|
||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
|||||||
@@ -189,3 +189,17 @@ fun noteTintFor(
|
|||||||
id: String,
|
id: String,
|
||||||
color: String,
|
color: String,
|
||||||
): NoteTint = noteTint(resolvedNoteColor(id, color, NOTE_TINTS.keys))
|
): NoteTint = noteTint(resolvedNoteColor(id, color, NOTE_TINTS.keys))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The tint for a LABEL, derived from its name when nobody has picked one.
|
||||||
|
*
|
||||||
|
* Every `#tag` is born colourless, so without this a board of tags is a board of
|
||||||
|
* identical grey chips. See `DerivedTint.kt` for why this derives rather than
|
||||||
|
* persisting a colour when the tag is minted.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
@ReadOnlyComposable
|
||||||
|
fun labelTintFor(
|
||||||
|
name: String,
|
||||||
|
color: String,
|
||||||
|
): NoteTint = noteTint(resolvedLabelColor(name, color, NOTE_TINTS.keys))
|
||||||
|
|||||||
@@ -93,6 +93,52 @@ class DerivedTintTest {
|
|||||||
assertEquals("default", resolvedNoteColor("", "default", known))
|
assertEquals("default", resolvedNoteColor("", "default", known))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** A tag with no colour of its own derives one from its NAME, which is what makes
|
||||||
|
* every `#todo` note the same colour rather than nine different ones. */
|
||||||
|
@Test
|
||||||
|
fun `a label with no colour derives one from its name`() {
|
||||||
|
val known = DERIVED_TINT_KEYS.toSet() + "default"
|
||||||
|
val todo = resolvedLabelColor("todo", "default", known)
|
||||||
|
assertEquals(derivedTint("todo"), todo)
|
||||||
|
assertNotEquals("default", todo)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Tags dedupe case-insensitively, so `#Todo` and `#todo` are one tag and must not
|
||||||
|
* be two colours. This is the whole reason the name is lowercased first. */
|
||||||
|
@Test
|
||||||
|
fun `label colour ignores case`() {
|
||||||
|
val known = DERIVED_TINT_KEYS.toSet() + "default"
|
||||||
|
assertEquals(
|
||||||
|
resolvedLabelColor("todo", "default", known),
|
||||||
|
resolvedLabelColor("ToDo", "default", known),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** `teal` deliberately, NOT the colour "todo" derives to (pink) — asserting the
|
||||||
|
* derived value here would pass even with the explicit branch deleted. */
|
||||||
|
@Test
|
||||||
|
fun `an explicitly picked label colour still wins`() {
|
||||||
|
val known = DERIVED_TINT_KEYS.toSet() + "default"
|
||||||
|
assertNotEquals("teal", derivedTint("todo"))
|
||||||
|
assertEquals("teal", resolvedLabelColor("todo", "teal", known))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The spread is real, and collisions are real too.
|
||||||
|
*
|
||||||
|
* Nine keys means two tags sharing a colour is not a bug and cannot be designed
|
||||||
|
* out — in this very sample `home`/`reading` are both gray and `work`/`ideas` are
|
||||||
|
* both green. Colour is a hint that two notes are related, never a claim that they
|
||||||
|
* carry the same tag; the chip's TEXT is what says which tag it is.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
fun `different tag names spread across the palette`() {
|
||||||
|
val known = DERIVED_TINT_KEYS.toSet() + "default"
|
||||||
|
val names = listOf("todo", "grocery", "work", "home", "ideas", "reading", "urgent")
|
||||||
|
val colours = names.map { resolvedLabelColor(it, "default", known) }
|
||||||
|
assertEquals(true, colours.toSet().size >= 5)
|
||||||
|
}
|
||||||
|
|
||||||
/** The reason the feature exists: two adjacent notes should not look identical. */
|
/** The reason the feature exists: two adjacent notes should not look identical. */
|
||||||
@Test
|
@Test
|
||||||
fun `the tint spreads across the palette`() {
|
fun `the tint spreads across the palette`() {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import ImportNotes from "./ImportNotes.vue";
|
|||||||
import LabelsModal from "./LabelsModal.vue";
|
import LabelsModal from "./LabelsModal.vue";
|
||||||
import { isDesktop } from "../desktop/bridge";
|
import { isDesktop } from "../desktop/bridge";
|
||||||
import { facetsToQuery } from "../notes/facets";
|
import { facetsToQuery } from "../notes/facets";
|
||||||
import { NOTE_SWATCH_CLASSES, type NoteColor } from "../notes/colors";
|
import { NOTE_SWATCH_CLASSES, resolveLabelColor } from "../notes/colors";
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -173,8 +173,10 @@ onBeforeUnmount(() => {
|
|||||||
|
|
||||||
const currentLabelId = computed(() => (route.name === "label" ? String(route.params.id) : null));
|
const currentLabelId = computed(() => (route.name === "label" ? String(route.params.id) : null));
|
||||||
|
|
||||||
function labelDot(color: string): string {
|
// The drawer's tag list. Same resolution as every other chip and dot — a tag that is
|
||||||
return NOTE_SWATCH_CLASSES[color as NoteColor] ?? NOTE_SWATCH_CLASSES.default;
|
// green on a card must be green here, or the sidebar stops being a way to find it.
|
||||||
|
function labelDot(label: { name: string; color: string }): string {
|
||||||
|
return NOTE_SWATCH_CLASSES[resolveLabelColor(label)] ?? NOTE_SWATCH_CLASSES.default;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The board lenses — the routes a search can happen *within*. Searching while looking
|
// The board lenses — the routes a search can happen *within*. Searching while looking
|
||||||
@@ -443,7 +445,7 @@ async function signOut() {
|
|||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="h-2.5 w-2.5 shrink-0 rounded-full border border-black/10 dark:border-white/15"
|
class="h-2.5 w-2.5 shrink-0 rounded-full border border-black/10 dark:border-white/15"
|
||||||
:class="labelDot(lb.color)"
|
:class="labelDot(lb)"
|
||||||
></span>
|
></span>
|
||||||
<span class="truncate">{{ lb.name }}</span>
|
<span class="truncate">{{ lb.name }}</span>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from "vue";
|
import { computed, ref } from "vue";
|
||||||
import { useLabelsStore, type Label } from "../stores/labels";
|
import { useLabelsStore, type Label } from "../stores/labels";
|
||||||
import { NOTE_COLOR_KEYS, NOTE_COLOR_LABELS, NOTE_SWATCH_CLASSES, type NoteColor } from "../notes/colors";
|
import {
|
||||||
|
NOTE_COLOR_KEYS,
|
||||||
|
NOTE_COLOR_LABELS,
|
||||||
|
NOTE_SWATCH_CLASSES,
|
||||||
|
resolveLabelColor,
|
||||||
|
type NoteColor,
|
||||||
|
} from "../notes/colors";
|
||||||
import BaseModal from "./BaseModal.vue";
|
import BaseModal from "./BaseModal.vue";
|
||||||
import Icon from "./Icon.vue";
|
import Icon from "./Icon.vue";
|
||||||
|
|
||||||
@@ -25,8 +31,12 @@ async function rename(id: string, value: string) {
|
|||||||
if (name) await labels.rename(id, name);
|
if (name) await labels.rename(id, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
function labelDot(color: string): string {
|
// Shows the colour the tag ACTUALLY wears, derived from its name when nobody has
|
||||||
return NOTE_SWATCH_CLASSES[color as NoteColor] ?? NOTE_SWATCH_CLASSES.default;
|
// picked one — so this screen agrees with the chips everywhere else. The ring in the
|
||||||
|
// swatch grid below follows the same resolution, so opening the picker highlights
|
||||||
|
// what you can already see rather than nothing at all.
|
||||||
|
function labelDot(label: { name: string; color: string }): string {
|
||||||
|
return NOTE_SWATCH_CLASSES[resolveLabelColor(label)] ?? NOTE_SWATCH_CLASSES.default;
|
||||||
}
|
}
|
||||||
|
|
||||||
function openColor(id: string) {
|
function openColor(id: string) {
|
||||||
@@ -75,8 +85,8 @@ async function doMerge(sourceId: string, targetId: string) {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="h-4 w-4 shrink-0 rounded-full border border-black/10 transition hover:scale-110 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand dark:border-white/15"
|
class="h-4 w-4 shrink-0 rounded-full border border-black/10 transition hover:scale-110 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand dark:border-white/15"
|
||||||
:class="labelDot(lb.color)"
|
:class="labelDot(lb)"
|
||||||
:title="`Color: ${NOTE_COLOR_LABELS[(lb.color as NoteColor)] ?? lb.color}`"
|
:title="`Color: ${NOTE_COLOR_LABELS[resolveLabelColor(lb)]}`"
|
||||||
aria-label="Change label color"
|
aria-label="Change label color"
|
||||||
@click="openColor(lb.id)"
|
@click="openColor(lb.id)"
|
||||||
/>
|
/>
|
||||||
@@ -120,7 +130,7 @@ async function doMerge(sourceId: string, targetId: string) {
|
|||||||
:title="NOTE_COLOR_LABELS[key]"
|
:title="NOTE_COLOR_LABELS[key]"
|
||||||
:aria-label="NOTE_COLOR_LABELS[key]"
|
:aria-label="NOTE_COLOR_LABELS[key]"
|
||||||
class="h-6 w-6 rounded-full border border-black/10 transition hover:scale-110 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand"
|
class="h-6 w-6 rounded-full border border-black/10 transition hover:scale-110 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand"
|
||||||
:class="[NOTE_SWATCH_CLASSES[key], lb.color === key ? 'ring-2 ring-brand' : '']"
|
:class="[NOTE_SWATCH_CLASSES[key], resolveLabelColor(lb) === key ? 'ring-2 ring-brand' : '']"
|
||||||
@click="pickColor(lb.id, key)"
|
@click="pickColor(lb.id, key)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -140,7 +150,7 @@ async function doMerge(sourceId: string, targetId: string) {
|
|||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="h-2.5 w-2.5 shrink-0 rounded-full border border-black/10 dark:border-white/15"
|
class="h-2.5 w-2.5 shrink-0 rounded-full border border-black/10 dark:border-white/15"
|
||||||
:class="labelDot(t.color)"
|
:class="labelDot(t)"
|
||||||
></span>
|
></span>
|
||||||
<span class="truncate">{{ t.name }}</span>
|
<span class="truncate">{{ t.name }}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
NOTE_COLOR_KEYS,
|
NOTE_COLOR_KEYS,
|
||||||
NOTE_COLOR_LABELS,
|
NOTE_COLOR_LABELS,
|
||||||
NOTE_SWATCH_CLASSES,
|
NOTE_SWATCH_CLASSES,
|
||||||
|
resolveLabelColor,
|
||||||
resolveNoteColor,
|
resolveNoteColor,
|
||||||
type NoteColor,
|
type NoteColor,
|
||||||
} from "../notes/colors";
|
} from "../notes/colors";
|
||||||
@@ -196,8 +197,10 @@ function cardClass(note: Note): string {
|
|||||||
return NOTE_CARD_CLASSES[resolveNoteColor(note)] ?? NOTE_CARD_CLASSES.default;
|
return NOTE_CARD_CLASSES[resolveNoteColor(note)] ?? NOTE_CARD_CLASSES.default;
|
||||||
}
|
}
|
||||||
|
|
||||||
function labelChip(color: string): string {
|
// Takes the label, not its colour: a tag nobody has coloured derives one from its
|
||||||
return LABEL_CHIP_CLASSES[color as NoteColor] ?? LABEL_CHIP_CLASSES.default;
|
// 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Per-card color popover (recolor without opening the editor).
|
// Per-card color popover (recolor without opening the editor).
|
||||||
@@ -300,7 +303,7 @@ onBeforeUnmount(() => document.removeEventListener("mousedown", onDocMousedown))
|
|||||||
v-for="lb in note.labels"
|
v-for="lb in note.labels"
|
||||||
:key="lb.id"
|
:key="lb.id"
|
||||||
class="rounded-full px-2 py-0.5 text-xs"
|
class="rounded-full px-2 py-0.5 text-xs"
|
||||||
:class="labelChip(lb.color)"
|
:class="labelChip(lb)"
|
||||||
>{{ lb.via_tag ? "#" + lb.name : lb.name }}</span
|
>{{ lb.via_tag ? "#" + lb.name : lb.name }}</span
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { fromLocalInput, toLocalInput } from "../notes/datetime";
|
|||||||
import { takeMorphOrigin } from "../composables/useEditorMorph";
|
import { takeMorphOrigin } from "../composables/useEditorMorph";
|
||||||
import { prefersReducedMotion } from "../composables/useReducedMotion";
|
import { prefersReducedMotion } from "../composables/useReducedMotion";
|
||||||
import type { Note, NoteLabel, NoteRevision } from "../stores/notes";
|
import type { Note, NoteLabel, NoteRevision } from "../stores/notes";
|
||||||
import { LABEL_CHIP_CLASSES, type NoteColor } from "../notes/colors";
|
import { LABEL_CHIP_CLASSES, resolveLabelColor, type NoteColor } from "../notes/colors";
|
||||||
import {
|
import {
|
||||||
afterEnter,
|
afterEnter,
|
||||||
type EditorBlock,
|
type EditorBlock,
|
||||||
@@ -358,8 +358,11 @@ async function onLabelsChange(next: NoteLabel[]) {
|
|||||||
async function removeLabel(id: string) {
|
async function removeLabel(id: string) {
|
||||||
await onLabelsChange(labelList.value.filter((lb) => lb.id !== id));
|
await onLabelsChange(labelList.value.filter((lb) => lb.id !== id));
|
||||||
}
|
}
|
||||||
function labelChip(c: string): string {
|
// Takes the label, not its colour — a tag nobody has coloured derives one from its
|
||||||
return LABEL_CHIP_CLASSES[c as NoteColor] ?? LABEL_CHIP_CLASSES.default;
|
// 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 ----
|
// ---- add a checklist ----
|
||||||
@@ -608,7 +611,7 @@ function revPreview(rev: NoteRevision): string {
|
|||||||
v-for="lb in labelList"
|
v-for="lb in labelList"
|
||||||
:key="lb.id"
|
:key="lb.id"
|
||||||
class="inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs"
|
class="inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs"
|
||||||
:class="labelChip(lb.color)"
|
:class="labelChip(lb)"
|
||||||
>
|
>
|
||||||
{{ lb.via_tag ? "#" + lb.name : lb.name }}
|
{{ lb.via_tag ? "#" + lb.name : lb.name }}
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -160,6 +160,37 @@ export function resolveNoteColor(note: { id: string; color?: string | null }): N
|
|||||||
return derivedTint(note.id);
|
return derivedTint(note.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The colour to paint a LABEL — its chip, and (step 3) every note carrying it.
|
||||||
|
*
|
||||||
|
* Derived from the tag's NAME, not stored, when nobody has picked one. Every `#tag`
|
||||||
|
* ever typed is currently `default`: `notes/tags.py` mints one as
|
||||||
|
* `Label(owner_id=…, name=name)` with no colour, so it takes the column default.
|
||||||
|
* Tag-driven note colour against that would leave the board exactly as grey as it
|
||||||
|
* was.
|
||||||
|
*
|
||||||
|
* DERIVED RATHER THAN PERSISTED AT MINT TIME, reversing the original plan in #2965.
|
||||||
|
* That plan wanted a hashed colour written at each of the four places a label can be
|
||||||
|
* born — and named the risk itself: `find_or_create_label` is "easy to miss, and it
|
||||||
|
* is the common one", because most tags are born from typing `#grocery`, not from a
|
||||||
|
* management screen. Deriving has no mint points to miss, needs no backfill for the
|
||||||
|
* tags that already exist, and reuses the hash the notes already use. The cost is
|
||||||
|
* that renaming a tag recolours it, which is defensible: the name IS the tag.
|
||||||
|
*
|
||||||
|
* An explicitly-picked colour is still stored and still wins, so tag colours stay
|
||||||
|
* editable exactly as asked.
|
||||||
|
*
|
||||||
|
* Lowercased because tags dedupe case-insensitively — `#Todo` renamed to `#todo` is
|
||||||
|
* the same tag and should not change colour. Both `toLowerCase` here and Kotlin's
|
||||||
|
* `lowercase()` are locale-independent, so the mirror holds.
|
||||||
|
*/
|
||||||
|
export function resolveLabelColor(label: { name: string; color?: string | null }): NoteColor {
|
||||||
|
const picked = label.color as NoteColor | undefined | null;
|
||||||
|
if (picked && picked !== "default" && picked in NOTE_CARD_CLASSES) return picked;
|
||||||
|
if (!label.name) return "default";
|
||||||
|
return derivedTint(label.name.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
// Fixture — the same ids and expected keys the Kotlin test asserts. Kept here as
|
// Fixture — the same ids and expected keys the Kotlin test asserts. Kept here as
|
||||||
// prose because there is nowhere on this side to assert it. If you change the hash
|
// prose because there is nowhere on this side to assert it. If you change the hash
|
||||||
// or the key order, these four must still hold on BOTH surfaces:
|
// or the key order, these four must still hold on BOTH surfaces:
|
||||||
@@ -168,3 +199,12 @@ export function resolveNoteColor(note: { id: string; color?: string | null }): N
|
|||||||
// 11111111-1111-1111-1111-111111111111 0x3d75cc01 blue
|
// 11111111-1111-1111-1111-111111111111 0x3d75cc01 blue
|
||||||
// 6ba7b810-9dad-11d1-80b4-00c04fd430c8 0xf108e530 orange
|
// 6ba7b810-9dad-11d1-80b4-00c04fd430c8 0xf108e530 orange
|
||||||
// f47ac10b-58cc-4372-a567-0e02b2c3d479 0x5b651540 orange
|
// f47ac10b-58cc-4372-a567-0e02b2c3d479 0x5b651540 orange
|
||||||
|
//
|
||||||
|
// And for labels, which hash the lowercased NAME rather than an id:
|
||||||
|
//
|
||||||
|
// todo -> pink grocery -> blue work -> green home -> gray
|
||||||
|
// ideas -> green reading -> gray urgent -> red
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
|||||||
Reference in New Issue
Block a user