Build the style, details, and actions sidebar sections

The sidebar completes: the shared style editor gains a second anchor —
StyleEditorLayout carries the geometry (the popover keeps its settled
268/14/7/8 untouched as the default; the sidebar packs columns to its
width with no inner scroller) while every well, the batch display, the
arrow grammar, and the one applyStyle bracket stay the shared
component's. The card anchor is fixed, not tracking: the target is
this card, and the fate walk retires the window when the card goes.
Details renders every unknown frontmatter key read-only in file order —
Card.document already carried them — showing the author's own bytes
where the raw span is a value and the engine's rendering for block
scalars and empties; reserved enhanced-schema keys are ordinary
unknowns, and no keys means no section. Actions: Delete rides the same
tombstone bytes as Backspace and drop-on-trash through a one-line
seam, says nothing about selection, and lets the fate walk dismiss;
Reveal in Finder resolves through the attachment scope so the two
paths cannot disagree. History reserves its m7 slot without drawing a
header no base board can honor.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
2026-07-28 12:22:04 -04:00
parent 46397c740e
commit 40322247e0
10 changed files with 936 additions and 49 deletions
+49 -24
View File
@@ -15,7 +15,8 @@ import UniformTypeIdentifiers
/// reads or writes beyond that is later work and is marked where it lands:
///
/// - the title as an editable field (commit on Return / focus loss, Escape abandons),
/// - the sidebar's five sections, which are section *headers* here and nothing more.
/// - the sidebar's History section, whose place in the stack is reserved and whose content waits on
/// a git mode to be honest about (`historySlot`).
///
/// The placeholders are structural rather than apologetic: the sidebar's inventory and its order are
/// settled (05 The attributes sidebar), so the shell states them and the sections fill in
@@ -39,6 +40,20 @@ import UniformTypeIdentifiers
struct CardWindowView: View {
let card: Card
/// The board this card belongs to.
///
/// The one place in this window a whole store is handed to a view rather than a narrow seam, and
/// the sidebar is why: the Style section hosts the **shared** style editor, whose API is
/// store-shaped by design (it reads the target set's current values and writes through the one
/// `applyStyle` bracket every anchor shares), and the Actions section's Delete is the store's own
/// tombstone. Routing either through a closure of this window's own would be a second card-styling
/// or card-deleting path to keep in step with the first exactly what "one component, one
/// behavior" and "exactly the tombstone" forbid.
let store: BoardStore
/// The app-wide quick-style recents the embedded editor feeds (03-board-ui.md Styling
/// Controls) app state, not board state, which is why it arrives beside the store rather than
/// on it.
let recents: StyleRecents
/// The card's folder on disk what relative images and links in the body resolve against
/// (05 Preview). `nil` only where a caller has no board root to build it from.
let cardFolder: URL?
@@ -194,40 +209,50 @@ struct CardWindowView: View {
// MARK: - Attributes sidebar
/// The sidebar's sections, **in 05's settled order**, as headers over empty space.
/// The sidebar's sections, **in 05's settled order**: Attachments, Style, Details, History,
/// Actions.
///
/// Two of them are conditional once they have content Details appears only when the card
/// carries unknown frontmatter keys, and History is absent on boards without app-managed git
/// and the shell shows them unconditionally because it has neither the key inventory nor a git
/// mode to consult yet. That is the one place these placeholders are not yet the final
/// composition, and it resolves when the sections do.
/// Two of the five are conditional, and both conditions are the section's own rather than a rule
/// restated here: **Details** renders nothing when the card carries no unknown frontmatter keys
/// ("shown only when any exist"), and **History** is absent on boards without app-managed git.
/// Everything else in the stack is unconditional, so the composition a user learns on one card is
/// the composition they get on the next.
private var sidebar: some View {
ScrollView(.vertical) {
VStack(alignment: .leading, spacing: bodyPointSize * 1.25) {
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")
// m6-card-sidebar: read-only key/value rows for every unknown frontmatter key, in
// file order.
section("Details")
// m7-git: the card's commit trail, read-only; absent on mode none / repo-nested.
section("History")
// m6-card-sidebar: Delete (tombstones, the window then dismisses itself) and Reveal
// in Finder.
section("Actions")
CardStyleSection(store: store, recents: recents, cardID: card.id)
// The snapshot's own document, not a re-read: the loader parsed this file, unknown
// keys and their order included, and `Card` has carried it since (`BoardModel`).
CardDetailsSection(rows: CardDetails.rows(of: card.document))
historySlot
CardActionsSection(store: store, cardID: card.id, cardFolder: cardFolder)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(CardWindowMetrics.gutter(bodyPointSize: bodyPointSize))
}
}
/// A stacked small-caps header over the space its section will occupy (05: "Stacked sections
/// 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 {
CardSidebarSectionHeader(title: title)
.accessibilityElement(children: .combine)
/// **The History section's reserved place in the stack** between Details and Actions, 05's
/// order (05 History: "the card's commit trail, read-only newest first semantic subject,
/// relative date, author").
///
/// Nothing is drawn yet, deliberately: the section is conditional on a git mode that does not
/// exist here, so a header over empty space would claim a commit trail on every board and on
/// the boards where it is *absent* by design (mode none, repo-nested) it would be claiming one
/// that can never arrive. What the slot reserves is the **position**, so filling it in moves
/// nothing above or below it.
///
// m7-git: the trail itself, plus the two rules that come with it absence on boards without
// app-managed git (the same honesty rule as the board popover's git section, 06-history-undo.md)
// and View History, which focuses this section (11-command-nexus.md).
@ViewBuilder
private var historySlot: some View {
EmptyView()
}
}