The card menu learns to tag — a labels submenu of twelve checkmarks, and Copy Link moves in beside its mirror
Right-clicking a card now offers Labels: up to twelve rows ranked by how many cards on the board carry each one, tie-broken by what this user reached for last, each a checkmark that says whether *this* card already has it. More… opens the whole inventory beside the card, in lookup order, with a field that mints a name the board has never used. The rows deliberately do not widen to the selection the way Copy, Cut and Send to Trash do. A checkmark is a claim about one card, and three cards where two carry `bug` have no honest checked state — the rows that widen are the ones that say what they will do rather than what is already true. It is also what keeps the menu cheap: a context menu's contents are rebuilt on every ordinary pass of every card face, so reading the selection there would resubscribe the whole board. The board-sized half of the ranking is cached on the store and gated on equality; what runs per face is bounded by how many distinct labels exist, not by how many cards do. Copy Link leaves the first group for a new Copy Special submenu, mirroring Paste Special — the placement the owner's latest layout asks for, and the one the row's own note has been waiting on since it shipped as a same-day deviation. It keeps its VoiceOver action even so: it is an action that changed doors, not a door. Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
This commit is contained in:
@@ -203,3 +203,40 @@ public enum LabelRanking {
|
||||
return ordered.prefix(limit).map(\.name)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - The More… dialog's session
|
||||
|
||||
/// **The open More… dialog's card** — the `labels` submenu's escape hatch, which lists every used
|
||||
/// label with a toggle plus a field to create a new one (the owner's card menu spec).
|
||||
///
|
||||
/// `StyleEditorSession`'s shape and its whole lifecycle argument, narrowed to one card: it lives in
|
||||
/// `TransientBoardState` rather than in a view because a card face is inside a lazy stack that may be
|
||||
/// recycled out from under an open surface, and because the reload rule has to be able to take the
|
||||
/// dialog down when its subject leaves the board.
|
||||
///
|
||||
/// **One card, not a target set**, which is the whole difference from the style session and the same
|
||||
/// decision the submenu itself makes: every row in this dialog is a *checkmark*, and a checkmark has
|
||||
/// to state a fact about one card. See `CardFaceView.labelsMenu` for the argument in full.
|
||||
public struct LabelEditorSession: Sendable, Equatable {
|
||||
|
||||
/// Whose labels are being edited. `let`, for `StyleEditorSession.target`'s reason: a session whose
|
||||
/// card has vanished is *gone*, never re-aimed at another one.
|
||||
public let cardID: ItemID
|
||||
|
||||
public init(cardID: ItemID) {
|
||||
self.cardID = cardID
|
||||
}
|
||||
|
||||
/// This session re-grounded on a freshly applied snapshot, or `nil` when its card is no longer a
|
||||
/// live board card — which the presenting anchor reads as "dismiss".
|
||||
///
|
||||
/// Liveness is **effective, ancestor-walked**, for free and for `StyleEditorSession`'s reason: the
|
||||
/// resolution runs through `ItemReferenceSet`, so a card under a lane somebody just trashed leaves
|
||||
/// the set exactly as it leaves the selection. A card moved into the trash dismisses too, which is
|
||||
/// right — `BoardStore.setLabels` refuses a trashed card, so a dialog left open over one would be
|
||||
/// a surface offering writes it cannot make.
|
||||
public func resolved(against snapshot: BoardModel) -> LabelEditorSession? {
|
||||
let live = ItemReferenceSet(ids: [cardID], container: .board).resolved(against: snapshot).ids
|
||||
return live.contains(cardID) ? self : nil
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user