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:
@@ -1,4 +1,5 @@
|
||||
import SwiftUI
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
// MARK: - CardWindowView
|
||||
|
||||
@@ -52,6 +53,14 @@ struct CardWindowView: View {
|
||||
let rawSource: CardRawSourceSession
|
||||
/// Whether a checkbox may write — `false` under the board's read-only lock.
|
||||
let isEditable: Bool
|
||||
/// This window's attachments section: the listing, the selection, and the two writes it starts
|
||||
/// (05 ▸ Attachments).
|
||||
let attachments: CardAttachments
|
||||
/// This window's thumbnail memory, held by the host so it outlives a snapshot.
|
||||
let thumbnails: AttachmentThumbnailCache
|
||||
/// The whole-window file drop (05 ▸ Attachments: "the drop surface remains the **whole
|
||||
/// window**"). `nil` only where a caller has no store to import through.
|
||||
let fileDrop: CardWindowDropDelegate?
|
||||
/// Commits a checkbox flip: the marker's byte offset in the body, and the state the user saw.
|
||||
let onToggleTask: (Int, Bool) -> Void
|
||||
|
||||
@@ -74,6 +83,18 @@ struct CardWindowView: View {
|
||||
/// does survive is the buffer, which is the Edit session's, not the view's — and it was flushed
|
||||
/// to disk on the way in regardless.
|
||||
var body: some View {
|
||||
content
|
||||
// **The drop surface is the whole window** (05 ▸ Attachments), which is why it hangs
|
||||
// here — outside the raw-source swap, so a file dropped while the source outlet is open
|
||||
// still imports — rather than on the attachments section it fills. The body editor lets
|
||||
// file drags through to this by declining the file types
|
||||
// (`CardBodyTextView.acceptableDragTypes`); dragged *text* never matches `.fileURL` and
|
||||
// so is never offered here at all, which is the other half of the payload split.
|
||||
.modifier(WindowFileDrop(delegate: fileDrop))
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var content: some View {
|
||||
if rawSource.isActive {
|
||||
CardRawSourceView(session: rawSource, presentation: bodyPresentation)
|
||||
} else {
|
||||
@@ -183,9 +204,7 @@ struct CardWindowView: View {
|
||||
private var sidebar: some View {
|
||||
ScrollView(.vertical) {
|
||||
VStack(alignment: .leading, spacing: bodyPointSize * 1.25) {
|
||||
// m6-card-attachments: every top-level file of `attachments/`, compact rows with a
|
||||
// QuickLook thumbnail, keyboard-navigable.
|
||||
section("Attachments")
|
||||
CardAttachmentsSection(attachments: attachments, thumbnails: thumbnails)
|
||||
// m6-card-sidebar: the embedded style editor — the same component the board popover
|
||||
// and Style… already host (`StyleEditor`).
|
||||
section("Style")
|
||||
@@ -204,16 +223,33 @@ struct CardWindowView: View {
|
||||
}
|
||||
|
||||
/// A stacked small-caps header over the space its section will occupy (05: "Stacked sections
|
||||
/// under small-caps headers").
|
||||
/// under small-caps headers") — the same header the Attachments section fills in for real, so
|
||||
/// the four still-empty ones cannot drift from it.
|
||||
private func section(_ title: String) -> some View {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(title)
|
||||
.font(.caption.weight(.semibold))
|
||||
.textCase(.uppercase)
|
||||
.foregroundStyle(.secondary)
|
||||
Divider()
|
||||
CardSidebarSectionHeader(title: title)
|
||||
.accessibilityElement(children: .combine)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - The window-wide drop
|
||||
|
||||
/// Attaches the whole-window file drop, or nothing at all.
|
||||
///
|
||||
/// A modifier rather than an `if` inside `body` because `.onDrop` has to be applied to the *same*
|
||||
/// view identity in both cases: a window whose store arrives a turn after its view would otherwise
|
||||
/// re-mount its entire content when the drop target appeared, throwing away the body's scroll
|
||||
/// position for nothing.
|
||||
private struct WindowFileDrop: ViewModifier {
|
||||
|
||||
let delegate: CardWindowDropDelegate?
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
if let delegate {
|
||||
// `.fileURL` alone: a text drag never matches, so it is never offered here and falls to
|
||||
// the Edit editor, where `NSTextView` inserts it at the caret (05 ▸ Attachments).
|
||||
content.onDrop(of: [.fileURL], delegate: delegate)
|
||||
} else {
|
||||
content
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.accessibilityElement(children: .combine)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user