3180e565e8c50f888abe847389057a529b4b64ed
48
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
10f4e9b452 |
A lane's context menu finds its five groups — style, clipboard, width, the fold, and the trash
LaneView.laneMenu/laneActions restructured to the owner's shape (card 22f660b0),
twinning the card menu's own reshape from earlier today (
|
||
|
|
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 |
||
|
|
41380a81d7 |
The lane collapse chevron shows a resize cursor on hover — now it shows the standard arrow cursor
Added .onHover modifier to the collapseButton to explicitly set NSCursor.arrow when hovering over the collapse chevron, preventing the lane resize handle's resize cursor from appearing over the button. |
||
|
|
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 |
||
|
|
eb950ee881 |
Remove the lane header's "+" new-card control
The lane header no longer draws a new-card button. Every other creation path is untouched: File > New Card (Cmd-N), Return on a selected lane, and double-click on lane empty space. - LaneView: drop the button and its overlay slot, leaving the collapse chevron as the header's one piece of trailing chrome. - NewCardTarget: drop the button's click-names-target carve-out over the Cmd-N target rule (Return on a selected lane is now the only direct-target path left). - AccessibilityPhrases: drop the button's spoken label. - BoardMetrics: drop its width reserve; laneHeaderTrailingReserve now reduces to the collapse chevron's own reserve. - DESIGN/03, 04, 10, 11: update the lane title-bar inventory, the Cmd-N override clause and search-clearing mechanism list, the accessibility tree-shape sentence, and the pointer-grammar row that described the button. - Tests: drop the two pinned accessibility-label tests and the newCardButtonReserve assertions; retarget the truncation-headroom test at the now-solo laneHeaderTrailingReserve; fix stale doc-quote comments and the manual accessibility-verification checklist. Full KanbanTests suite: 2807 tests, 482 suites, all passing. Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy |
||
|
|
bab456c08d |
Collapsible lanes — frontmatter-backed slim strips outside the width division
A lane folds to a fixed slim vertical strip carrying its glyph, its card-count badge and its title turned on its side, and the strip is deliberately not part of the window's division: the expanded lanes' units divide what is left once each folded strip's fixed width has come off the top, so folding a lane is a re-divide trigger of the Show/Hide Trash family — the window never moves and the siblings grow into what the lane gave up. The state is a first-class lane frontmatter key, `collapsed: true`, and document state exactly as `width` is: the files are the board, so an agent folds a lane by writing one key. Absent means expanded, expanding removes the key rather than writing `false` (the remove-at-default family beside a one-unit `width`, the empty rename's `title` and the None well's `background`), and the lane's `width` rides along untouched so expanding restores the lane the user had. The read is `width`'s leniency one type over — a boolean scalar or a quoted boolean word reads as itself, everything else has no reading at all and renders as expanded, bytes preserved either way. Toggling is the header's always-visible collapse chevron, the lane context menu's single Collapse Lane / Expand Lane row, and a plain click anywhere on the strip; a modified click on the strip stays the ordinary selection grammar, so a folded lane is still selectable by pointer. The title reads bottom-up and is justified to the top of the room below the strip's chrome (owner ruling 2026-08-08), truncating against the strip's own height. While folded the lane draws no cards at all, which is what makes every exclusion true by construction rather than by a guard per gesture: no card face means no marquee target and no navigation frame, and no registered grid means the masonry's drop zones have nothing to resolve against. What did need code is the half that names absolute destinations — the option-arrow jumps and the arrow seed scan past a folded lane, the lane domain's down-arrow is inert on one, and New Card skips it (a selection inside one falls through to the last-active lane, the stale selection's rule). A drop on the strip appends at the lane's end, cards and Finder files alike, with an accent edge standing in for the shadow the strip has no masonry to open; there is no hover-to-auto- expand yet. Lane reorder works on the strip, and a dragged folded lane carries its fold, so its shadow and its replica are the strip rather than its units. The write is `writeLaneWidths` clause for clause — one `updateIndex` bracket, the same stamp behaviour, the same three do-nothing paths — with two new `WriteOperation` cases and two new undo verbs rather than one of each, because a banner or an Edit-menu row that said "resize" after Collapse Lane would name a control the user never touched. 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 |
||
|
|
51cf994cb9 |
Lane selection sheds its slab — a hairline edge with a soft halo, intensity inverse to area
The whole-lane wash-and-solid-ring scaled its emphasis with the lane's area: what read as a ring on a card read as a slab on a column. Resettled as the system focus ring's vocabulary at selection strength — a hairline accent edge with a soft accent glow bleeding outward, riding the stroke shape rather than the translucent plate (a shadow behind it would bleed through as murk), and no wash at all: nothing tints the cards' backdrop, and lane colour stays the accent band's alone. Under Increase Contrast the halo yields to the solid full-alpha ring — "strengthens borders" means crisper, and a blur is the one thing a border cannot become — so the setting sees exactly the indicator it always has; cards and trash rows are card-scale and keep the solid ring at every contrast. One selection vocabulary, pinned by Accommodations.drawsSelectionHalo and its test. 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 |
||
|
|
eef0a4539f |
Clicks land the instant they happen — the empty-space gestures move behind the masonry
The measured defect: LaneView's empty-space double-click was a second sequential .onTapGesture(count: 2) stacked over the single tap, and that recogniser held every click in the lane — its own empty space and every card face alike — hostage to the system double-click interval while it disambiguated (~475 ms click-to-selection on a hosted board). The fix is structural: the empty-space surfaces live on a background layer behind the masonry, so a card's click never shares a gesture path with a lane recogniser; one .onTapGesture branches on PointerClick.count (AppKit's own clickCount, read the way ClickModifier reads the keyboard) — first click selects, second creates, Finder's cadence with nothing to disambiguate; and the layer carries a load-bearing empty .onDrag, because without a drag source macOS holds primary clicks pending multi-click disambiguation (measured: never fires alone, ~90 ms with one present). A measured viewport floor makes each lane's blank space actually belong to the layer — a ScrollView proposes nothing along its scroll axis, so only an explicit minimum stretches the content — with the trash column as its twin, less the padding that sits inside its scroll content. The template chooser's stacked pair collapses to the same one-recogniser branch, and the attachment rows move their double-click to a simultaneous gesture (instant there, because the row's real .onDrag forces immediate delivery). PointerLatencyTests pins the recovery with synthetic pointer events on a hosted board. 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 |
||
|
|
d9b6232dea |
The lane plate's strays return to their anchors — the replica's wash leaves the height arithmetic it broke
The pathfinder surface stack landed in
|
||
|
|
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 |
||
|
|
f174a524af |
Masonry goes column-major — cards read top-down, then across
Replaces the pathfinder-inherited round-robin deal (child i -> column i % C) with contiguous column segments: base = n/C, the first n%C columns take one more, and logical order runs down each column before crossing to the next. Only the geometric mapping changes -- ranks, selection flatten, and VoiceOver order are untouched, and MasonryPlacement stays the single placement function both the Layout and the drop model replay. Why: an insertion under round-robin shifted every later card across columns; under the column-major deal later cards slide within their column and at most one card crosses each boundary, so the drag reflow is far calmer. Drop-slot math gets simpler too -- a column's cards are one contiguous range, a non-final column's tail is now a genuine mid-list position, and only the last column's tail means append. DropSlotMathTests recomputed and extended (46 -> 50): the uneven-fill deal, boundary positions, the shared tail/head boundary index, and a placement/ drop-model shadow-agreement check. DRAG-REORDER.md and DESIGN/10 amendments are listed for ratification, deliberately not edited here. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY |
||
|
|
95133860e1 |
Drag pickup: evaluate a lane's slots once per body, not once per card
The per-element .accessibilitySortPriority read slots.count from inside the ForEach closure, re-running the whole slots -> renderedCards chain (an O(n) card filter plus the session's hidden-member resolution) once per element -- O(n^2) per lane body. A drag pickup runs a synchronous whole-board layout inside the drag-start nested run loop, which multiplied this into a stack- sampled ~700ms-1s stall between mouse-down and the visible lift. Hoisting the evaluation into a local makes pickup effectively immediate. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY |
||
|
|
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 |
||
|
|
f83385e79f |
Lane drag preview attaches at the grabbed title bar, not its middle
SwiftUI's .onDrag(_:preview:) exposes no anchor or grab-point API and lays the preview centered over the grabbed view, so a full-height lane replica grabbed by its ~1.5-line title bar hung half a lane above the pointer. DragPreviewAnchor is the pure fix: transparent one-sided padding computed so the padded image's center IS the replica's title bar - the system's centering then lands the bar under the cursor and the body over the lane it was lifted from. LaneView measures the real bar height in the geometry observer it already runs, accounting for the accent band when the lane's color resolves. Pixel-exact grab preservation would need re-homing the whole gesture stack onto an AppKit beginDraggingSession path; declined for a polish card - this gets the cursor onto the grabbed bar under either of SwiftUI's possible placement rules. Manual verification pending (no display here): grab specific bar pixels, colored + uncolored lanes, large text sizes, multi-lane fan, cross-board drop geometry. 9 tests. 1661 green on both schemes with the paired card-width fix. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY |
||
|
|
92a088fdd3 |
Runtime contrast for hand-written hex backgrounds
DESIGN/10's ≥ 4.5:1 rule gets its owner. ContrastMath is the pure seam: WCAG relative luminance (piecewise sRGB linearization), symmetric contrast ratio, source-over compositing (an #RRGGBBAA board colour resolves over the window background of the active appearance), and inkChoice — native label if it clears AA, else the other appearance's, else the higher ratio with meetsAA false (a mid-grey hex can max out below 4.5 against both 85%-alpha labels; the app paints the best available rather than overriding the user's colour). BoardTextInk is the board's application: the decision is a ColorScheme, not a Color — the text on the board fill is a hierarchy (.primary/.secondary/ .quaternary), and overriding the subtree's scheme moves the whole vocabulary coherently. Recomputed on appearance change by construction (read in body); label/backdrop colours resolve inside the asked-for appearance, Increase Contrast variants included. Two render sites — the only board text that sits on the user's colour: the lane header (lanes draw no plate; title, icon, badge, rename field and the + button land directly on the board fill) and the trash header (its wash is ~5% effective alpha). Menus, popovers, and drag replicas deliberately stay native; card faces carry their own opaque plates. The card's premise fell during implementation: 03's "palette pairs AA-verified at design time, pinned by a computed-contrast unit test" was false — no such test existed, and the m4 path drew the native label, failing AA in one appearance for all twelve wells (obsidian in Light Mode: 1.0:1). Palette names now route through the same ink selection (paintedColor delegates to Palette.nsColor — one predicate with BoardView's paint decision), and PaletteContrastTests pins that the chosen ink clears AA for every well in both appearances — plus theNativeLabelIsNeverEnough, which would have failed on the m4 code. Filed on the Redesign board for ratification. 1579 unit tests green, both schemes build. 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 |
||
|
|
53bc71f7fb |
Materialize the trash — store, undo, and the container universe
Phase 2 swaps every consumer: Liveness and its ancestor walk are gone, replaced by ItemContainer — a UUID set plus the container side it lives on, presence the whole test, one selection boundary instead of the old liveness law. Deletion stages by place: board cards move to the trash at a store-minted head rank, trash-side delete is permanent behind its confirmation, Delete Immediately skips the trash from anywhere, lane delete captures the subtree and removes the folder. Restore has no method at all — moveCards resolves members in either container, so drag-out and cut-paste are the ordinary moves 13 calls them, registering ordinary Move steps. The delete inverse moves the card back to its captured lane and rank; redo replays the captured trash rank, a value the gesture actually wrote; lane undo recreates the subtree byte-faithfully in session. Purges register nothing — where 13's trash section contradicts its own Rules on that, Rules wins, filed for ruling. Staleness collapsed to present-or-absent: a container is a path, so a foreign restore fails the delete step's expectation structurally. Legacy tombstones migrate on the loose-file tail hook, cards oldest-first so minting above top reproduces the retired newest-first column, lanes returning live, one folded loss row naming both directions. Put Back, restoreByDrag, receiveRestoredCards, TrashEntry, and the kind machinery are deleted; the trash column renders the container correctly with its full face rework left to phase 3. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY |
||
|
|
7f1adf47c5 |
Back out the render-at-release drop presentation
The settle rule
|
||
|
|
5c0c0e5619 |
Realign search, clipboard, and lane-hover code with the second batch
Creation now clears the search by mechanism, not gesture: one seam (noteUserCreation) states 04's rule once, called from the placeholder funnel, paste — cards and lanes, after the staleness guard so a stale paste clears nothing — and Finder file-drop creation; the attach path deliberately doesn't clear, and cross-board arrivals and New Lane stay outside the seam (a transfer isn't creation; a lane can't be born invisible). An open inline rename now survives the filter hiding its card: the model already kept the editor, but the field renders in the card's slot, so renderedCards keeps the renaming card's slot exactly as long as the editor is open — the query stands throughout, and commit or Escape lets the predicate apply in the same pass. Verified conformant and newly pinned: query-emptied lanes keep their slot with a 0 badge, pasteboard staleness (takeover before paste and mid-staging both no-op), out-transition reachability and the strip pre-divide hold by construction with comments citing their rulings. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY |
||
|
|
524488122f |
Land Finder file drops positionally, header release topmost
04's settled clauses were mostly shipped already — the create landing resolved through DropSlotMath.cardSlot with one nominal shadow per importable file — but a release on the lane header fell through to the card zones, which clamp inward, so a scrolled lane could propose behind the header stripe. FileDropZones now folds header, attach hit-test, and card-slot resolution into one pure seam asked in that order, the header answering topmost per the ruling; lane headers register their frames for it. FinderDrop.shadowCount names the floor-at-one rule. New tests pin the header boundary, a differential against cardSlot's own zones (same zones, not similar), and a store-level differential proving a file landing takes the very ranks a card move there takes. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY |
||
|
|
1020d9fca4 |
Remove the face carousel — one card presentation
03's resettlement reverses the pathfinder carry-over: the selection-keyed dual presentation proved undesirable, so a card has one presentation — selection changes styling, never geometry, and the masonry never reflows on click. Deleted the carousel view (page dots, glass underlay, scroll-tick monitor), the QuickLook thumbnail cache (sole consumer), the pure paging/suppression rules, and the sole-selected animation key — Motion now keys transactions on the search query and the drop proposal only. The attachment chip stays as the face's whole attachment story; viewing media is the card window's job. No carousel state had leaked beyond the view layer. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY |
||
|
|
3f4125e324 |
Render the dropped card at release, and pin the hold's timeout
03's sharpened settle rule: rendering the arrangement means rendering the card — at release the shadow swaps for the dropped card(s) drawn in place immediately, the appear never waiting for the echo reload. The committed hold now carries the landing (ids, payload titles, operation) and surfaces read one DropLanding seam: within-board moves draw the real faces at their proposed slots under the arriving card's own key, so the echo is an invisible content swap; cross-board card arrivals draw payload-titled faces keyed positionally, so the echo reads as an ordinary arrival. Cross-board lane arrivals deliberately keep their shadow until the echo — a lane's face is a whole column with no honest payload equivalent. The 1500 ms failed-write timeout is now seamed (injectable duration, extracted expire) and pinned by tests. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY |
||
|
|
15006ad233 |
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 |
||
|
|
2fc4020a2b |
Build the sole-selection attachment carousel
The card face's in-place expansion (03-board-ui.md § Card face): a card with attachments, as the sole selection, grows a paged media band below its title; every other state stays compact behind the quiet paperclip. - CardCarousel owns the pure rules: the sole-selection predicate decides by identity (a sole-selected lane matches no face, no snapshot walk), and expansion is suppressed outside the animation key while a rubber band is active — a band names a set in progress, so carousels neither flicker nor animate under it. - QuickLook thumbnails generate off-main into a per-window cache keyed to survive reselection, with the Finder-icon fallback while loading and for non-previewable types; pages ride the platform paging behavior, dots (glass underlay, solid under Reduce Transparency) click to page, and a local wheel monitor turns a discrete tick into one clamped page — precise trackpad pans fall through untouched. - The expansion animates under Motion's new carouselExpansion transaction keyed narrowly on the sole-selected card; Reduce Motion goes instant. Drop-slot math and the marquee read the expanded height for free — both re-register on every size change. 928 unit tests (24 new). m5-interactions complete. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY |
||
|
|
6c490ec71c |
Build per-surface context menus
The remaining rows of 11-command-nexus.md § Context menus, every entry a twin of an existing command path, never a parallel implementation: - Card: Open (the double-click's own openCard closure, always the clicked card alone), Rename (Board ▸ Rename's beginRename path), Style… and the quick-style recents (already present), Delete (File ▸ Delete's store.delete on the standard widened target — selection when the clicked card is a member, else the card alone). - Lane (one menu, header and empty space): Rename and Delete join the existing Style…/recents/Width rows, in table order. - Trash entries and welcome recents verified already exact against the table; the attachment row's menu is marked for m6 beside its command. - File ▸ Reveal in Finder gains its board-window scope, the branch the m4 comment deferred here: the selection's folders on either side of the trash boundary — enabled under every lock, inspection being a read — or the board root with nothing selected; a selection resolving to no folders disables rather than guessing. 904 unit tests. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY |
||
|
|
cf87b72092 |
Implement live search filtering
The board's live title+body filter per 04-interactions.md § Search: - SearchFilter — a pure value folding the query once (case- and diacritic-insensitive substring, locale-stable); title OR body matches, attachment filenames never searched; only the literal empty string is inactive. - One universe: the filter threads through SelectionGrammar's order lists as a defaulted parameter, so ranges, Select All, arrow navigation, the marquee, drop zones, count badges, and the shown trash all read the same filtered set by construction; lanes are deliberately never filtered out (an emptied lane keeps its slot with a 0 badge). Hidden cards leave the selection through the existing constrain primitive, run on every query change and as the last line of the reload resolve; the delete successor is filtered so ⌫ never selects a hidden neighbour. - The field: an NSSearchField-backed toolbar item (the toolbar's sole default item); Edit ▸ Find ⌘F focuses it through a focused-value presentation; stock field-editor dispatch — Return swallowed, Tab is the keep-filter path to the board, board commands stay enabled except the caret-chord pair, now one shared caretChordsYield expression. - Escape is staged: clear the non-empty query (focus stays), hand an empty field back to the board, clear an active search from board focus — before Escape's clear-selection meaning. - Creating a card clears the search (the placeholder funnel); a rename deliberately gets no carve-out; filter reflow rides the content spring keyed narrowly on the query. 903 unit tests (24 new). Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY |
||
|
|
7eee0934ee |
Implement the hybrid clipboard with deferred cut
⌘X/⌘C/⌘V for cards and lanes per 04-interactions.md § Clipboard: - ClipboardStore stages full folder snapshots eagerly at the gesture into Application Support (at most the current copy; sweep at launch and on each copy purges what the pasteboard no longer references; a copy made before quitting pastes whole after restart) and writes the pasteboard a JSON manifest — every entry embedding its index.md, lane entries their cards' too — plus plain-text titles. - Cut is Finder-style deferred: items dim in place off pendingCut, void on pasteboard takeover (changeCount, no timers), source-board close, or per-item external tombstoning; the first armed paste moves the surviving originals whole (tombstoned interior cards land in the destination's trash), a second paste materializes copies from staging. - Paste anchors by the shared flatten-order rule (NewCardTarget's anchor, extracted); a tombstoned selection never anchors; lane paste reaches the right end and stays enabled on a zero-lane board; paste into the source board is the within-board lane duplicate; copies keep created, take fresh GUIDs, and strip tombstoned cards; trash-sourced copies strip deleted: at materialization; ⌘X is disabled on the trash side. - A degraded paste is loud, never silent: staging gone → the embedded index.md fallback lands content-intact, attachments absent, and a BannerCenter-phrased row names what was lost. - The standard Edit items validate through conditionally-attached onCommand handlers, so AppKit's enablement mirrors the availability predicates; text fields keep their own clipboard while focused. 879 unit tests (68 new). Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY |
||
|
|
8f116934b4 |
Implement Finder file drops
Files from Finder land on the board per 04-interactions.md § Drag & drop: - Dropped on a card, they copy into its attachments/ (any type, multi-file), the face highlighting while hovered; the hovered card is resolved by hit-testing the same analytic masonry frames the card zones are built from, so attach-beats-create adds no drop region and cannot drift from the dispatch. - Dropped on lane empty space, one card per file — filename minus extension as the title (a blank stem omits the key), fresh GUID, rank at the drop position through the ordinary insertion machinery, the file attached — all in one bracket; a failed import removes the just-minted card, so creating-then-abandoning never leaves an empty card behind. - Every board drop surface now declares .fileURL beside the two board types (the single-target-dispatch rule); tombstoned surfaces are inert; file sessions ride a distinct session mode with their own watchdog and no hysteresis, leaving the board-drag machinery untouched. - Also: four empty fixture directories pinned with .keep files so git preserves them, and a test-only visibility fix in DragSessionTests. 811 unit tests (12 new). Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY |
||
|
|
90cf82d740 |
Implement drag & drop with the locality model
The second half: system drag sessions over phase 1's model, per DRAG-REORDER.md and 04-interactions.md § Drag & drop. - Card faces, lane headers, and trash rows drag as NSItemProvider sessions (two exported UTTypes, JSON payload in flatten order, plain-text titles as the secondary representation) — replacing m4's custom lane-reorder gesture and trash drag-out wholesale; the app-wide DragSession carries the members, the frozen dragged sizes, the live proposal, and the effective operation. - Three drop delegates (lane masonry, strip, window fallback), each accepting both types and routing internally per the single-target-dispatch rule; the cursor is the physical mouse converted to strip space; proposals come from DropSlotMath with hysteresis threaded through, and the lane-strip proposal clamps in front of the shown trash. - Locality picks the default — move within a board, copy across, the badge tracking live; ⌥ forces copy (ignored on within-board lane drags), ⌘ forces move; trash rows restore within their board (positional), copy out across boards by default, ⌘ forcing the true restore-move. - N contiguous shadows with reflow keyed on the proposal; the committed-overlay hold renders the dropped arrangement until the reload echo lands (1.5 s dissolution deadline for refused writes); the re-grounding trio: geometry re-derives per render, proposals re-validate by liveness at release, an emptied drag cancels itself. - Edge autoscroll (ticking driver over DragAutoScrollMath, re-targeting per step), the mouse-up-gated late-event cleanup, and the polling watchdog — the pathfinder's lifecycle traps, ported. - Store: moveLanes and multi-card restoreByDrag join the one-bracket drop commits. 784 unit tests. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY |
||
|
|
4035ba7986 |
Implement the keyboard grammar and full command map
The board's fixed grammar keys and the menu-backed chords of 04-interactions.md § Keyboard, per the Command Nexus inventory: - Spatial arrow navigation (NavigationMath.nearest over the marquee registry's frames — one geometry source), walking across interior masonry columns, lanes, and into the shown trash; ⇧-arrows extend via the same range function as ⇧-click and go inert at the liveness and kind boundaries; ⌥-jumps with the ⌥↑ lane-domain escalation and ↓ descent; the empty selection seeds at the first lane's first card; selection scrolls into view. - selectionHead — the navigation cursor beside the anchor, set by every click, moved by every arrow, dropped by the reload vanish rule. - Board ▸ Open Card ⌘↩ (the one command enabled mid-edit: commits the placeholder or rename and opens), Move Up/Move Down ⌥⌘↑/⌥⌘↓ (within-lane sort, gather-then-step, rank-permuting writes in one bracket), Move Left/Move Right ⌘←/⌘→ (sole lane, one slot, never the trash) — all validating and acting off one shared answer. - Delete now selects the Finder-style successor sibling from the pre-write snapshot, so repeated ⌫ walks down a lane; external vanishing still only shrinks the selection. - handleReturn rejects modified Returns; the trash column renders eagerly so every row stays registered for navigation and the marquee. 686 unit tests (27 new). Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY |
||
|
|
2e229735b1 |
Implement the selection model
The full pointer grammar of 04-interactions.md § Selection, stated once as a pure function (SelectionGrammar) and reached through one store funnel from every click surface — card face, lane header, lane empty space, trash row: - Plain click replaces and anchors; the lane surfaces (header and empty space alike, per the settled one-lane-click-behavior rule) toggle off on a sole-membership repeat. - ⌘-click toggles within a homogeneous set; crossing any axis — cards XOR lanes, live XOR trashed, card entries XOR lane entries in the trash — degrades to a replace, so no click can produce a mixed selection. - ⇧-click ranges from the anchor in the (side, kind) order list: flatten order for cards, lane order for lanes, the trash's deterministic sort filtered to kind — the pointer twin of the keyboard's boundary rule (the keyboard goes inert, the pointer skips). - The rubber band (MarqueeSession/MarqueeMath) arms from lane empty space, the board backdrop, and the trash column; side frozen at the origin, trash bands homogeneous by topmost kind, frames self-registered in strip space, geometric begin guard, never animated. - Fast plain double-click opens the card window (⌘↩'s pointer twin); Select All answers the standard Edit menu item via the responder chain, trash- and kind-respecting. - The range anchor lives in TransientBoardState beside the selection and obeys the same reload vanish rule. 659 unit tests (28 new in SelectionGrammarTests). Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY |
||
|
|
5ebf9fb90c |
Implement the motion language
One named home for every curve and duration — the two-voice split 03 fixes (snappy structural: drag reflow 0.18, delete 0.25, lane resize 0.2; smooth content-reflow 0.28), the scale-and-fade appear/disappear transitions (cards 0.8, lanes 0.9), and a Reduce Motion variant on every accessor (animations go instant, transitions go crossfade). A post-migration grep holds the invariant: zero motion literals outside Motion.swift. The animate-vs-snap split lands where Lanework's one-way flow puts it: the store's reload seam. An app-mediated echo applies its snapshot inside the structural transaction; foreign and reconciling reloads — and every bracket-ending wholesale reload — assign bare, because live-reload is the board becoming what's on disk, not an event to perform. A window that merged foreign events into an app-mediated span animates, deliberately: the ratified merge rule makes it indistinguishable from a pure echo, and a test pins that reading so a future mixed case fails loudly. The lane-reorder reflow keys on the drop proposal alone (pointer tracking stays 1:1), the resize session freezes its Reduce Motion answer at drag start so the unit tick and the window resize can never disagree, and the m5 drag / m5 search / m7 undo voices are named seams waiting for their call sites. 13 new tests. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY |
||
|
|
c6298c2e41 |
Build the styling system and shared style editor
One style-editor component, anchor-agnostic: a background grid (None well plus the 12 palette colors) and a curated symbol grid (the pathfinder's five-dozen set, leading well removing the icon key for the level default), selection-aware across cards, lanes, and the board itself. Batch edits compute per-dimension state — uniform, mixed (no well selected), or an off-palette value labeled verbatim outside the grids — and choosing a well applies to the whole target set as one write bracket, skipping no-ops per field. The popover tracks its target set live per the freshly ratified rule: targets re-resolve by UUID on every reload, a vanished target leaves the set, an emptied set dismisses the editor, and nothing ever silently retargets to the board. Anchors landing now: Board > Style (Opt-Cmd-S) and the card/lane context menus, which also carry the quick-style recents row (app-wide, persisted, capped at six, None never recorded) and the lane's width control twinning the menu chords. The styling system's other two renders arrive with it: a lane's background paints the C7 top-edge band, the board's paints the window content background — malformed values paint nothing and stay byte-identical on disk. 31 new tests. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY |
||
|
|
b4c90838b4 |
Build card faces with edge-accent styling
The card face becomes real: leading SF Symbol (card default doc.text, tinted by a valid hand-written iconColor — schema yes, control no), title or the quiet untitled placeholder, and a quiet paperclip when the card has attachments — title-only by design, no body excerpt. Color is the settled K1 edge accent, not a fill: background paints a 4pt stripe down the left edge, resolved through the ported pathfinder palette (12 icon tints + 12 backgrounds carried over verbatim, plus raw #RRGGBB[AA]); anything unresolvable paints nothing and stays on disk exactly as written. The snapshot now carries each card's flat attachment names — the loader's one read inside a card folder, shared with the Writer's listing so the m5 carousel and m6 sidebar can never disagree on order (Finder order, the Writer's existing comparator). The face keeps its top-aligned structure so the sole-selection carousel can expand inside the card without moving masonry neighbors. 18 new tests. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY |
||
|
|
b35566e0fe |
Build lane chrome — title bar, badge, inline rename
The lane title bar becomes real: leading SF Symbol (hand-written names render leniently, unknown ones fall back to the level default), title or secondary untitled placeholder, a quiet count badge that counts exactly the cards the body renders (so the m5 search filter is followed by construction), and a new-card button. The whole bar is the reorder drag surface — no grip — with click-vs-movement splitting select from drag; a pure proposal function maps the drag to an insertion index and release commits through the Writer's same-parent degenerate reorder, compacting and retrying when midpoint precision runs out. Clicking never edits: inline rename is Return on the sole selected card or Board > Rename for either kind, a third transient editor beside the placeholder that tracks its target by UUID, commits on focus loss, discards silently when the target vanishes, and removes the title key on an empty commit. The new-card placeholder renders at last — the settled Cmd-N target rule (pure, tested) files it after the anchor card, at a selected lane's bottom, or into the last-active lane; Return commits and re-selects the lane, Cmd-Return also opens the card window, and a failed create discards the overlay. New Card / New Lane / Rename land in the menus with focused-editor and read-only validation; rename gets its own WriteOperation case in the banner vocabulary. 59 new tests. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY |
||
|
|
ff3ba298f0 |
Build the full-visibility lane layout
The lane strip replaces the placeholder board: window width divides
across the lanes' width units (no horizontal scroll, no minimum width,
degenerate compression accepted), a lane of n units flowing its cards
into n round-robin masonry columns via a measurement-cached Layout.
Width has two deliberately opposite controls, both landing here: the
right-edge drag (ported verbatim from the pathfinder's ColumnResize)
freezes the 1x standard at drag start, snaps between integer widths
with the asymmetric shadow-leads tick and 10pt re-entry, grows the
window one standard width per snap so siblings keep their exact
pixels, and rubber-bands at the screen's visible frame — uncapped
otherwise; the Increase/Decrease Lane Width items (new Board menu,
Cmd-Opt-arrows) are the stepper's keyboard face and re-divide the
existing window width instead, never touching the window. Width
changes write through the new .resize WriteOperation ("Couldn't
resize…" in the banner vocabulary, which grows with the surfaces by
design); malformed width values render as one unit and stay untouched
on disk. 27 new tests port the pathfinder's resize-math suite onto
the uncapped range and pin the write path's fidelity.
Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
|