Build the card window shell and lifecycle
The m4 scene plumbing was already honest — one WindowGroup value per CardWindowRef enforces one-window-per-card, and CardWindowFate's ancestor walk answered dismissal — so this card fills the window: a two-column shell whose body column takes all resize flex and whose sidebar width derives once from font metrics (26 characters of average body advance plus em gutters), the five 05-ordered section headers as placeholders, and the card body as selectable plain text until Preview mode lands. The fate walk now returns a CardPlacement (card + lane), so one pass answers both liveness and the live board › lane subtitle; a board rename lands for free through displayName. Card windows remember their frames per card in the board record (case-folded id keys, unchanged-writes-nothing), restoring instead of cascading; only unremembered cards take the last-used size and cascade. Store acquisition stays gated on liveStore — a card window never opens a board — and the close-flush hook stands with nothing to flush until the Edit-session card. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
@@ -99,6 +99,25 @@ public struct BoardRecord: Codable, Sendable, Equatable, Identifiable {
|
||||
|
||||
public var windowFrame: WindowFrame?
|
||||
|
||||
/// This board's **card** window frames, keyed by card GUID (05-card-window.md ▸ Window: "frames
|
||||
/// restore per card across relaunch where state restoration allows").
|
||||
///
|
||||
/// Here rather than in `UserDefaults` for the reason the board's own frame is here: a frame
|
||||
/// belongs to a board that the bookmark follows through renames and moves, and a path-keyed
|
||||
/// preference would lose every card frame the first time the board was renamed. And here rather
|
||||
/// than in the board folder because files-first is absolute — a window frame is per-machine app
|
||||
/// state, never board data.
|
||||
///
|
||||
/// **The key is the card id case-folded** (`ItemID`'s comparison rule), so a folder respelled
|
||||
/// `ABC…` finds the frame stored under `abc…` — the same identity rule the window key itself
|
||||
/// uses, since the two must agree about what "this card" means.
|
||||
///
|
||||
/// The honest residual: entries accumulate for every card the user has ever opened a window for
|
||||
/// on this board, and a card deleted afterwards leaves its entry behind. Accepted — the record
|
||||
/// is per-machine convenience state measured in tens of bytes per entry, and it dies wholesale
|
||||
/// with Forget like everything else here.
|
||||
public var cardWindowFrames: [String: WindowFrame]?
|
||||
|
||||
/// The board's `icon` — registry-cached with live write-through, beside `displayName`
|
||||
/// (02-architecture.md § Per-board app state: "The row's title and icon are registry-cached
|
||||
/// too — with live write-through"). `nil` when the board's `icon` key is missing or
|
||||
@@ -150,6 +169,7 @@ public struct BoardRecord: Codable, Sendable, Equatable, Identifiable {
|
||||
laneCount: Int? = nil,
|
||||
cardCount: Int? = nil,
|
||||
windowFrame: WindowFrame? = nil,
|
||||
cardWindowFrames: [String: WindowFrame]? = nil,
|
||||
isOpenNow: Bool? = nil,
|
||||
pushOnCommit: Bool = false,
|
||||
remoteLocationWarned: Bool = false,
|
||||
@@ -164,6 +184,7 @@ public struct BoardRecord: Codable, Sendable, Equatable, Identifiable {
|
||||
self.laneCount = laneCount
|
||||
self.cardCount = cardCount
|
||||
self.windowFrame = windowFrame
|
||||
self.cardWindowFrames = cardWindowFrames
|
||||
self.isOpenNow = isOpenNow
|
||||
self.pushOnCommit = pushOnCommit
|
||||
self.remoteLocationWarned = remoteLocationWarned
|
||||
@@ -434,6 +455,28 @@ public final class BoardRegistry {
|
||||
update(id) { $0.windowFrame = frame }
|
||||
}
|
||||
|
||||
/// One card window's remembered frame on this board, or `nil` when that card has never had one
|
||||
/// (05-card-window.md ▸ Window). The caller decides what "no frame" means — for a card window it
|
||||
/// means "open at the last-used size and cascade".
|
||||
public func cardWindowFrame(id: UUID, cardID: ItemID) -> WindowFrame? {
|
||||
record(id: id)?.cardWindowFrames?[cardID.canonicalValue]
|
||||
}
|
||||
|
||||
/// Remembers one card window's frame. Keyed by `ItemID`'s own comparison value, so the read
|
||||
/// above finds it whatever case the card's folder is spelled in.
|
||||
///
|
||||
/// **Unchanged writes nothing**, `syncDisplayState`'s rule: a card window reports its frame on
|
||||
/// every move and at the end of every live resize, and the board window's twin already saves on
|
||||
/// each of those — this one must not add a registry file write for a frame that did not move.
|
||||
public func updateCardWindowFrame(id: UUID, cardID: ItemID, frame: WindowFrame) {
|
||||
guard cardWindowFrame(id: id, cardID: cardID) != frame else { return }
|
||||
update(id) { record in
|
||||
var frames = record.cardWindowFrames ?? [:]
|
||||
frames[cardID.canonicalValue] = frame
|
||||
record.cardWindowFrames = frames
|
||||
}
|
||||
}
|
||||
|
||||
/// The live write-through for an *open* board's title, icon, and iconColor
|
||||
/// (02-architecture.md § Per-board app state, "these three refresh whenever an open board's
|
||||
/// reload changes them"). `BoardStore` calls into this — indirectly, through the delegate
|
||||
|
||||
Reference in New Issue
Block a user