Card windows drop the title from the title bar — the body already says it

The card's name renders as part of the card body, so the chrome said it
twice. HostedWindowController gains a titleVisibility slot on the same
retained-slot pattern as the accessory and toolbar (reapplied at attach,
so the macOS 26 provisional-window swap can't lose it); CardWindowHost
opts in beside installToolbar, board windows keep AppKit's default.
window.title itself is untouched — navigationTitle still feeds the
Window menu, Exposé, VoiceOver and restoration.

Known consequence, probed empirically: under .hidden AppKit collapses
title and subtitle into one hidden field, so the "board › lane"
navigationSubtitle disappears with it — and the details sidebar carries
no placement rows, so a card window currently shows its home nowhere.
Filed on the Redesign board to bless or relocate; 05-card-window.md's
subtitle sentence needs reconciling either way.

Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97
This commit is contained in:
2026-08-01 08:10:31 -04:00
parent 8206a78ecc
commit 6f2e0d15da
3 changed files with 124 additions and 0 deletions
+37
View File
@@ -91,6 +91,22 @@ final class HostedWindowController: NSObject, NSWindowDelegate {
/// their board has loaded.
private var toolbarController: WindowToolbarController?
/// Whether this window's title is hidden from the title bar **card windows only**
/// (05-card-window.md Window: the card's name is shown as part of the card's body, not the
/// chrome). `nil` leaves AppKit's own default (`.visible`) untouched, which is what every board
/// window keeps without a call of its own the same "nothing to do" posture `titlebarAccessory`
/// has on welcome, the bootstrap and card windows, mirrored here for the one window kind that
/// *does* have an opinion.
///
/// A slot, not a one-shot write, for the accessory and toolbar's own reason: the value has to
/// survive the provisional-window swap (`detach()`'s doc comment) and reapply itself when the
/// real window attaches, which a write made once at `onAttach` time would not survive if that
/// closure only fired for the provisional window. `NSWindow.title` itself is a different slot
/// entirely SwiftUI's `navigationTitle` sets it directly, and it is left alone on purpose: the
/// Window menu, Mission Control/Exposé, VoiceOver and state restoration all read the string, not
/// what the chrome draws from it.
private var titleVisibility: NSWindow.TitleVisibility?
private static let logger = Logger(subsystem: "dev.rzen.indie.Kanban", category: "window")
// MARK: Attachment
@@ -110,6 +126,7 @@ final class HostedWindowController: NSObject, NSWindowDelegate {
// window existed is installed here instead, and one handed over later installs immediately.
addTitlebarAccessoryIfPossible()
applyToolbarIfPossible()
applyTitleVisibilityIfPossible()
}
/// Puts the previous delegate back and takes the titlebar accessory and toolbar off the window
@@ -192,6 +209,26 @@ final class HostedWindowController: NSObject, NSWindowDelegate {
window.toolbar = nil
}
// MARK: Title visibility
/// Hides this window's title from the title bar, leaving the toolbar exactly as it renders today
/// the card-window seam (`CardWindowHost`, 05-card-window.md Window). `window.title` is
/// untouched by this call on purpose; see the property's doc comment for why.
///
/// Safe to call whenever the caller learns it wants this before the window exists (the value is
/// held and applied at `attach`) or after (applied immediately) and safe to call more than once,
/// unlike the accessory and toolbar slots: writing `NSWindow.titleVisibility` twice has no side
/// effect worth guarding against, so this is not an install-once seam.
func hideTitle() {
titleVisibility = .hidden
applyTitleVisibilityIfPossible()
}
private func applyTitleVisibilityIfPossible() {
guard let window, let titleVisibility else { return }
window.titleVisibility = titleVisibility
}
/// Closes the window for real, after the flush has run. `performClose` rather than `close` so the
/// standard path runs SwiftUI's own delegate gets its callbacks, tabbing behaves with the
/// flag telling our own `windowShouldClose` to stand aside.