android: a Tags screen, so the phone can do more than attach tags to a note
Android / Build, or is the channel already serving this? (push) Successful in 4s
CI & Build / Build now, or wait for Android? (push) Successful in 4s
CI & Build / Python lint (push) Successful in 4s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 7s
Desktop (Tauri) / Tauri desktop (Linux) (push) Skipped
Desktop (Tauri) / Windows installer (cross-compiled) (push) Skipped
Desktop (Tauri) / Update manifest (push) Skipped
CI & Build / Python tests (push) Successful in 12s
CI & Build / integration (push) Successful in 21s
CI & Build / Build & push image (push) Skipped
Android / Kotlin + Rust (APK) (push) Failing after 3m28s

Android could list tags and mint new ones. It could not rename, recolour,
delete or merge one — and since the per-note colour picker was removed with
2949, tag colour is the ONLY colour control in the product, which meant an
Android-only session had no way to change any colour anywhere.

A destination reached from the drawer, not a modal. The web's LabelsModal is
a modal because a desktop can float one over the board; on a phone this is a
place you go to tidy up, and a full screen is what that is.

The manage entry is an action ON the drawer's Tags header rather than a row
in it, so it cannot be mistaken for a sixth lens. The header now renders even
when there are no tags: this screen is where you make the first one, and
hiding the way in until one exists is a door that only appears once you are
already inside.

## The two calls this needed

RENAME and MERGE deliberately do not follow the same rule, and the screen
says so rather than hiding it.

  * A rename that lands on an existing name merges, older survives (3324).
    That path is accident-prone — it is a text field, and a typo reaches it —
    so it needs a rule that cannot depend on which way round it was typed.
    The screen catches the collision against the LIST, not from what the core
    returns: the survivor may be the tag being renamed, so an unchanged id
    afterwards proves nothing. Then it asks before merging.

  * An explicit merge keeps its direction. Here the person is choosing, and
    the direction IS the intent — folding #grocery into #groceries is a
    decision, and overriding it with age would refuse the thing they asked
    for. The price is that the direction has to be unmissable, so the body
    names the tag that stops existing and every row offered is the survivor.

Delete quotes the note count, because "it is on 40 notes" is a different
decision from "delete this tag?". The count comes from `list_labels`, the
only call the core populates one on. It also says that a tag written as #tag
in a body comes back on that note's next edit — deleting the row cannot
un-write the word, and that is better said than discovered.

## The board had to learn something

`Destination.WithLabel` holds an id, and deleting or merging a tag the board
is currently LOOKING at would strand it on a lens that queries a row which no
longer exists — permanently empty, escapable only via the drawer. So
`loadLabels` became `refreshLabels`: public, and it drops back to Notes when
the current lens is gone. A failed listing deliberately does NOT trigger that
fallback — "I could not read the tags" is not evidence that this one went.

Reused rather than rewritten: `ErrorBanner` (the board and editor already
share it), `MenuItem` from Panel.kt (it closes the menu before acting so a
dialog cannot open under a hanging menu), `PlainTextField`, and the
`NOTE_TINTS` palette — the screen consumes it and does not fork a copy.

`default` stays in the palette on purpose: a tag with that colour gets a hue
derived from its name, so it means "let it pick", and removing it would leave
no way back to that.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K3MMqUtzX1TJgA1oypvm1c
This commit is contained in:
2026-08-31 19:44:54 -04:00
co-authored by Claude Opus 5
parent 550a34d8e2
commit 1e54b80f15
6 changed files with 894 additions and 12 deletions
@@ -66,6 +66,51 @@
<string name="editor_delete_forever_body">It will be removed from this device and from every device you sync with. This cannot be undone.</string>
<string name="editor_delete_forever_confirm">Delete</string>
<!-- Tag management. The whole vocabulary is "tag" (see Scribe #2966); the
schema still says Label, and no string here needs to know that. -->
<string name="tags_manage">Manage tags</string>
<string name="tags_title">Tags</string>
<string name="tags_back">Back</string>
<string name="tags_new_hint">New tag</string>
<string name="tags_create">Create</string>
<string name="tags_count">%1$d notes</string>
<string name="tags_count_one">1 note</string>
<string name="tags_count_none">No notes yet</string>
<string name="tags_empty_title">No tags yet</string>
<string name="tags_empty_body">Create one above, or write a #tag in a note and it becomes one.</string>
<string name="tags_actions">More actions</string>
<string name="tags_colour">Colour</string>
<string name="tags_colour_of">Colour for %1$s</string>
<string name="tags_rename">Rename</string>
<string name="tags_rename_title">Rename %1$s</string>
<string name="tags_rename_confirm">Rename</string>
<!-- Renaming onto an existing tag merges the two, older survives (Scribe
#3324). A merge cannot be undone by repeating it and is reachable here by
a typo, so it says so before it happens — same reasoning as #2116. -->
<string name="tags_rename_merges_title">Merge with %1$s?</string>
<string name="tags_rename_merges_body">A tag called %1$s already exists. Renaming will merge these two into one, carrying every note from both. The notes are kept; one of the two tags stops existing, and that cannot be undone.</string>
<string name="tags_rename_merges_confirm">Merge</string>
<string name="tags_merge">Merge into…</string>
<string name="tags_merge_title">Merge %1$s into…</string>
<!-- The survivor is named in the button, not just the title: this is the one
operation here that repeating does not undo. -->
<string name="tags_merge_body">Every note tagged %1$s will be tagged with the one you pick instead, and %1$s will stop existing. The notes are kept.</string>
<string name="tags_merge_none">There is no other tag to merge into.</string>
<string name="tags_delete">Delete</string>
<string name="tags_delete_title">Delete %1$s?</string>
<string name="tags_delete_body">It will be removed from every note that has it, on every device you sync with. The notes themselves are kept.</string>
<string name="tags_delete_body_counted">It is on %1$d notes. It will be removed from all of them, on every device you sync with. The notes themselves are kept.</string>
<string name="tags_delete_confirm">Delete</string>
<!-- A tag written as #tag in a note's body is owned by that text. Deleting the
row cannot un-write the word, so it comes back on that note's next edit —
said here rather than left as a surprise. -->
<string name="tags_delete_from_text">Tags written as #tag in a note come back when that note is next edited.</string>
<string name="tags_cancel">Cancel</string>
<!-- Pickers -->
<string name="label_picker_title">Tags</string>
<string name="label_new_hint">Type a tag and press enter</string>