The comments column gets a handle — the beside-mount divider is now user-draggable
Kanban.xcodeproj must be regenerated (xcodegen generate) before building. CardWindowMetrics.commentsColumnWidth was a fixed figure between the body column and the comments pane in the side-by-side layout; it is now only the resting default. A new CommentsColumnDivider replaces the plain Divider() between the two panes with a real HStack element carrying a resize-left-right cursor and a drag gesture, clamped through a new pure seam, CardWindowMetrics.clampedCommentsColumnWidth — never narrower than the existing commentsMinimumWidth floor, never wide enough to push the body under its own bodyMinimumWidth. The drag tracks live in memory (commentsColumnWidthOverride) and writes AppPreferences.commentsColumnWidthKey exactly once, on release, mirroring LaneResizeSession's live-track/write-once split rather than hammering UserDefaults per tick. Persistence is app-wide via @AppStorage, matching showComments and commentsBesideBody — the pane's other two layout bits — rather than the per-card BoardRegistry.cardWindowFrames: this is "how the pane is arranged," the same kind of fact those two already are, not a per-card window geometry. Flagged on the card thread as a call worth owner review. Six new unit tests cover the clamp's two floors, the degenerate case where a container is too narrow for both, and its agreement with CardWindowMetrics.minimumSize at the window's own floor. Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
This commit is contained in:
@@ -18,7 +18,10 @@ import UniformTypeIdentifiers
|
||||
/// arithmetic behind the frame is `CommentsMount`, which is pure and therefore checkable.
|
||||
///
|
||||
/// The sidebar is unchanged by any of it — it is a third pane, it has always been fixed-width, and
|
||||
/// the resize flex still goes to the body and never to the two fixed panes.
|
||||
/// the resize flex still goes to the body and never to the sidebar. **The comments column is no
|
||||
/// longer in that second category** (ruled 2026-08-09): its divider is user-draggable in the beside
|
||||
/// mount, so the flex the body and the comments pane split is now the user's to move, within the two
|
||||
/// floors `CardWindowMetrics.clampedCommentsColumnWidth` keeps either side from crossing.
|
||||
///
|
||||
/// ### What this milestone builds, and what it deliberately does not
|
||||
///
|
||||
@@ -37,8 +40,12 @@ import UniformTypeIdentifiers
|
||||
/// ### The width rule, in one line
|
||||
///
|
||||
/// The sidebar has a fixed width from `CardWindowMetrics`; the body column takes `.infinity`. That
|
||||
/// is the whole of "the window's resize flex goes to the body" — no split view, no stored divider
|
||||
/// position, nothing for a drag to disagree with.
|
||||
/// is the whole of "the window's resize flex goes to the body, never the sidebar" — no split view
|
||||
/// between body and sidebar, no stored divider position there, nothing for a drag to disagree with.
|
||||
///
|
||||
/// **The comments column keeps its own, separate width rule** (added 2026-08-09,
|
||||
/// `CommentsColumnDivider`): a real divider, a drag, and a stored width — the exception this
|
||||
/// sentence used to have none of, and still does not have between the body and the *sidebar*.
|
||||
///
|
||||
/// ### Why the title no longer scrolls with the body
|
||||
///
|
||||
@@ -110,6 +117,21 @@ struct CardWindowView: View {
|
||||
@AppStorage(AppPreferences.showCommentsKey) private var showComments = true
|
||||
@AppStorage(AppPreferences.commentsBesideBodyKey) private var commentsBesideBody = true
|
||||
|
||||
/// **The comments column's user-set width**, in points — the divider's memory (05-card-window.md
|
||||
/// ▸ The comments column, extended 2026-08-09). `@AppStorage` for the same reason its two
|
||||
/// neighbours above are: every open card window's divider answers to one figure. `0` is the
|
||||
/// "never dragged" reading (`AppPreferences.commentsColumnWidthKey`'s doc) — a real drag can
|
||||
/// never land there, since `CardWindowMetrics.clampedCommentsColumnWidth`'s floor is always
|
||||
/// positive.
|
||||
@AppStorage(AppPreferences.commentsColumnWidthKey) private var storedCommentsColumnWidth: Double = 0
|
||||
|
||||
/// **The divider's in-flight width**, live only while a drag is running — `nil` the rest of the
|
||||
/// time. `LaneResizeSession.liveWidth`'s same split applied to a much smaller session: the drag
|
||||
/// tracks the cursor in memory on every tick (`CommentsColumnDivider.onChange`), and only the
|
||||
/// *release* writes `storedCommentsColumnWidth` (`.onCommit`), so a flick that fires dozens of
|
||||
/// events writes `UserDefaults` — and every sibling card window's divider — exactly once.
|
||||
@State private var commentsColumnWidthOverride: CGFloat?
|
||||
|
||||
/// The body font's point size, read once per body evaluation: every measurement in this view —
|
||||
/// the sidebar's width, both gutters, the vertical rhythm — is derived from it, so they scale
|
||||
/// together when the system text size changes.
|
||||
@@ -118,6 +140,32 @@ struct CardWindowView: View {
|
||||
/// Where the comments pane sits, when it is shown at all.
|
||||
private var mount: CommentsMount { CommentsMount(besideBody: commentsBesideBody) }
|
||||
|
||||
/// The comments column's width before this render's clamp: the live drag's own width while one is
|
||||
/// running, the persisted figure once the divider has ever been dragged and released, and
|
||||
/// `CardWindowMetrics.commentsColumnWidth`'s font-derived default before either has ever happened.
|
||||
/// Unclamped on purpose: clamping needs the beside `HStack`'s live container width, which only the
|
||||
/// `.beside` render branch has (`clampedCommentsColumnWidth(in:)`), so this stays the one place
|
||||
/// that decides *which number* to clamp rather than also deciding *how far*.
|
||||
private var commentsColumnWidth: CGFloat {
|
||||
if let commentsColumnWidthOverride { return commentsColumnWidthOverride }
|
||||
return storedCommentsColumnWidth > 0
|
||||
? CGFloat(storedCommentsColumnWidth)
|
||||
: CardWindowMetrics.commentsColumnWidth(bodyPointSize: bodyPointSize)
|
||||
}
|
||||
|
||||
/// `commentsColumnWidth` clamped against `containerWidth` — the beside `HStack`'s own width this
|
||||
/// render (`CardWindowMetrics.clampedCommentsColumnWidth`). Re-derived every render rather than
|
||||
/// written back into `storedCommentsColumnWidth`: a window too narrow for the persisted figure
|
||||
/// (a smaller display, say) shows the clamped width without silently shrinking the preference a
|
||||
/// wider window will want back.
|
||||
private func clampedCommentsColumnWidth(in containerWidth: CGFloat) -> CGFloat {
|
||||
CardWindowMetrics.clampedCommentsColumnWidth(
|
||||
commentsColumnWidth,
|
||||
bodyPointSize: bodyPointSize,
|
||||
containerWidth: containerWidth
|
||||
)
|
||||
}
|
||||
|
||||
/// The two columns — **or the raw-source editor in place of both of them**.
|
||||
///
|
||||
/// A swap rather than an overlay, which is 05 ▸ Raw source outlet's own word for it ("swaps the
|
||||
@@ -178,17 +226,37 @@ struct CardWindowView: View {
|
||||
if showComments {
|
||||
switch mount {
|
||||
case .beside:
|
||||
HStack(spacing: 0) {
|
||||
bodyColumn
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
||||
// The `GeometryReader` is new here for the divider's sake — a resizable column needs
|
||||
// the `HStack`'s own live width to clamp against (`clampedCommentsColumnWidth(in:)`),
|
||||
// exactly the reason the stacked branch below already reaches for one.
|
||||
GeometryReader { proxy in
|
||||
HStack(spacing: 0) {
|
||||
bodyColumn
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
||||
|
||||
Divider()
|
||||
CommentsColumnDivider(
|
||||
bodyPointSize: bodyPointSize,
|
||||
containerWidth: proxy.size.width,
|
||||
currentWidth: clampedCommentsColumnWidth(in: proxy.size.width),
|
||||
onChange: { commentsColumnWidthOverride = $0 },
|
||||
onCommit: { width in
|
||||
storedCommentsColumnWidth = Double(width)
|
||||
// The persisted figure now says the same thing the override does —
|
||||
// clearing it hands `commentsColumnWidth` back to one source of truth
|
||||
// rather than two that happen to agree.
|
||||
commentsColumnWidthOverride = nil
|
||||
}
|
||||
)
|
||||
|
||||
commentsPane
|
||||
// Fixed, like the sidebar: "resize flex always goes to the body, never the
|
||||
// fixed panes" (05 ▸ Composition).
|
||||
.frame(width: CardWindowMetrics.commentsColumnWidth(bodyPointSize: bodyPointSize))
|
||||
.frame(maxHeight: .infinity, alignment: .top)
|
||||
commentsPane
|
||||
// **User-resizable since 2026-08-09** — the sidebar's "resize flex
|
||||
// always goes to the body" (05 ▸ Composition) still holds between the
|
||||
// body and the *sidebar*; the body and the *comments column* now split
|
||||
// it by the divider's drag instead, clamped so neither pane's own floor
|
||||
// gives way (`CardWindowMetrics.clampedCommentsColumnWidth`).
|
||||
.frame(width: clampedCommentsColumnWidth(in: proxy.size.width))
|
||||
.frame(maxHeight: .infinity, alignment: .top)
|
||||
}
|
||||
}
|
||||
|
||||
case .stacked:
|
||||
|
||||
Reference in New Issue
Block a user