Build the attachments sidebar section
The card's complete file inventory: compact QuickLook-thumbnail rows over Card.attachments — no reference tracking, subfolders tolerated and unsurfaced — with a quiet header add affordance and the drop hint empty state. The whole window is the file-drop surface, Edit mode included (the editor's drag types were already filtered; now tested), sharing the board's folder-refusal semantics literally: FinderDrop moved verbatim into its own file so both windows run the same partition and loss row. Dragged text still lands at the caret and is inert elsewhere — the window delegate accepts file payloads only. Rows open on double-click or Return, drag out their file URL, and Remove is a bracketed write through FileManager.trashItem — the system Trash, never a hard delete, returning the in-Trash URL so the promise is testable; the attachment listing is the guard, so traversal and subfolder names refuse in one line. Keyboard-native per 05: the section is one Tab stop, arrows walk rows by name, Space toggles the shared QuickLook panel, Backspace removes. File > Add Attachment (shift-cmd-A) comes alive through the same import path. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
@@ -322,12 +322,15 @@ private final class DuplicateCancellation {
|
||||
/// will drop) disables rather than falling back to the root: revealing the wrong thing is worse
|
||||
/// than nothing, and only a genuinely empty selection means "the board".
|
||||
///
|
||||
// m6-card-window: the item's third scope — the card's folder, or the selected attachment's file
|
||||
// when the attachments section is focused — adds a focused value and a branch here; the two below
|
||||
// do not move.
|
||||
/// **The card-window scope is the third branch**, and it is the one the attachment row's context
|
||||
/// menu twins (11-command-nexus.md ▸ Context menus): "card window: the card's folder — the selected
|
||||
/// attachment's file instead when the attachments section is focused". The rule itself is
|
||||
/// `CardAttachments.revealURLs`, so the menu row and the row's own Reveal cannot disagree about what
|
||||
/// "the selected attachment" means.
|
||||
struct RevealInFinderCommand: View {
|
||||
|
||||
@FocusedValue(\.boardStore) private var store
|
||||
@FocusedValue(\.cardAttachments) private var attachments
|
||||
@FocusedValue(\.welcomeSelection) private var selection
|
||||
|
||||
var body: some View {
|
||||
@@ -338,7 +341,8 @@ struct RevealInFinderCommand: View {
|
||||
}
|
||||
|
||||
/// What the item would reveal, and therefore whether it is enabled — one answer for both, the
|
||||
/// codebase's usual shape. The board in front wins; the welcome branch stands when no board is.
|
||||
/// codebase's usual shape. The board in front wins; the card-window branch stands when a card
|
||||
/// window is; the welcome branch stands when neither is.
|
||||
private var urls: [URL] {
|
||||
if let store {
|
||||
let ids = store.selection.ids
|
||||
@@ -346,6 +350,13 @@ struct RevealInFinderCommand: View {
|
||||
return TrashModel.paths(of: ids, on: store.selection.liveness, in: store.snapshot)
|
||||
.map { $0.folder(under: store.rootURL) }
|
||||
}
|
||||
if let attachments {
|
||||
return CardAttachments.revealURLs(
|
||||
cardFolder: attachments.cardFolder,
|
||||
selectedURL: attachments.selectedURL,
|
||||
isSectionFocused: attachments.isFocused
|
||||
)
|
||||
}
|
||||
guard let selection, selection.canReveal, let url = selection.url else { return [] }
|
||||
return [url]
|
||||
}
|
||||
|
||||
@@ -139,6 +139,15 @@ struct CardWindowHost: View {
|
||||
/// (`CardRawSourceSession`). Beside the body handle rather than inside it: the two are different
|
||||
/// scopes, and the Edit Body row reads both.
|
||||
@State private var rawSource = CardRawSourceSession()
|
||||
/// This window's attachments section — the listing, the keyboard selection, and the two writes
|
||||
/// it starts (05-card-window.md ▸ Attachments). Window-scoped for `CardBodyPresentation`'s
|
||||
/// reason: two card windows on one board have two different selections, and the menu bar reaches
|
||||
/// the frontmost one through the focus system.
|
||||
@State private var attachments = CardAttachments()
|
||||
/// This window's thumbnail memory. Held here rather than in the section so it survives every
|
||||
/// snapshot the store applies — a cache that died with the view would regenerate every thumbnail
|
||||
/// on every reload (`AttachmentThumbnailCache`).
|
||||
@State private var thumbnails = AttachmentThumbnailCache()
|
||||
/// Whether a close is waiting on the dirty-buffer modal. Set when `windowShouldClose` could not
|
||||
/// flush; cleared by the resolution that lets the close resume.
|
||||
@State private var isClosePending = false
|
||||
@@ -210,6 +219,9 @@ struct CardWindowHost: View {
|
||||
// reads it too — "View ▸ Edit Body (⌘E) disables while source mode is active"
|
||||
// (05-card-window.md ▸ Raw source outlet).
|
||||
.focusedSceneValue(\.cardRawSource, rawSource)
|
||||
// File ▸ Add Attachment… (⇧⌘A) and File ▸ Reveal in Finder's card-window scope reach the
|
||||
// frontmost card window the same way (11-command-nexus.md).
|
||||
.focusedSceneValue(\.cardAttachments, attachments)
|
||||
// The raw-source outlet's detailed alert, presented over this window — a validation
|
||||
// refusal on Apply, or a file that could not be opened as source. It hangs *here* rather
|
||||
// than inside the editor because the second of those fires while source mode is still
|
||||
@@ -266,12 +278,32 @@ struct CardWindowHost: View {
|
||||
rawSource: rawSource,
|
||||
// "Under the read-only lock the controls disable in place — an in-content mutation
|
||||
// menu validation can't reach" (05 ▸ Preview). The checkbox is that control, and
|
||||
// the store's own lock is the whole predicate.
|
||||
// the store's own lock is the whole predicate — and it is the attachments section's
|
||||
// predicate too ("the attachment row's ⌫/Remove shares the posture").
|
||||
isEditable: !store.isReadOnly,
|
||||
attachments: attachments,
|
||||
thumbnails: thumbnails,
|
||||
fileDrop: CardWindowDropDelegate(store: store, cardID: placement.card.id),
|
||||
onToggleTask: { offset, checked in
|
||||
store.toggleTaskMarker(inCard: placement.card.id, bodyOffset: offset, checked: checked)
|
||||
}
|
||||
)
|
||||
// **The listing is the snapshot's, republished** — `Card.attachments`, which the loader
|
||||
// fills from `attachments/`'s top-level files in Finder order. Every write in the
|
||||
// section is bracketed, so the reload that refreshes this arrives by itself and the
|
||||
// section never lists a directory of its own (05 ▸ Attachments; the one-way flow).
|
||||
.onChange(of: placement.card.attachments, initial: true) { _, names in
|
||||
attachments.names = names
|
||||
}
|
||||
.onChange(of: Self.cardFolder(root: store.rootURL, placement: placement), initial: true) { _, folder in
|
||||
// Re-derived from the store's *current* root, `cardFolder`'s rule: a mid-session
|
||||
// folder rename moves the board, and rows resolving against where it used to be
|
||||
// would open nothing.
|
||||
attachments.cardFolder = folder
|
||||
}
|
||||
.onChange(of: store.isReadOnly, initial: true) { _, locked in
|
||||
attachments.isEditable = !locked
|
||||
}
|
||||
} else {
|
||||
// Nothing to render and nothing worth animating: this window is on its way out.
|
||||
Color.clear
|
||||
@@ -386,6 +418,31 @@ struct CardWindowHost: View {
|
||||
store: store,
|
||||
cardID: cardID
|
||||
)
|
||||
Self.configureAttachments(attachments, store: store, cardID: cardID)
|
||||
}
|
||||
|
||||
/// Points the attachments section at its card — **the one place Add Attachment… and Remove
|
||||
/// learn which card they act on** (05-card-window.md ▸ Attachments).
|
||||
///
|
||||
/// Both seams are the store's own bracketed methods, unchanged: `importAttachments(_:toCard:)`
|
||||
/// is the *same* call the board window's Finder drop makes, so a file added through ⇧⌘A, through
|
||||
/// the header's plus, through a drop anywhere in this window, and through a drop on the card's
|
||||
/// face on the board all take one path — one collision rename, one set of banners, one commit
|
||||
/// shape. There is deliberately no card-window import of its own to keep in step with it.
|
||||
///
|
||||
/// The store is captured **weakly**, `configureSession`'s rule: a panel still running after the
|
||||
/// board window has gone should write nothing rather than resurrect a released store.
|
||||
///
|
||||
/// `static`, and taking every collaborator as a parameter, for `configureRawSource`'s reason:
|
||||
/// the target resolution is invisible in a running window until it is wrong, and this shape is
|
||||
/// what lets a test drive the real wiring rather than a re-typed copy of it.
|
||||
static func configureAttachments(_ attachments: CardAttachments, store: BoardStore, cardID: ItemID) {
|
||||
attachments.importFiles = { [weak store] urls in
|
||||
store?.importAttachments(urls, toCard: cardID)
|
||||
}
|
||||
attachments.removeFile = { [weak store] name in
|
||||
store?.removeAttachment(named: name, fromCard: cardID)
|
||||
}
|
||||
}
|
||||
|
||||
/// Points the raw-source outlet at its card — the outlet's three seams (05-card-window.md ▸ Raw
|
||||
|
||||
@@ -52,27 +52,10 @@ struct SaveAsTemplateCommand: View {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - File ▸ Add Attachment…
|
||||
|
||||
/// File ▸ Add Attachment… (⇧⌘A) — card window only (11-command-nexus.md).
|
||||
///
|
||||
// m6-card-window: the menu-bar twin of the attachments section's quiet add affordance
|
||||
// (05-card-window.md § Attachments) and of a whole-window Finder file drop. Validation will be scope
|
||||
// alone — a card window in front, the read-only lock aside — `BoardInfoCommand`'s shape for its own
|
||||
// scope-only item.
|
||||
//
|
||||
// m5-context-menus, m6-card-window: the attachment row's own context menu is a second thing this
|
||||
// milestone owes — Open, Remove (system Trash), Reveal in Finder (11-command-nexus.md ▸ Context
|
||||
// menus' Attachment row), twinning the focused section's grammar keys (Return open / ⌫ remove — 05
|
||||
// ▸ Attachments) and Reveal in Finder's attachments-focused scope, exactly the way this milestone's
|
||||
// card and lane menus twin their own grammar and menu-bar commands: no new store method, no parallel
|
||||
// implementation. There is no row view to hang a `.contextMenu` off yet, so nothing scaffolds here
|
||||
// beyond this marker.
|
||||
struct AddAttachmentCommand: View {
|
||||
var body: some View {
|
||||
FutureCommand(title: "Add Attachment…", key: "a", modifiers: [.shift, .command])
|
||||
}
|
||||
}
|
||||
// File ▸ Add Attachment… (⇧⌘A) was the scaffold here and is now live, beside the focused value it
|
||||
// reads (`AddAttachmentCommand`, in `CardAttachments.swift`) — the diff this file predicts: the
|
||||
// title and the chord did not move, the validation and the action filled in. The attachment row's
|
||||
// context menu (Open / Remove / Reveal in Finder) shipped with it, on the row view that now exists.
|
||||
|
||||
// MARK: - Edit ▸ Find Next / Find Previous
|
||||
|
||||
|
||||
Reference in New Issue
Block a user