A refused trash hover withdraws the trash's own promise — the fall-through holds only keepable ones
Gap #12 ruled into 04 ▸ the trash-drop bullet: the refusal fall-through's honest reading (the column declines to be a target, it does not cancel the held drag) covers only promises the refusal leaves keepable — lane shadows keep drawing and an ⌥-copy lands where they are. A standing proposal naming the trash itself is the promise the refusal just broke, so it withdraws: the drag goes proposal-less over the column, release with no proposal cancels in agreement with the empty picture, and lifting ⌥ re-asks at the same address. Implementation filed on the Implementation board. Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
import SwiftUI
|
||||
|
||||
/// The board's **standard chrome** — the neutral surface stack under every lane and card,
|
||||
/// carried over from the pathfinder (03-board-ui.md § Styling: user colour is an edge accent;
|
||||
/// these are the fills underneath, and they never carry the user's colour).
|
||||
///
|
||||
/// The stack is three steps deep, and each step has its own home: the window keeps the standard
|
||||
/// background (`BoardView.boardBackground` paints over it only when the board *chooses* a colour),
|
||||
/// every lane lays a quiet quaternary wash over that (`Accommodations.lanePlateWash`), and every
|
||||
/// card sits on the opaque plate below — which is what makes a card pop against the washed lane in
|
||||
/// both appearances: white on grey in light, a step *darker* than the window in dark.
|
||||
enum BoardSurface {
|
||||
|
||||
/// A card's opaque plate — `controlBackgroundColor`, the pathfinder's card surface. Every face
|
||||
/// on the board fills with this one value (the resting face, the drag replica, the lane
|
||||
/// replica's miniatures, the placeholder editor and the arriving face), so a card is the same
|
||||
/// object wherever it is drawn. Opaque deliberately: a card's title never sees the board
|
||||
/// colour, which is why the ≥ 4.5:1 rule binds to the board level and not here
|
||||
/// (10-accessibility.md; `BoardView.boardBackground`).
|
||||
static var cardPlate: Color { Color(nsColor: .controlBackgroundColor) }
|
||||
}
|
||||
@@ -423,8 +423,9 @@ struct BoardView: View {
|
||||
/// hand-written hex, they reach the same place — has its text colour computed against the
|
||||
/// threshold by `BoardTextInk`, composited over the window background in the active appearance
|
||||
/// and recomputed on an appearance flip; the two subtrees that sit on this fill
|
||||
/// (`LaneView.header` and `TrashLaneView.header` — every other surface on the board carries its
|
||||
/// own opaque plate) take the answer as a `\.colorScheme` override.
|
||||
/// (`LaneView.header` and `TrashLaneView.header` — their plates are translucent washes the
|
||||
/// colour shows through, where every card carries its own opaque plate,
|
||||
/// `BoardSurface.cardPlate`) take the answer as a `\.colorScheme` override.
|
||||
///
|
||||
/// **One path, two verification stories** (`ContrastMath`): the twelve palette pairs are checked
|
||||
/// statically, by a test over the ink this seam chooses for each of them (03-board-ui.md §
|
||||
|
||||
@@ -249,7 +249,7 @@ struct CardFaceView: View, Equatable {
|
||||
// Constant, whether or not a stripe paints: every card's text sits on the same grid, so
|
||||
// colouring a card never shifts its title relative to its uncoloured neighbours.
|
||||
.padding(.leading, stripeWidth)
|
||||
.background(RoundedRectangle(cornerRadius: cornerRadius).fill(.background.secondary))
|
||||
.background(RoundedRectangle(cornerRadius: cornerRadius).fill(BoardSurface.cardPlate))
|
||||
.overlay(alignment: .leading) { accentStripe }
|
||||
// The selection treatment, which a hovering Finder file drag borrows outright: "the card
|
||||
// highlights while hovered" (04-interactions.md ▸ Drag and drop), and the accent stroke is
|
||||
@@ -510,7 +510,7 @@ struct CardFaceView: View, Equatable {
|
||||
width: BoardMetrics.cardReplicaWidth(measured: measuredWidth, bodyPointSize: pointSize),
|
||||
alignment: .leading
|
||||
)
|
||||
.background(RoundedRectangle(cornerRadius: cornerRadius).fill(.background.secondary))
|
||||
.background(RoundedRectangle(cornerRadius: cornerRadius).fill(BoardSurface.cardPlate))
|
||||
.overlay(alignment: .leading) { accentStripe }
|
||||
.dragReplicaShadow()
|
||||
}
|
||||
|
||||
@@ -82,6 +82,10 @@ struct LaneView: View, Equatable {
|
||||
/// what "increased" does to a stroke.
|
||||
@Environment(\.colorSchemeContrast) private var contrast
|
||||
|
||||
/// Reduce Transparency, for the lane plate's wash (`Accommodations.lanePlateWash` — translucent
|
||||
/// normally, the standard secondary background under the setting; the trash plate's rule).
|
||||
@Environment(\.accessibilityReduceTransparency) private var reduceTransparency
|
||||
|
||||
/// The window's appearance — the input to 10-accessibility.md's runtime-contrast rule, and the
|
||||
/// answer for every board whose background is not a hand-written hex (`headerInk`).
|
||||
///
|
||||
@@ -167,6 +171,10 @@ struct LaneView: View, Equatable {
|
||||
// lane is cut cards and all.
|
||||
.cutTreatment(of: lane.id, in: store)
|
||||
.onGeometryChange(for: CGFloat.self) { $0.size.height } action: { measuredHeight = $0 }
|
||||
// Behind the selection wash, not composed into it: the plate is the lane's resting
|
||||
// surface, the wash above it is the selection's own layer, and stacked `.background`s
|
||||
// put the later one further back.
|
||||
.background(lanePlate)
|
||||
// **This lane's drop target**, on the whole body. It accepts *every* session type and routes
|
||||
// internally — card sessions against this lane's masonry zones, lane sessions forwarded to
|
||||
// the strip's logic, external Finder file sessions against those same zones — because
|
||||
@@ -248,12 +256,13 @@ struct LaneView: View, Equatable {
|
||||
// since the ink they need is the same question and only the *verification* differs
|
||||
// (`ContrastMath`).
|
||||
//
|
||||
// A lane draws *no plate*: its background is the selection wash, which is `.clear` at
|
||||
// rest (`selectionBackground`), so the title, the icon, the count badge and the rename
|
||||
// field all land directly on the board's `background` — the surface the design binds the
|
||||
// threshold to ("text does sit on it", `BoardView.boardBackground`). The card faces below
|
||||
// are a different matter and deliberately untouched: they carry their own opaque plate
|
||||
// (`CardFaceView`'s `.background.secondary`), so their titles never see the board colour.
|
||||
// A lane's plate is a *translucent wash* (`lanePlate`), so the title, the icon, the
|
||||
// count badge and the rename field still land, effectively, on the board's `background`
|
||||
// — the surface the design binds the threshold to ("text does sit on it",
|
||||
// `BoardView.boardBackground`); the wash shifts it too little to change the answer. The
|
||||
// card faces below are a different matter and deliberately untouched: they carry their
|
||||
// own opaque plate (`BoardSurface.cardPlate`), so their titles never see the board
|
||||
// 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
|
||||
@@ -639,7 +648,7 @@ struct LaneView: View, Equatable {
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: BoardMetrics.cardCornerRadius(bodyPointSize: pointSize))
|
||||
.fill(.background.secondary)
|
||||
.fill(BoardSurface.cardPlate)
|
||||
)
|
||||
}
|
||||
Spacer(minLength: 0)
|
||||
@@ -663,6 +672,10 @@ struct LaneView: View, Equatable {
|
||||
|
||||
private var replicaHeight: CGFloat {
|
||||
max(measuredHeight, BoardMetrics.laneReplicaMinimumHeight(bodyPointSize: pointSize))
|
||||
// The lane's own wash over an opaque base — the replica floats over whatever the cursor is
|
||||
// above, so unlike the resting lane it cannot let the wash composite against the board; the
|
||||
// opaque `.background` behind it stands in for the window's neutral surface.
|
||||
.background(lanePlate)
|
||||
}
|
||||
|
||||
/// Where the title bar sits inside the replica — its **centre**, measured down from the replica's
|
||||
@@ -977,13 +990,23 @@ struct LaneView: View, Equatable {
|
||||
/// (10-accessibility.md: "Increase Contrast strengthens borders and the selection indicator";
|
||||
/// `Accommodations`, and `CardFaceView.plateStroke` for the same three-way branch on a card).
|
||||
///
|
||||
/// A lane is otherwise bounded by nothing but the gap between it and its neighbour, which is the
|
||||
/// distinction the setting most needs to restore here.
|
||||
/// A lane is otherwise bounded by its quiet wash and the gap between it and its neighbour,
|
||||
/// which is the distinction the setting most needs to restore here.
|
||||
private var selectionStroke: some View {
|
||||
RoundedRectangle(cornerRadius: cornerRadius)
|
||||
.strokeBorder(plateStroke, lineWidth: plateStrokeWidth)
|
||||
}
|
||||
|
||||
/// The lane's resting plate — the standard chrome's middle step (03-board-ui.md § Styling ▸
|
||||
/// Capabilities: window, washed lane, opaque card), the same quaternary wash the trash column
|
||||
/// wears (`Accommodations.lanePlateWash`). Translucent deliberately: a board-chosen colour
|
||||
/// shows through, so the header ink computed against the board's background (`headerInk`)
|
||||
/// keeps its premise on a painted board.
|
||||
private var lanePlate: some View {
|
||||
RoundedRectangle(cornerRadius: cornerRadius)
|
||||
.fill(Accommodations.lanePlateWash(reduceTransparency: reduceTransparency).style)
|
||||
}
|
||||
|
||||
private var plateStroke: AnyShapeStyle {
|
||||
if isSelected {
|
||||
AnyShapeStyle(Color.accentColor)
|
||||
@@ -1171,7 +1194,7 @@ private struct NewCardStubView: View {
|
||||
.padding(BoardMetrics.cardContentPadding(bodyPointSize: pointSize))
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: BoardMetrics.cardCornerRadius(bodyPointSize: pointSize))
|
||||
.fill(.background.secondary)
|
||||
.fill(BoardSurface.cardPlate)
|
||||
)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: BoardMetrics.cardCornerRadius(bodyPointSize: pointSize))
|
||||
@@ -1207,7 +1230,7 @@ private struct NewCardStubView: View {
|
||||
.padding(.leading, BoardMetrics.cardStripeWidth(bodyPointSize: pointSize))
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: BoardMetrics.cardCornerRadius(bodyPointSize: pointSize))
|
||||
.fill(.background.secondary)
|
||||
.fill(BoardSurface.cardPlate)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -105,7 +105,7 @@ struct TrashLaneRowView: View {
|
||||
.padding(BoardMetrics.cardContentPadding(bodyPointSize: pointSize))
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
// Quieter than a card's plate, which is what "dimmed" is here: the cards beside it keep the
|
||||
// ordinary secondary background, and this row sits a step further back.
|
||||
// ordinary card plate (`BoardSurface.cardPlate`), and this row sits a step further back.
|
||||
.background(RoundedRectangle(cornerRadius: cornerRadius).fill(.background.tertiary))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: cornerRadius)
|
||||
|
||||
Reference in New Issue
Block a user