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:
+107
-22
@@ -11,6 +11,10 @@ import SwiftUI
|
||||
/// what lets "one component, one behavior, three anchors" be a fact about the code rather than a
|
||||
/// promise. The Style… popover's *lifecycle* lives elsewhere for the same reason: it is a reload
|
||||
/// rule, and it belongs with the other reload rules (`StyleEditorSession`, `TransientBoardState`).
|
||||
///
|
||||
/// The one thing here that *names* an anchor is `StyleEditorLayout`, and it names only geometry: a
|
||||
/// popover is a window this app sizes and a sidebar section is a column the window sizes, so the two
|
||||
/// cannot share a frame. Nothing behavioral hangs off it — see its own doc comment.
|
||||
|
||||
// MARK: - The write funnel
|
||||
|
||||
@@ -89,6 +93,77 @@ enum CuratedSymbols {
|
||||
static var available: [String] { all.filter(ItemSymbol.exists) }
|
||||
}
|
||||
|
||||
// MARK: - The anchor's chrome
|
||||
|
||||
/// Everything about the editor that is the **anchor's** business rather than the editor's: how wide
|
||||
/// it is, what padding it brings, how many wells fall in a row, and whether its symbol grid scrolls.
|
||||
///
|
||||
/// **It exists so "one component, one behavior, another anchor" survives an anchor that is not a
|
||||
/// popover** (05-card-window.md ▸ Style: the card sidebar embeds this same editor). A popover is a
|
||||
/// window the app sizes; a sidebar section is a column the window sizes — and the 268-point frame
|
||||
/// that makes the first one narrow enough to sit beside a card would overflow the second by 70
|
||||
/// points. Nothing about *behavior* is in here: every well, every write, the batch display and the
|
||||
/// keyboard grammar are the editor's, identical at every anchor. Only the geometry moves.
|
||||
struct StyleEditorLayout: Equatable {
|
||||
|
||||
/// One well's side, and the gap between two — the numbers the grids are laid out on, named once
|
||||
/// so the fit rule below and the wells themselves cannot drift apart.
|
||||
static let wellSide: CGFloat = 20
|
||||
static let wellSpacing: CGFloat = 6
|
||||
|
||||
/// A fixed width, or `nil` to take whatever the anchor proposes.
|
||||
var width: CGFloat?
|
||||
/// The editor's own inset. Zero where the anchor already insets its column.
|
||||
var padding: CGFloat
|
||||
var backgroundColumns: Int
|
||||
var symbolColumns: Int
|
||||
/// How tall the symbol grid may grow before it scrolls inside itself, or `nil` for "never" —
|
||||
/// the grid then draws whole and the anchor scrolls it.
|
||||
var symbolGridMaximumHeight: CGFloat?
|
||||
|
||||
/// The Style… popover and the board popover's styling area: a fixed frame, its own padding, and
|
||||
/// a symbol grid that scrolls within it.
|
||||
///
|
||||
/// Thirteen background wells (None + the twelve) fall as 7 + 6, which keeps the popover narrow
|
||||
/// enough to sit beside a card without covering the lane it came from; the symbol grid's cap is
|
||||
/// eight rows or so — enough that it reads as a set rather than as a strip, short enough that the
|
||||
/// popover fits beside a card on a laptop screen.
|
||||
static let popover = StyleEditorLayout(
|
||||
width: 268,
|
||||
padding: 14,
|
||||
backgroundColumns: 7,
|
||||
symbolColumns: 8,
|
||||
symbolGridMaximumHeight: 168
|
||||
)
|
||||
|
||||
/// The card window's sidebar section (05-card-window.md ▸ Style).
|
||||
///
|
||||
/// - **No width and no padding of its own**: the sidebar's width is `CardWindowMetrics`' one
|
||||
/// decision and its gutter is already applied to the whole section stack, so an editor with an
|
||||
/// opinion here would either overflow the column or inset twice.
|
||||
/// - **As many wells per row as the column holds**, rather than the popover's 7 and 8 — the
|
||||
/// sidebar is narrower than the popover at every text size, and a grid wider than its column is
|
||||
/// a grid with wells the pointer cannot reach.
|
||||
/// - **The symbol grid does not scroll.** The sidebar is already a scroll view, and a scroll view
|
||||
/// inside a scroll view is a scroll view that fights (`CardWindowView`'s rule, for its reason).
|
||||
static func sidebar(contentWidth: CGFloat) -> StyleEditorLayout {
|
||||
let columns = columns(fitting: contentWidth)
|
||||
return StyleEditorLayout(
|
||||
width: nil,
|
||||
padding: 0,
|
||||
backgroundColumns: columns,
|
||||
symbolColumns: columns,
|
||||
symbolGridMaximumHeight: nil
|
||||
)
|
||||
}
|
||||
|
||||
/// How many wells fit across `width` — `n` wells and `n - 1` gaps, floored, and never less than
|
||||
/// one. Pure, and the whole of "the grid never overflows the column it was given".
|
||||
static func columns(fitting width: CGFloat) -> Int {
|
||||
max(1, Int((width + wellSpacing) / (wellSide + wellSpacing)))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - The editor
|
||||
|
||||
/// The style editor: a background section and a symbol section, each a leading "no value" well
|
||||
@@ -112,11 +187,9 @@ struct StyleEditorView: View {
|
||||
let store: BoardStore
|
||||
let recents: StyleRecents
|
||||
let target: StyleTarget
|
||||
|
||||
/// Wells per row. Thirteen background wells (None + the twelve) fall as 7 + 6, which keeps the
|
||||
/// popover narrow enough to sit beside a card without covering the lane it came from.
|
||||
private let backgroundColumns = 7
|
||||
private let symbolColumns = 8
|
||||
/// The anchor's geometry, and nothing else (`StyleEditorLayout`). Defaulted to the popover's, so
|
||||
/// the two anchors that were here first say nothing about it.
|
||||
var layout: StyleEditorLayout = .popover
|
||||
|
||||
var body: some View {
|
||||
let subjects = store.styleSubjects(of: target)
|
||||
@@ -129,8 +202,8 @@ struct StyleEditorView: View {
|
||||
Divider()
|
||||
symbolSection(icon)
|
||||
}
|
||||
.padding(14)
|
||||
.frame(width: 268)
|
||||
.padding(layout.padding)
|
||||
.frame(width: layout.width)
|
||||
// The read-only lock and the focused-editor rule disable every mutating surface, not only
|
||||
// the menu items (02-architecture.md § The lock's scope) — an editor whose wells would be
|
||||
// refused should not look available. The popover stays *open*: the lock is a condition the
|
||||
@@ -163,7 +236,7 @@ struct StyleEditorView: View {
|
||||
sectionHeader("Background", current: backgroundCurrent(state))
|
||||
StyleWellGrid(
|
||||
wells: backgroundWells(state),
|
||||
columns: backgroundColumns,
|
||||
columns: layout.backgroundColumns,
|
||||
apply: { change in
|
||||
StyleCommand.apply(background: change, to: target, in: store, recents: recents)
|
||||
}
|
||||
@@ -204,18 +277,27 @@ struct StyleEditorView: View {
|
||||
let fallback = ItemSymbol.default(for: level)
|
||||
return VStack(alignment: .leading, spacing: 8) {
|
||||
sectionHeader("Symbol", current: symbolCurrent(state, fallback: fallback))
|
||||
ScrollView(.vertical) {
|
||||
StyleWellGrid(
|
||||
wells: symbolWells(state, fallback: fallback),
|
||||
columns: symbolColumns,
|
||||
apply: { change in
|
||||
StyleCommand.apply(icon: change, to: target, in: store, recents: recents)
|
||||
}
|
||||
)
|
||||
symbolGrid(state, fallback: fallback)
|
||||
}
|
||||
}
|
||||
|
||||
/// The curated grid, scrolling within its own cap or drawn whole — the anchor's call
|
||||
/// (`StyleEditorLayout.symbolGridMaximumHeight`), and the one shape difference between the
|
||||
/// popover and the card sidebar.
|
||||
@ViewBuilder
|
||||
private func symbolGrid(_ state: StyleFieldState, fallback: String) -> some View {
|
||||
let grid = StyleWellGrid(
|
||||
wells: symbolWells(state, fallback: fallback),
|
||||
columns: layout.symbolColumns,
|
||||
apply: { change in
|
||||
StyleCommand.apply(icon: change, to: target, in: store, recents: recents)
|
||||
}
|
||||
// Eight rows or so before it scrolls: enough that the grid reads as a set rather than as
|
||||
// a strip, short enough that the popover fits beside a card on a laptop screen.
|
||||
.frame(maxHeight: 168)
|
||||
)
|
||||
if let maximumHeight = layout.symbolGridMaximumHeight {
|
||||
ScrollView(.vertical) { grid }
|
||||
.frame(maxHeight: maximumHeight)
|
||||
} else {
|
||||
grid
|
||||
}
|
||||
}
|
||||
|
||||
@@ -316,7 +398,7 @@ private struct StyleWellFace: View {
|
||||
}
|
||||
|
||||
let face: Face
|
||||
var size: CGFloat = 20
|
||||
var size: CGFloat = StyleEditorLayout.wellSide
|
||||
|
||||
var body: some View {
|
||||
switch face {
|
||||
@@ -380,8 +462,11 @@ private struct StyleWellGrid: View {
|
||||
|
||||
var body: some View {
|
||||
LazyVGrid(
|
||||
columns: Array(repeating: GridItem(.flexible(minimum: 20), spacing: 6), count: columns),
|
||||
spacing: 6
|
||||
columns: Array(
|
||||
repeating: GridItem(.flexible(minimum: StyleEditorLayout.wellSide), spacing: StyleEditorLayout.wellSpacing),
|
||||
count: columns
|
||||
),
|
||||
spacing: StyleEditorLayout.wellSpacing
|
||||
) {
|
||||
ForEach(wells) { well in
|
||||
Button {
|
||||
|
||||
Reference in New Issue
Block a user