50e26efdb992087c2278403319fc5d3b03df5fea
24
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
0609104b2d |
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 |
||
|
|
085a84abb2 |
Navigation's Move Left/Right learn a real card — the context menu's disabled rows compute a per-card cross-lane destination and land it through the drop's own rank machinery
CardMoveTarget (BoardCommands.swift), LaneMoveTarget's cousin: validates the clicked card's own current lane against the live lane order rather than the board's live selection, refuses a target that reaches outside that lane (no coherent left for a spread), and answers an index — the clicked card's own position in its lane, clamped — for the adjacent live lane in either direction. Trash is never a candidate (not a Lane); a collapsed lane is a fine landing (a fold hides cards, it doesn't close the lane). CardFaceView's Navigation rows now call moveCardAcrossLane(by:), which hands CardMoveTarget's answer straight to BoardStore.moveCards(_:toLane:at:) — the exact call a released drag makes, so rank-minting, the undo step, the watcher echo and the banner all come free. Targeting is Copy/Cut's own widening (targetIDs): the clicked card, or the live selection when the clicked card is a member of it. Enablement stays render-safe the way isSelected/selectedCount already are: three new CardFaceView parameters (hasLeftNeighbor, hasRightNeighbor, selectionSpansLanes) are hoisted once per lane in LaneView.scrollableCards and handed down as compared parameters, never read from inside a card face's own .disabled. Caught and fixed a real regression here during development: an early cut of the multi-lane-spread check answered true for any lane that simply didn't contain the selected card, which flipped a compared parameter for most of the board on an ordinary single-card select and defeated CardFaceView's equality gate wholesale (BoardRenderPerformanceTests.selectionStillRepaints caught it at 151 of 180 card faces). Tests: CardMoveTargetTests (KeyboardGrammarTests.swift) pins the pure predicate — leftmost/rightmost lane, single lane, index clamping, a widened group anchoring on the clicked member rather than its own extent, the multi-lane-spread refusal, and an integration test feeding the answer straight through moveCards. CardFaceViewEquatableTests gains a case pinning the three new compared parameters. Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy |
||
|
|
200fbce276 |
The card window's ⌘V drops its picture branch — the attachments header now offers one instead
Raw image data on a card window's ⌘V was a keyboard shortcut with no visible trigger; the sidebar's Attachments header now grows a quiet control — beside the existing add affordance, present only while the pasteboard holds a picture this card could take — that pastes it through the exact seam the retired branch used (ClipboardStore.pasteImage(intoCard:in:), the board's "Paste Image into Card" row's own call). The file-URL branch stays on ⌘V; a Finder copy is still unambiguous. CardBodyTextView's paste-yield mechanism needed no change at all — it forwards by capability, not by picture-specific logic, so a screenshot ⌘V with the body editor focused is now a genuine no-op there, served by the new control instead. The pasteboard's re-read gains a fourth checkpoint — a window becoming key — alongside menu-tracking, ⌘-down and app activation: a persistent visible control has to read true continuously while its window is frontmost, not only at the instant a menu or chord probes it. Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy |
||
|
|
72ca22251b |
A card's context menu finds its four groups — style, clipboard, navigation, and the trash
CardFaceView.boardMenu/boardActions restructured to the owner's shape (card
fe66c461): Open/Copy Link/Rename/Style▸(Symbol,Color), a divider, then
Copy/Cut/Paste/Paste Special▸(Paste Image into Card), a divider, then
Navigation▸(Move Left,Move Right), a divider, then Send to Trash. Every row
routes through existing machinery — no new commands.
ClipboardStore gains copy(from:targeting:)/cut(from:targeting:) and their
canCopy/canCut twins, so Copy and Cut can widen to the clicked card exactly
as Delete and Style already do ("right-clicking something outside the
selection acts on what was clicked"), without disturbing the Edit-menu path.
LaneMoveTarget.destination is extracted out of MoveLaneCommands so the
card menu's Navigation rows validate against the identical sole-live-lane
predicate as Board ▸ Move Left/Right. Since a card id can never itself
satisfy that predicate, the two rows are wired to the real store call but
unconditionally disabled — reading the live selection per card face would
reproduce the O(board) render regression isSelected/selectedCount exist to
prevent (contextMenu's builder is not lazy).
Style ▸ Symbol and ▸ Color both open the one existing style popover — no
per-section pre-focus (StyleEditorSession has no such concept, and
StyleEditorView internals are out of scope while another pass redesigns
the pickers). Paste and Paste Image into Card reduce their .disabled
checks to selection/snapshot-free forms, proven safe by construction (a
rendered card face already guarantees a live lane / a live board card).
Journaled on the card: Copy Link kept (shipped same day, not in the
owner's list), "Delete" relabeled "Send to Trash" (board-side move, not
the permanent trash delete), quick-style recents row dropped from this
menu, Navigation's always-disabled rows, and Paste not retargeting to the
clicked card — all flagged needs owner review. DESIGN/11-command-nexus.md's
Card row is owed a rewrite, left for the main session.
Tests: LaneMoveTarget.destination (new), targeted copy/cut (new), plus
existing ClipboardStore/PasteTarget/MoveLane/CopyLink/Trash-menu/PasteImage/
PasteFile/Style/CaretChord/render-performance/equatable-gate suites —
156 tests, all passing.
Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
|
||
|
|
c87616f3fb |
Comments join attachments on the card face — a quiet bubble-and-count chip, present-only
A card whose thread holds one comment or more now draws a second trailing chip beside the paperclip: a secondary-tinted bubble glyph plus its count, shown only when the count is above zero (design ruling 2026-08-09, card e729e30a). Same styling family as the attachments chip — caption size, secondary tint, decorative and hidden outright from the accessibility tree — but this one carries a visible count rather than staying icon-only, per the ruling's own "bubble-style SF Symbol + count." It sits after the attachments chip at the row's trailing edge, in both the live title row and the drag replica. The count is a new `Card.commentCount` field the loader fills with a readdir over `comments/`'s identity-shaped children that carry their own `index.md` — `BoardLoader.commentCount(in:)`, built on the same `identityShapedChildren` predicate a trash entry's held-card count already uses. Never a parse: `.draft` and `.trash/` are excluded for free, the same dot-prefixed hidden-entry skip `CommentThread.load` documents for both, so the walk stays exactly the O(cards) shape 01-storage-format.md § Enhanced schema already commits to. Because the count rides inside the `card: Card` parameter `CardFaceView` already takes — not a new parameter of its own — drawing the chip costs nothing beyond a field read on an already-compared value: no new Observable read joins the body, and the equatable gate already covers it via `Card`'s synthesized `Equatable`. The one divergence from the comments pane's parsed count is documented rather than hidden: a comment folder whose `index.md` exists but fails to parse is a `Stray` the thread read excludes by opening and rejecting it, a cost this readdir does not pay. The face may then read one comment high until that folder is fixed or removed — the trade the ruling's "cheap directory-entry count… not a parse" asks for, over paying full parse cost on every card of every load. Every well-formed comment, and every card with no malformed one, agrees with the pane exactly. VoiceOver: `AccessibilityPhrases.cardValue` gains a `comments: Int` parameter, appended after attachments and before the cut-pending phrase — the same left-to-right order the two chips draw in, so a sighted read and a VoiceOver read never disagree about which comes first. The trashed lane row's own call site (an opaque unit with no comments to speak of) passes `comments: 0`. Docs: DESIGN/03-board-ui.md's card-face section describes both chips and retires the stale "closed with no growth" sentence, honestly recording the 2026-08-09 growth (the hero banner landed hours earlier, this chip after it) as exposure of facts the card already carries rather than a body excerpt. DESIGN/10-accessibility.md's flattened-element sentence gains the comment count. DESIGN/01-storage-format.md's Enhanced schema paragraph records the chip as shipped. WISHLIST #9 is marked shipped in place — not renumbered, since #10 and #11 are cross-referenced elsewhere. Tests: CardCommentCountListingTests (BoardLoaderTests.swift) pins the readdir against a synthetic tree — no comments/ folder, an empty one, non-identity-shaped and index-less strays excluded, .draft/.trash/ excluded for free, agreement with CommentThread.load's parsed count in the well-formed case, and the one documented divergence on a malformed index.md. AccessibilityPhrasesTests covers cardValue's new parameter alone, alongside attachments, and all three fragments together. ViewEquatableTests pins that a comment landing on a card is a gate difference. BoardRenderPerformanceTests adds a render-cost guard: one comment added to one card on a hosted 180-card board re-renders a handful of bodies, not the board. Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy |
||
|
|
ce92c24190 |
Hero image for cards — one of the card's own attachments, banded across its face
A card whose `hero:` names one of its own attachments draws that picture as a banner across the full width of its plate, above the icon-and-title row, aspect-fill cropped into a fixed 2.75 em band — 36pt at the standard body, and em-scaled like every other figure the board draws, so it grows with the system text size and with the board's zoom rather than shrinking against a title twice its usual size. The figure sits deliberately under the 44pt a plain one-line card is tall: a hero card should read as a card with a picture on it rather than a picture with a caption, which is 03's standing rule that the title dominates. The key's grammar is a **bare filename**, and that is what separates it from the board background's `image` subkey rather than a nervousness about paths. A board names a file anywhere under its root, so a path is that key's reading and where it leads is the renderer's question. A card names one of the files it already owns — the flat `attachments/` folder the app lists, relocates into, and carries through every move, copy, trash and restore — so `hero: art/sketch.png` is not an awkward spelling of a hero image, it is a value the key cannot mean. It therefore has no reading at all: a value carrying a separator, or spelling `.`/`..`, or empty, is malformed at the document layer, which renders it as absent and leaves the coerce tier's trace, exactly as `width: 1.5` does. The bytes stay as written, the resolver re-checks containment anyway, and the whole degrade family below that — a name pointing at a missing file, an unreadable one, or one that is not an image — ends the same way: no banner, no defect, nothing written. That last promise is about *height* as much as about ink, so the band is given no height at all until a picture has actually decoded. A card whose hero cannot be drawn lays out identically to a card with no key, structurally rather than by a branch somebody has to remember; the price is one settle per hero as a board opens, and none after that. Everything else the face draws is attached outside the new stack and is untouched by it — the accent stripe still runs the plate's full leading edge across the band's corner, the selection and file-hover strokes still ring the whole plate, the cut and drag dims still cover it, and the drop model still registers the plate's real height, so a hero card is simply a taller card the masonry already understands. The trash draws it too, by the one-face rule. Decoding is ImageIO's downsampling path off the main actor at a quarter of the backdrop's pixel budget (`BoardBackdrop.decode` gained the limit as a parameter rather than being copied), and the results live in one app-wide, deliberately non-observable cache keyed on path plus the file's date and size. Non-observable because a tracked write there would invalidate every hero face on the board, which is the O(board) invalidation this view was rebuilt once already to shed; each face holds its own picture in view state and seeds it from the cache, which is also what lets the drag replica — whose preview builder is non-escaping and cannot await anything — carry the band at the face's real height. Taking a stamp twice from one URL value turned out to answer with the first read's date and size however many times the bytes had changed, so `stamp(of:)` now drops its cached resource values first; noticing a replacement is the only thing a stamp is for. The face takes the resolved URL as a compared input rather than resolving it, for selected-ness's reason one axis over: resolving needs the card's folder, which a face does not know, and finding it from the snapshot would be a board walk per face. The lane and the trash column each know their own container and compute it once for the whole strip. There is no in-app setter this version — the key is written by hand or by an agent, which is why the guide bumps to v13 with a clause spelling the grammar out beside the other card keys, and why `attachments/` gets the one-line pointer an agent that has just written `` will need. "Set as Hero" from the attachment row is future work, as is the card window and print, which draw the same model and show no banner today. Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy |
||
|
|
b09c4bd5c0 |
Copy Link — a card context-menu row that puts the card folder on the pasteboard
Card 737a949f: "Add an option to card context menu to copy a link to the card folder." Implements the design ruling verbatim. - New context-menu row "Copy Link" (CardFaceView.boardMenu, board side only — trash cards are excluded, matching "sole selected live card"). Writes the clicked card's folder in one pasteboard item carrying two representations: the file:// URL under .fileURL and the plain absolute path under .string (FolderLinkPasteboard.swift). Enabled on a sole selected live card; disabled on a multi-selection and wherever edit-shaped actions already disable, per the ruling. Also exposed as a VoiceOver custom action alongside its siblings. - Menu-bar twin: Board ▸ Copy Link (BoardCommands.swift, CopyLinkCommand), no default chord — the every-function-a-menu-item contract in DESIGN/11-command-nexus.md is still current, so this is the twin that contract calls for, homed the way Open Card/Rename/ Style… already are. - DESIGN/11-command-nexus.md: new Board-menu row and an updated Card context-menu row. - Tests (KanbanTests/CopyLinkTests.swift): the target predicate's enablement (sole card / multi-selection / lane / trash / empty / inline-editing), the pasteboard write's exact bytes via a fake pasteboard (both representations, exact folder URL), and a disabled-target no-op. Caught and fixed during self-review: an early version read the context menu's widened-selection helper (targetIDs) inside the Copy Link row's .disabled(...), which reads store.selection. Since .contextMenu's content closure is evaluated on every ordinary body pass (not only when the menu opens), that resubscribed every card face on the board to every selection change — the exact O(board) regression RENDER-INSTRUMENTATION.md's isSelected/selectedCount split exists to prevent, caught by BoardRenderPerformanceTests and MarqueeRenderCostTests. Fixed by reading the already-hoisted, non-Observable `selectedCount` parameter instead, which answers the same "how many ride along" question at zero extra subscription cost. xcodegen generate, the Kanban scheme build, and the full KanbanTests suite (2816 tests) are clean. Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy |
||
|
|
9c857ae0cc |
Selected-ness rides down as a compared parameter — a marquee crossing repaints its faces, not the board
A selection change re-ran every CardFaceView on the board (180 bodies ≈ 85 ms on the 6×30 fixture, 515 ≈ 233 ms on a real 515-card board, debug): the face's body read store.selection in three places — isSelected, the drag replica's count, and the context menu's styleTarget — and Observation invalidates every reader of the property, past the equatable gate entirely. The band overlay stayed cheap, which is why the marquee tracked the cursor while the highlight lagged ~0.4 s behind. Now LaneView and TrashLaneView hoist one selection read per body and hand each face isSelected/selectedCount as compared parameters; StyleMenuItems takes its target as a deferred closure; TrashLaneRowView gains the same treatment plus the Equatable gate it never needed before. Select-one-card: 180 bodies → 1. A growing band costs the selection's own running size; the real board's crossing fell 233 → 112 ms — the remainder is lane bodies re-measuring their masonry, a separate lane-level finding recorded in RENDER-INSTRUMENTATION.md. Also: select() gains defaultsSoleMember — the marquee's explicit nils never avoided the sole-member default, so a one-card band acquired a selectionHead and could scroll the lane out from under its own drag. MarqueeRenderCostTests pins the shape: redundant samples cost zero bodies, a growing band pays per crossing, and selectionStillRepaints holds a ≤8 budget. |
||
|
|
8aefaf23ce |
The empty provider was never load-bearing — the dragless layer frees the rubber band
Measured on real events 2026-08-07, correcting the 2026-08-06 hosted finding: a bare count-1 tap on LaneView's empty-space layer fires in ~1-3 ms with no drag source at all — the hold that made the empty .onDrag look necessary was the sterile NSApp.postEvent stream over-disambiguating. And the provider was actively harmful: even an empty drag source claims the mouse-drag at threshold, starving the marquee's simultaneous DragGesture after one sample — the band froze and the mouseUp never arrived. The layer goes dragless; drags from empty space belong wholly to MarqueeControl. PointerClick's and the layer's comments retell the corrected story. Alongside: openCard is typed @MainActor throughout, which makes the closure Sendable and lets CardFaceRole carry it under CardFaceView's nonisolated ==. Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy |
||
|
|
f126614b56 |
The pickup seeds its own slot — the lift and the shadow become one transaction
"At drag start it replaces the item's original space" is now true from the very first frame: begin takes the run's own resting slot as a seed, set in the same transaction that lifts the run out. Without it the vacated gap closed un-animated and sprang back open at the first dropUpdated — a shuffle carrying no information. The seed bypasses propose deliberately (a pickup is not a new landing spot, so no alignment tick), and the first real sample's re-propose of the same slot is the early-out's ordinary silence. A ⌥-pickup seeds nothing — a copy's resting layout keeps the originals in place, so there is no vacated space to hold. Card pickups compute the seed through the same LaneView.rendered rules the retargets count in; lane pickups seed their strip slot the same way. DRAG-REORDER.md's own-slot paragraph records the rule, and SeededPickupTests pins all four corners. Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy |
||
|
|
cfee4a4b41 |
The board learns to zoom — eight rungs on one ruler, and Actual Size is the untouched board
View ▸ Zoom In / Zoom Out / Actual Size (⌘+ / ⌘− / ⌘0): 75%–200% in eight rungs, app-wide and persisted (the Show Comments precedent) — a viewing comfort, not a property of any one board. The level travels as BoardZoomContext in the environment, injected on BoardView alone so the banner strip, search bar, sheets and popovers stay at the system size; the environment is also what carries it through CardFaceView's equality gate, which compares nothing that moves with the level. Every BoardMetrics figure follows zoom.bodyPointSize — card and lane chrome, drag replicas and the count badge, the resize handle, the trash column — and the drop registry carries the ruler for event-time reads, with the autoscroller's three reaches turning font-derived (reachSide named as the stripGap it always equalled). Lanes still divide the window; zoom never moves the window or its floor. The toolbar gains a catalog-only Zoom In/Out pair mirroring the menu rows' predicate; zoom holds shut mid-drag (frozen geometry), each rung announces itself to VoiceOver, and the render suite pins both invariants: a rung repaints every face, a no-op Actual Size repaints nothing. Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy |
||
|
|
e1ffe71e5b |
A refused trash hover withdraws the trash's own promise — the fall-through holds only keepable ones
Gap #12 ruled into 04 ▸ the trash-drop bullet: the refusal fall-through's honest reading (the column declines to be a target, it does not cancel the held drag) covers only promises the refusal leaves keepable — lane shadows keep drawing and an ⌥-copy lands where they are. A standing proposal naming the trash itself is the promise the refusal just broke, so it withdraws: the drag goes proposal-less over the column, release with no proposal cancels in agreement with the empty picture, and lifting ⌥ re-asks at the same address. Implementation filed on the Implementation board. Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy |
||
|
|
43e6ad229e |
The drag replicas cast the shadow the pickup lift already promised
DragReplicaStyle spells the pathfinder's constant ambient shadow once (black 16%, radius 2.5, y 1.5) and all three replica faces — card, lane, trashed lane — wear it via one modifier; the ghost fans compose from the same faces so every copy inherits it, dimmed by its own opacity. 03 § Motion's "pickup lift (scale + shadow)" supplies the scale from the system session but no shadow on the preview image — the replica is the only place it can come from. BoardMetrics.replicaPadding was already written to keep exactly this shadow from clipping and was simply unused until now. Drag-perf card 24397310. Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97 |
||
|
|
0218ae4c21 |
Drag instrumentation — render counters, hot-path signposts, and the invariants that prove the gates
BoardRenderMetrics (DEBUG-only, the pathfinder's counter bag plus a strip-body discriminator that tells a failed gate from a direct Observation invalidation) counted at BoardView/LaneView/CardFaceView/ TrashLaneView bodies and MasonryLayout's callbacks. DragSignposts wraps dropUpdated, retargetCards, commitDrop, and the commit-to-covering- snapshot release pause; input latency reports honestly against NSApp.currentEvent's mach base or labels itself base=none — no event timestamp rides the drop path. BoardRenderPerformanceTests hosts the real BoardView off-screen: a value-equal reload runs zero lane and zero card bodies, a one-card edit repaints one card of 180. The lane-body budget is <= laneCount with the headerInk chain documented and a two-way tripwire that fails when the fix lands. Methodology in RENDER-INSTRUMENTATION.md. Release build proves it all compiles out. Drag-perf card a450ad09. Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97 |
||
|
|
84f909a720 |
Proposal changes stop rebuilding the whole board — lanes and card faces gate on their values
LaneView and CardFaceView become Equatable and are instantiated through .equatable(): the strip's body re-runs on every drop-proposal change, and without the gates that rebuilt every lane and every card face on every cursor move of a drag. The == compares value inputs and window-lived collaborator identities; the closures BoardView rebuilds each pass are deliberately excluded (BoardDropContext.isEquivalent / MarqueeControl. isEquivalent / CardFaceRole.isEquivalent own that judgment). Observation reads inside the bodies still self-invalidate — the lane under the drag keeps re-running; the other lanes stop. Ports the pathfinder's CardView/ColumnView gating pattern (drag-perf suspect #2, card cbb6e476). Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97 |
||
|
|
ef423bb9d2 |
Every board root keys once at its store — the hover path stops walking the filesystem
`DragLocality.isSameBoard` resolved symlinks on both URLs, which stats every path component. It ran five-plus times per `dropUpdated` and again per lane per body evaluation through `renderedCards` → `hiddenMembers` — order of 50–100 stat calls per mouse-move, and worse on iCloud-backed paths. A pickup stall sample had it on ~47 of 943 stacks. `BoardRootKey` mints that canonical spelling once, where the root is, and every locality comparison downstream is an `==` on two strings. The drag carriers hold keys rather than URLs, so re-deriving under the cursor is no longer expressible. `rootURL` keeps the user's spelling — the folder name is the display-name fallback, and canonicalizing there would visibly rename a board opened through a pin. The key follows the folder through `relocate(to:)`: a key frozen at open would collide with a new board opened at the vacated path, and two boards comparing as one is a cross-board drag silently behaving as a move. Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97 |
||
|
|
8206a78ecc |
Redraw a copy's originals at the source — the resting layout follows the operation
The modifier flip (⌥ copy / ⌘ move) now reflows the source board once: a copy re-admits the dragged originals into the resting layout, standing dimmed in place, and a move lifts them out as before. This retires DRAG-REORDER.md's operation-blind carve-out and makes 04-interactions.md's "originals stay" true in flight, not just at echo. The one seam is DragSession.hiddenMembers reading the observed operation. Consequences carried honestly: copyCards now takes its index in the lane's full rendered space (the zones counted the originals, so the geometry's number is the writer's number — the old neighbour remap is deleted); resolveOperation freezes under the committed hold exactly as propose does, fixing a real bug where a settled within-board ⌥-copy drew the move arrangement until the echo and visibly re-shuffled. Stationary flips still wait for the next dropUpdated (their own Backlog card). Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97 |
||
|
|
bec75e4282 |
Realign code with the 2026-07-31 rulings
The trash sorts by modified descending — the arrival rank mint retires (Ranks.isOrderedForTrash one comparator, loader + merged order agree; the legacy deleted: migration stamps modified from the tombstone timestamp where parseable; delete undo steps validate existence-only; agent guide v8). Trash selection goes kind-blind — ranges, marquee, Select All, and the successor walk sweep both kinds; the guard moves to the exits (mixed-payload drop refusal, copy/cut validation). The copy stamping preflight widens back to comment depth (load-scoped posture — the board always loads, the gesture refuses whole). Fixes a latent no-op: trashed-lane drag restore never fired (DragSession.beginLanes hard-coded the board container). 2403 tests in 413 suites green. Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97 |
||
|
|
785ef5fe14 |
Realign code with the 2026-07-30 findings-resolution rulings
Delete Immediately is removed entirely (ruling |
||
|
|
69084fdff7 |
Realign code with the 2026-07-29 findings-resolution rulings
Nine rulings land as code. Reorders don't stamp — one container-change predicate (WriteOperation.rewritesOrderOnly): within-container reorders and the renumber rescale rewrite only order, while cross-lane, cross-board, and trash moves stamp modified and clear modified-by; no trash special case exists, and the m8 undo inverses conform through the same seam. Copies are transactions: the root-strict/nested-lenient split retires for a whole-subtree stampability preflight that refuses loudly naming the offender, and every item-level copy severs remote/remote-state at every level (whole-board forks carry them verbatim). Paste refuses, never degrades: the embedded-index.md materialization and its loss row retire; a missing staged snapshot produces nothing and posts an error-tone one-shot named from manifest metadata. Coerce-tier fallbacks log through the Defect stream with path context attached loader-side. Displacement is level-uniform: a file squatting attachments inside a card heals by the same rename ladder as board-root squatters; comments stays tolerated. Delete Immediately joins card and lane context menus as Delete's ⌥-alternate with its own VO custom action, routed through an explicit container so the menu target outranks standing selection. Agent guide v7 teaches the stamp discipline and the card-level attachments claim, and sheds two stale v6 lines (lanes trash now; kind is taught). Verified conformant, unchanged: edition-aware Undo/Redo disable, trash marquee full-height backdrop. Both schemes 1854 tests / 318 suites green; verify-editions 30/30. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY |
||
|
|
71b112d04c |
Card drag preview renders at the source face's laid-out width
The replica framed to a font-derived nominal width, so faces in wide lanes' interior masonry columns and narrow lanes dragged a preview of the wrong size and the cursor could sit over empty space beside it. CardFaceView's existing onGeometryChange now reports size, not height alone - height still feeds LaneDropRegistry, width feeds the replica - and BoardMetrics.cardReplicaWidth(measured:bodyPointSize:) keeps the old figure as the documented not-yet-laid-out fallback. Same row, same paddings, same lineLimit at the same width means the same height, so the preview is now an exact overlay of the face it left. Manual verification pending alongside the lane grab-point card: 3x vs 1x lanes, interior masonry columns, window re-divide, trash rows, large text sizes, drag-at-creation fallback. 3 tests. 1661 green on both schemes. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY |
||
|
|
8564814754 |
Implement visual accommodations and Full Keyboard Access
Full relative text scaling per DESIGN/10: BoardMetrics is the board strip's geometry as a pure function of the body point size (CardWindowMetrics' twin) — lane plate/header/band, card corner/stripe/padding, masonry spacing, the drop model's nominal card height, resize-handle geometry, trash hatch pitch, and both window floors all derive from an em; CardFaceMetrics folded in. The two fixed font sizes (welcome brand/glyph) went relative; the toolbar search field is 17 ems like the transient bar's. The no-horizontal-scroll invariant is pinned by test at six text sizes by twelve lane counts. Accommodations is Motion's sibling for the visual settings: Increase Contrast adds a flat point to strokes (monotone, hierarchy-preserving), gives borderless card/lane plates a resting separator hairline, and takes faded accents to full alpha; Reduce Transparency turns the transient search bar's glass solid and does the same for the alpha washes that composite over a user-chosen board background (trash plate, hatched header, drag shadow). Reduce Motion audited — every animated surface already routes through Motion with a reduced variant; no gaps. Full Keyboard Access: the template chooser's tiles were pointer-only — now focusable, arrow-navigable (clamped, StyleWellGrid's rule), Space picks, Return stays the sheet's default action, focus names the selection one-way. The board's single tab stop shows its focus ring under FKA (focusEffectDisabled inverts). Style editor verified already conformant. Edge accents verified text-free; trash hatch pitch now font-derived so it still reads as hatching at large text. 1549 unit tests green, both schemes build. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY |
||
|
|
273c182ef4 |
Build the VoiceOver tree and actions
The board window's accessibility tree per DESIGN/10: lanes are containers labeled "<title>, lane, N cards" (filter-aware count = renderedCards, the badge's own collection); cards are one flattened element each — label = title or the untitled placeholder, value = attachment count + "cut, pending paste", selection via trait; face icon, stripe, and paperclip are decorative and hidden. Masonry never leaks into traversal: slots carry order-keyed accessibilitySortPriority, so a wide lane reads by card order, not column-major. Lane titles carry the heading trait for the rotor. VO-Space is the ⌘-click analogue routed through the existing BoardStore.click funnel (SelectionGrammar stays the single answer for toggle and container-boundary rules) — cards and lane headers both. Context-menu rows double as custom accessibility actions, each calling the same private method as its menu row so the surfaces cannot drift; trash cards expose Delete and Reveal in Finder and never Open. The trash column is pinned last via sort priority 0, its label/value re-routed through the new AccessibilityPhrases seam; toggling trash visibility posts a one-line announcement from the store seam (both command faces). The invisible lane-resize drag strip leaves the tree — the stepper and menu items are the accessible width path. AccessibilityPhrases is the pure vocabulary seam (labels, values, plural folding shared with TrashModel.phrase), pinned by its own test suite. Both schemes build; 1466 unit tests green. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY |
||
|
|
797d020d01 |
Materialize the trash — faces, menus, and grammar
Phase 3 finishes the pivot at the surface. One card face serves two containers: CardFaceView extracted with a role — board or trash — so stripe, tint, chip, selection stroke, cut dim, marquee registration, and drag are shared by construction, the trash side differing only in its absences: no Open, no rename, no Style, no file-hover highlight, and a Delete that goes through the confirmation host. The column rewrote around the lanes' own single-column masonry so drag reflow reads as positional slides; chrome stays the hatched header, symbol, and count — 11 gives Empty Trash to the File menu alone. Two real grammar bugs die here: plain Backspace on a trash selection purged without the confirmation the menu raises, and the context menu's Delete resolved against the standing selection, so right-clicking a trash card under a board selection silently did nothing — it now stages the clicked set explicitly. Open, Rename, Style, and Empty Trash validation became testable store seams; the column is one named accessibility container of ordinary card elements. The tombstone era is swept: deleteItem, restoreItem, stripTombstonedChildren — dead since lane copies stopped nesting trash — the restore verb, the unreachable put-back banner row, and every quasi-lane doc comment. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY |