Remove the lane header's "+" new-card control
The lane header no longer draws a new-card button. Every other creation path is untouched: File > New Card (Cmd-N), Return on a selected lane, and double-click on lane empty space. - LaneView: drop the button and its overlay slot, leaving the collapse chevron as the header's one piece of trailing chrome. - NewCardTarget: drop the button's click-names-target carve-out over the Cmd-N target rule (Return on a selected lane is now the only direct-target path left). - AccessibilityPhrases: drop the button's spoken label. - BoardMetrics: drop its width reserve; laneHeaderTrailingReserve now reduces to the collapse chevron's own reserve. - DESIGN/03, 04, 10, 11: update the lane title-bar inventory, the Cmd-N override clause and search-clearing mechanism list, the accessibility tree-shape sentence, and the pointer-grammar row that described the button. - Tests: drop the two pinned accessibility-label tests and the newCardButtonReserve assertions; retarget the truncation-headroom test at the now-solo laneHeaderTrailingReserve; fix stale doc-quote comments and the manual accessibility-verification checklist. Full KanbanTests suite: 2807 tests, 482 suites, all passing. Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
This commit is contained in:
@@ -8,7 +8,7 @@ import Foundation
|
||||
/// ### Why the strings live here and not at the modifiers
|
||||
///
|
||||
/// 10-accessibility.md states the board's tree as *sentences*: a lane container is
|
||||
/// "⟨title⟩, lane, N cards", the header button is "New card in ⟨lane⟩", a card's value carries its
|
||||
/// "⟨title⟩, lane, N cards", its collapse chevron is "Collapse ⟨lane⟩", a card's value carries its
|
||||
/// attachment count and, when it is cut-pending, "cut, pending paste". Those are rules about text —
|
||||
/// which placeholder an untitled item wears, how a count folds its plural, what order two value
|
||||
/// fragments join in — and a rule about text is only checkable if there is a function to ask. Split
|
||||
@@ -100,16 +100,9 @@ enum AccessibilityPhrases {
|
||||
return "\(displayTitle(title)), lane, \(state)\(cardCount(count))"
|
||||
}
|
||||
|
||||
/// The lane header's new-card button — "New card in ⟨lane⟩", the one labeled child
|
||||
/// 10-accessibility.md gives the header.
|
||||
static func newCardLabel(lane title: String?) -> String {
|
||||
"New card in \(displayTitle(title))"
|
||||
}
|
||||
|
||||
/// The lane header's **collapse chevron** — "Collapse ⟨lane⟩" (03-board-ui.md § Lane ▸ Collapsed
|
||||
/// lanes), the header's second labeled child and the new-card button's twin in every respect: a
|
||||
/// glyph-only control needs a word, and the word names the lane so the label stands on its own out
|
||||
/// of context.
|
||||
/// lanes), the header's one labeled child: a glyph-only control needs a word, and the word names
|
||||
/// the lane so the label stands on its own out of context.
|
||||
static func collapseLaneLabel(lane title: String?) -> String {
|
||||
"Collapse \(displayTitle(title))"
|
||||
}
|
||||
|
||||
@@ -115,35 +115,27 @@ enum BoardMetrics {
|
||||
em(0.4, bodyPointSize: bodyPointSize)
|
||||
}
|
||||
|
||||
/// The trailing room the header reserves for the new-card button, so **a long title truncates
|
||||
/// before it collides with the button** rather than running under it.
|
||||
/// The room the header reserves for the **collapse chevron**, its one piece of trailing chrome
|
||||
/// (03-board-ui.md § Lane ▸ Collapsed lanes). A new-card button once shared this edge and had its
|
||||
/// own matching reserve (`newCardButtonReserve`, retired with the button — this milestone); the
|
||||
/// chevron kept its own figure rather than folding into a since-widowed combined one.
|
||||
///
|
||||
/// This is the one figure that is not decoration: the button is an `Image` at
|
||||
/// `.imageScale(.small)`, which *is* a relative size, so a reserve fixed at 22pt would be
|
||||
/// overrun by the glyph itself at a large system text size and the truncation rule would stop
|
||||
/// being true. 1.7 em is the button plus its breathing room, measured in the same unit the glyph
|
||||
/// grows in.
|
||||
static func newCardButtonReserve(bodyPointSize: CGFloat) -> CGFloat {
|
||||
em(1.7, bodyPointSize: bodyPointSize)
|
||||
}
|
||||
|
||||
/// The room the header reserves for the **collapse chevron** beside the new-card button
|
||||
/// (03-board-ui.md § Lane ▸ Collapsed lanes) — `newCardButtonReserve`'s twin, and it exists for
|
||||
/// exactly that figure's reason: two glyphs now share the header's trailing edge, and a title has
|
||||
/// to truncate before it reaches either.
|
||||
///
|
||||
/// 1.4 em — 18pt at the standard 13pt body: the chevron is a smaller glyph than the plus and sits
|
||||
/// closer to it than to the title, so it asks for less breathing room than the button it precedes.
|
||||
/// This is the one figure that is not decoration: the chevron is an `Image` at
|
||||
/// `.imageScale(.small)`, which *is* a relative size, so a reserve fixed at 18pt would be overrun
|
||||
/// by the glyph itself at a large system text size and the truncation rule would stop being true.
|
||||
/// 1.4 em — 18pt at the standard 13pt body — is the glyph plus its breathing room, measured in the
|
||||
/// same unit it grows in.
|
||||
static func laneCollapseButtonReserve(bodyPointSize: CGFloat) -> CGFloat {
|
||||
em(1.4, bodyPointSize: bodyPointSize)
|
||||
}
|
||||
|
||||
/// Everything the lane header's trailing chrome takes — the chevron plus the new-card button. The
|
||||
/// one figure the header pads by, so "a long title truncates before it collides" stays one rule
|
||||
/// however many controls end up living there.
|
||||
/// Everything the lane header's trailing chrome takes — the collapse chevron alone now that the
|
||||
/// new-card button beside it is gone (this milestone narrowed this from a two-term sum to one).
|
||||
/// Kept as its own figure rather than inlined at the call site: the one thing the header pads by,
|
||||
/// so "a long title truncates before it collides" stays one rule however many controls end up
|
||||
/// living there again.
|
||||
static func laneHeaderTrailingReserve(bodyPointSize: CGFloat) -> CGFloat {
|
||||
newCardButtonReserve(bodyPointSize: bodyPointSize)
|
||||
+ laneCollapseButtonReserve(bodyPointSize: bodyPointSize)
|
||||
laneCollapseButtonReserve(bodyPointSize: bodyPointSize)
|
||||
}
|
||||
|
||||
/// The count badge's capsule inset — horizontal and vertical, which are deliberately different:
|
||||
|
||||
@@ -247,7 +247,7 @@ struct BoardView: View {
|
||||
// **The board is one tab stop** (10-accessibility.md ▸ Full Keyboard Access: "the board is
|
||||
// one tab stop with arrow-key navigation within"), and this is it: one focusable strip,
|
||||
// whose interior movement is the arrow grammar below rather than a focus stop per card.
|
||||
// The controls *around* the cards — each lane's new-card button, the popovers, the toolbar —
|
||||
// The controls *around* the cards — each lane's collapse chevron, the popovers, the toolbar —
|
||||
// are ordinary `Button`s and stay Tab-reachable in their own right, which is the other half
|
||||
// of the same sentence ("every control … is Tab-reachable").
|
||||
//
|
||||
|
||||
@@ -9,13 +9,15 @@ import SwiftUI
|
||||
///
|
||||
/// Leading SF Symbol from `icon` — lenient, an unknown name renders the `square.stack` default
|
||||
/// (`ItemSymbol`) — then the title or its quiet "Untitled" placeholder, a quiet secondary
|
||||
/// card-count badge, and a trailing quiet new-card button. **The whole bar is the drag surface**:
|
||||
/// card-count badge, and a trailing quiet collapse chevron. **The whole bar is the drag surface**:
|
||||
/// a click selects the lane — toggling off on a repeat, exactly as empty space does
|
||||
/// (04-interactions.md § Selection, settled) — and movement begins a **system drag session**
|
||||
/// carrying the lane (`DragSession`, DRAG-REORDER.md). The click-versus-drag split is the system's
|
||||
/// own now: `.onTapGesture` and `.onDrag` coexist, so a hesitant click can never start a drag and a
|
||||
/// drag can never also select. The one thing carved out of the drag region is the new-card button,
|
||||
/// which sits in an overlay outside it.
|
||||
/// drag can never also select. The one thing carved out of the drag region is the collapse chevron,
|
||||
/// which sits in an overlay outside it. **Card creation has no header control**: the "+" that once
|
||||
/// lived beside the chevron is gone (m-collapse's own follow-up), and every creation path — ⌘N,
|
||||
/// Return on a selected lane, double-click on lane empty space — is untouched by its absence.
|
||||
///
|
||||
/// ### The lane's one context menu
|
||||
///
|
||||
@@ -37,8 +39,9 @@ import SwiftUI
|
||||
/// invisibility works and why this needs so little code elsewhere: no card face means no marquee
|
||||
/// registration and no navigation frame, no grid registration means the card zones have nothing to
|
||||
/// resolve against (`BoardDropContext.retargetCards` answers end-of-lane instead), and no header means
|
||||
/// no rename target and no new-card button. The strip keeps exactly three gestures: a plain click
|
||||
/// expands, a modified click selects the lane, and a drag reorders it.
|
||||
/// no rename target and no collapse chevron — the strip's own plain click is its reversal instead. The
|
||||
/// strip keeps exactly three gestures: a plain click expands, a modified click selects the lane, and a
|
||||
/// drag reorders it.
|
||||
///
|
||||
/// ### The card face
|
||||
///
|
||||
@@ -223,7 +226,7 @@ struct LaneView: View, Equatable {
|
||||
.onDrop(of: boardDropTypes, delegate: LaneDropDelegate(context: drops, laneID: lane.id))
|
||||
// **The lane is an accessibility container** — "window → lanes (accessibility containers, in
|
||||
// lane `order`) → cards (leaf elements, in card `order`)" (10-accessibility.md ▸ The board
|
||||
// through VoiceOver). `.contain` rather than `.combine`: the header, the new-card button and
|
||||
// through VoiceOver). `.contain` rather than `.combine`: the header, the collapse chevron and
|
||||
// every card must stay individually reachable, which is the whole point of a container the
|
||||
// VoiceOver cursor enters (`TrashLaneView` states the same rule from the trash's side).
|
||||
.accessibilityElement(children: .contain)
|
||||
@@ -456,13 +459,11 @@ struct LaneView: View, Equatable {
|
||||
}
|
||||
.onDisappear { drops.registry.removeHeader(lane.id) }
|
||||
// **The header's trailing chrome, outside the drag region** — the overlay is attached
|
||||
// after `.onDrag` deliberately, which is what makes both controls clickable rather than
|
||||
// grabbable (the new-card button's long-standing arrangement, now shared).
|
||||
// after `.onDrag` deliberately, which is what makes the chevron clickable rather than
|
||||
// grabbable (the new-card button's old arrangement, now the chevron's alone — the button
|
||||
// itself is gone).
|
||||
.overlay(alignment: .trailing) {
|
||||
HStack(spacing: 0) {
|
||||
collapseButton
|
||||
newCardButton
|
||||
}
|
||||
collapseButton
|
||||
}
|
||||
// **10-accessibility.md's ≥ 4.5:1 rule, at the one place on the board where text sits on
|
||||
// a colour the user chose** (`BoardTextInk`) — palette name and hand-written hex alike,
|
||||
@@ -478,7 +479,7 @@ struct LaneView: View, Equatable {
|
||||
// colour.
|
||||
//
|
||||
// **Placed here, not at the end of the chain**, which is the modifier order doing real
|
||||
// work: everything above — including the new-card button in the overlay — is text on the
|
||||
// work: everything above — including the collapse chevron in the overlay — is text on the
|
||||
// board background and takes the computed ink, while the context menu and the Style…
|
||||
// popover attached below stay in the window's own appearance, because a menu is system
|
||||
// chrome drawn on its own surface and not on this board's colour.
|
||||
@@ -687,11 +688,11 @@ struct LaneView: View, Equatable {
|
||||
Spacer(minLength: 0)
|
||||
}
|
||||
// Reserves the trailing chrome's width so a long title truncates before it collides, and keeps
|
||||
// those controls out of the gestured region. **Font-derived** rather than a fixed figure: both
|
||||
// are `Image`s at a relative image scale, so a fixed reserve would be overrun by the glyphs
|
||||
// themselves at a large system text size and 03-board-ui.md's graceful-truncation rule would
|
||||
// quietly stop holding (`BoardMetrics.laneHeaderTrailingReserve`, which is the collapse
|
||||
// chevron's room plus the new-card button's).
|
||||
// the chevron out of the gestured region. **Font-derived** rather than a fixed figure: it is an
|
||||
// `Image` at a relative image scale, so a fixed reserve would be overrun by the glyph itself at
|
||||
// a large system text size and 03-board-ui.md's graceful-truncation rule would quietly stop
|
||||
// holding (`BoardMetrics.laneHeaderTrailingReserve`, the collapse chevron's own room now that
|
||||
// the new-card button that once shared it is gone).
|
||||
.padding(.trailing, BoardMetrics.laneHeaderTrailingReserve(bodyPointSize: pointSize))
|
||||
.padding(.horizontal, BoardMetrics.laneHeaderInset(bodyPointSize: pointSize))
|
||||
}
|
||||
@@ -757,13 +758,13 @@ struct LaneView: View, Equatable {
|
||||
}
|
||||
|
||||
/// **The collapse chevron** (03-board-ui.md § Lane ▸ Collapsed lanes) — the pointer's way into the
|
||||
/// fold, beside the new-card button at the header's trailing edge.
|
||||
/// fold, at the header's trailing edge, and its **only** trailing control since the new-card button
|
||||
/// that once sat beside it was removed (this milestone).
|
||||
///
|
||||
/// **Always visible, not hover-revealed**, because the header has no hover chrome to match: the
|
||||
/// new-card button beside it has been unconditional since it existed, and one control that appears
|
||||
/// on hover next to one that does not reads as a glitch rather than as a convention. The cost is
|
||||
/// paid in the header's trailing reserve, which is the figure that keeps the title truncating before
|
||||
/// either glyph (`headerContent`).
|
||||
/// **Always visible, not hover-revealed**, because the header has no hover chrome at all: an
|
||||
/// always-on glyph that only sometimes appeared would read as a glitch rather than as a convention.
|
||||
/// The cost is paid in the header's trailing reserve, which is the figure that keeps the title
|
||||
/// truncating before it (`headerContent`).
|
||||
///
|
||||
/// `chevron.left` because the fold travels that way — the lane's content collapses toward its
|
||||
/// leading edge and leaves a spine — and the strip's reversal is a click on the strip itself, which
|
||||
@@ -785,30 +786,6 @@ struct LaneView: View, Equatable {
|
||||
.disabled(!store.acceptsBoardMutations)
|
||||
}
|
||||
|
||||
/// The new-card button — a **pointer twin** of File ▸ New Card whose click *names its target*:
|
||||
/// "the lane header's new-card button overrides [the ⌘N target] rule — the click names its
|
||||
/// target lane, selection notwithstanding" (11-command-nexus.md ▸ Pointer grammar, settled), so
|
||||
/// it passes this lane and no anchor rather than consulting `NewCardTarget`.
|
||||
private var newCardButton: some View {
|
||||
Button {
|
||||
store.transient.beginPlaceholder(inLane: lane.id)
|
||||
} label: {
|
||||
Image(systemName: "plus")
|
||||
.imageScale(.small)
|
||||
.foregroundStyle(.secondary)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
// "The lane header's new-card button is a labeled child ('New card in ⟨lane⟩')"
|
||||
// (10-accessibility.md ▸ The board through VoiceOver) — the header's one child element, which
|
||||
// is why it lives in an overlay outside the flattened bar rather than inside it.
|
||||
.accessibilityLabel(AccessibilityPhrases.newCardLabel(lane: lane.title.value))
|
||||
// Mutating, so the read-only lock disables it like every other write path
|
||||
// (02-architecture.md § The lock's scope), and the focused-editor rule closes it while an
|
||||
// inline editor is open (04 ▸ Grammar) — the pointer twin of a disabled menu item.
|
||||
.disabled(store.isReadOnly || store.isEditingInline)
|
||||
}
|
||||
|
||||
// MARK: - The lane drag
|
||||
|
||||
/// Begins the lane's system drag session (DRAG-REORDER.md; 04-interactions.md ▸ Drag and drop).
|
||||
|
||||
@@ -20,11 +20,11 @@
|
||||
///
|
||||
/// ### What it deliberately does not decide
|
||||
///
|
||||
/// - **The lane header's new-card button overrides this rule entirely** (11-command-nexus.md ▸
|
||||
/// Pointer grammar, settled): "the click names its target lane, selection notwithstanding". That
|
||||
/// call site passes its own lane and never comes here.
|
||||
/// - **Return on a selected lane** is the same target as this rule's lane branch, but it is reached
|
||||
/// by grammar rather than by the menu; it also passes its lane directly.
|
||||
/// by grammar rather than by the menu; it passes its lane directly rather than coming here. (The
|
||||
/// lane header once carried a new-card button with the same carve-out — click names its target,
|
||||
/// selection notwithstanding — but the button itself is gone; Return is the one direct-target path
|
||||
/// left.)
|
||||
enum NewCardTarget {
|
||||
|
||||
/// Where a new card goes: which lane, and which card it lands immediately after (`nil` = the
|
||||
|
||||
@@ -199,7 +199,7 @@ extension View {
|
||||
/// change. Chrome outside the strip keeps using `.font(_:)` and stays at system size on purpose.
|
||||
///
|
||||
/// SF Symbols need no equivalent — `Image(systemName:)` under `.imageScale(_:)` sizes off the
|
||||
/// current font, which is why `BoardMetrics.newCardButtonReserve` is 1.7 em rather than 22pt.
|
||||
/// current font, which is why `BoardMetrics.laneCollapseButtonReserve` is 1.4 em rather than 18pt.
|
||||
func boardFont(_ style: Font.TextStyle, weight: Font.Weight? = nil) -> some View {
|
||||
modifier(BoardFontModifier(style: style, weight: weight))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user