Make the card-create handoff read as one arrival

The committed placeholder's lane slot now takes the arriving card's
identity — .awaitingArrival keys as card:<id>, so when the echo reload
lands the ForEach sees one persisting element whose content swaps from
stub to card face instead of a removal and an insertion with two
scale+fade transitions (DESIGN/02 > overlays: the handoff must read as
one arrival). The awaiting face renders the card's exact chrome from
shared CardFaceMetrics, so any residual branch crossfade is between
pixel-identical renderings; the editing phase keeps its constant key so
typing identity holds. Slot position math is untouched.
CreateHandoffIdentityTests pins the key derivation.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
2026-07-28 07:15:39 -04:00
parent e6d3673891
commit 15006ad233
2 changed files with 255 additions and 45 deletions
+90
View File
@@ -15,6 +15,12 @@ import Testing
/// testable at all `AnyTransition` is opaque).
/// 3. **The named durations.** 03 fixes five figures by name; a silent drift in one of them would be
/// invisible in every other test in the suite.
/// 4. **Which changes are not motion at all.** The create handoff has to "read as one arrival"
/// (02-architecture.md TransientBoardState overlays), and it achieves that by *removing* an
/// animation rather than adding one the placeholder adopts the arriving card's `ForEach`
/// identity, so no transition has anything to fire on. The claim is a pure function of a
/// placeholder phase (`LaneSlot.identity(ofPlaceholderIn:)`), so it is pinned here rather than
/// watched for.
// MARK: - Which reloads perform
@@ -161,3 +167,87 @@ struct ReduceMotionVariantTests {
#expect(Motion.carouselAppearance != Motion.cardAppearance(reduced: false))
}
}
// MARK: - The create handoff
/// **"The handoff must read as one arrival"** (02-architecture.md TransientBoardState overlays,
/// settled 2026-07-28): the new-card placeholder "is handed off by discarding itself the moment the
/// created card's UUID appears in a snapshot", and "the placeholder renders at the arriving card's
/// exact geometry/chrome".
///
/// The mechanism is `LaneSlot`'s identity function and nothing else no `matchedGeometryEffect`, no
/// second transaction, no suppression flag. A committed placeholder is keyed by the *arriving card's*
/// slot id, so when the echo reload replaces the pseudo-card with the real one the `ForEach` element
/// is neither inserted nor removed: only its content changes, and `Motion.cardTransition` attached
/// per slot in the lane's masonry has nothing to run on. Two scale-and-fades become none.
///
/// That is the whole of what is unit-testable about it. The masonry is a `Layout`, the transition is
/// an opaque `AnyTransition`, and neither renders here but the key derivation those two depend on
/// is a pure function of a phase, and it is the line that would have to break for the handoff to go
/// back to being two events.
struct CreateHandoffIdentityTests {
private let arriving = ItemID(rawValue: "6C1D4B4E-0000-4000-8000-00000000A001")
/// **The claim, stated as an equality.** The committed placeholder's key *is* the arriving card's
/// key not a parallel spelling of it, the same function's answer so nothing can drift the two
/// apart without failing here.
@Test func aCommittedPlaceholderIsKeyedAsTheArrivingCard() {
#expect(
LaneSlot.identity(ofPlaceholderIn: .awaitingArrival(arriving))
== LaneSlot.identity(of: arriving)
)
#expect(LaneSlot.placeholder(.awaitingArrival(arriving)).id == LaneSlot.identity(of: arriving))
}
/// The other half of the handoff, from the slot the real card builds: the two `LaneSlot` cases
/// that stand for one card at two moments answer with one id, which is what makes them one
/// element to `ForEach` rather than two.
@Test func theArrivingCardAndItsPlaceholderAreOneElement() throws {
let fixture = try WriterFixture()
defer { fixture.tearDown() }
try fixture.item("", Item.board)
try fixture.item(Ident.lane1, Item.rich(order: "1024", title: "Todo"))
try fixture.item("\(Ident.lane1)/\(Ident.card1)", Item.rich(order: "1024", title: "First"))
let snapshot = try BoardLoader.load(boardRoot: fixture.root).model
let card = try #require(snapshot.lanes.flatMap(\.cards).first)
#expect(LaneSlot.card(card).id == LaneSlot.placeholder(.awaitingArrival(card.id)).id)
}
/// **The editing phase keeps the constant key**, and that is not an oversight the handoff missed:
/// an open editor has no identity to adopt yet, and a key that changed while the user typed would
/// tear the field down mid-word and take its keyboard focus with it. The key changes exactly once,
/// at commit.
@Test func anOpenEditorKeepsItsConstantKey() {
#expect(LaneSlot.identity(ofPlaceholderIn: .editing) == LaneSlot.editingPlaceholderIdentity)
#expect(LaneSlot.placeholder(.editing).id == LaneSlot.editingPlaceholderIdentity)
// And it is emphatically *not* any card's key that is the whole point of the one change.
#expect(LaneSlot.identity(ofPlaceholderIn: .editing) != LaneSlot.identity(of: arriving))
}
/// The keys stay disjoint by construction a shadow is never mistaken for the card arriving into
/// the slot it opened, and the editing placeholder is never mistaken for either.
@Test func theSlotNamespacesDoNotCollide() {
let keys: Set<String> = [
LaneSlot.identity(of: arriving),
LaneSlot.editingPlaceholderIdentity,
LaneSlot.shadow(index: 0, height: 44).id,
LaneSlot.shadow(index: 1, height: 44).id,
]
#expect(keys.count == 4)
}
/// **Reduce Motion needs no branch of its own here.** Identity continuity introduces no motion to
/// reduce: the key is the same key in both variants, so the reduced path reads as one arrival for
/// exactly the reason the standard path does a transition that never fires has no variant to
/// choose between. Asserted as the absence of a parameter, which is the shape the claim takes.
@Test func theHandoffIsIdenticalUnderReduceMotion() {
// The derivation takes no `reduced:` flag at all, and the transition it defeats has a reduced
// variant that is equally irrelevant while the element persists.
#expect(LaneSlot.identity(ofPlaceholderIn: .awaitingArrival(arriving)) == LaneSlot.identity(of: arriving))
#expect(Motion.cardAppearance(reduced: true) == .crossfade)
#expect(Motion.cardAppearance(reduced: false) == .scaleAndFade(from: 0.8))
}
}