Deck builder: tag cards within a deck

Cards in a deck can carry free-form tags ("to remove", "ramp", …), set
from the card preview or, for several cards at once, from the bulk bar's
Tag… dialog. Tags belong to the card in that deck: they survive
maybeboard moves and quantity changes, go when the card leaves the deck,
and are copied when the deck is duplicated.

The list view gets a Tags column whose chips filter to their tag, tiles
show tags as footer badges, and the advanced filter popup gets a Tags
field (any / all, or untagged only). Select all now selects only the
cards shown, so a filtered view can be acted on as a whole.

Tags sync, which needs library document format 5; libraries without tags
keep writing their old version. Deploy the sync server before releasing
apps that write tags.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-29 14:42:40 +00:00
parent ef4a0d740d
commit 70c89a5644
13 changed files with 557 additions and 35 deletions

View File

@@ -463,6 +463,20 @@ async fn only_valid_libraries_are_stored() {
assert_eq!(s.put_library(&t, Some("0"), json!(["nope"])).await.status(), StatusCode::BAD_REQUEST);
}
#[tokio::test]
async fn a_library_with_deck_card_tags_is_stored() {
let s = Server::local();
let t = s.sign_in("alice", "correct horse battery").await;
let doc = json!({
"version": 5,
"cards": {},
"decks": { "u1": { "name": "Fub", "cards": { "a": { "quantity": 1, "tags": ["ramp"] } } } }
});
assert_eq!(s.put_library(&t, Some("0"), doc).await.status(), StatusCode::OK);
let got = body_json(s.get_library(&t).await).await;
assert_eq!(got["doc"]["decks"]["u1"]["cards"]["a"]["tags"], json!(["ramp"]));
}
#[tokio::test]
async fn each_user_has_their_own_library() {
let s = Server::local();