The window-title widget arrives as a leading titlebar accessory — a quiet chevron on board windows only, installed and removed by the window controller's own attach lifecycle — anchoring the one board-level surface as a transient popover (the board window deliberately grows no toolbar item for it). Inside: board rename editing frontmatter title only (the folder is never renamed; an empty commit removes the key and the window title falls back to the folder name), the embedded shared style editor permanently targeting the board, and the labeled Git section that this milestone only reserves — a mode-none explanation and a disabled stub where m7's add-git, branch, remote, and authentication controls land. Cmd-I (File > Board Info) toggles it per window through a focused scene value, kept apart from board-scoped transient state since a titlebar popover belongs to one window, not to the board. Escape reverts a dirty rename field and falls through to dismiss otherwise; a foreign rename resyncs the field only while unfocused. 12 new tests. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
151 lines
6.2 KiB
Swift
151 lines
6.2 KiB
Swift
import SwiftUI
|
|
|
|
/// The scene graph (02-architecture.md § Windows, § Launch and window lifecycle).
|
|
///
|
|
/// ### Four scenes, and why each is the kind it is
|
|
///
|
|
/// - **Welcome** is a `Window`: there is one of it, ever, and `openWindow(id:)` focuses the existing
|
|
/// one rather than making a second.
|
|
/// - **The restore bootstrap** is a `Window` too, and a deliberate oddity — see
|
|
/// `RestoreBootstrapView` for why launch-time work has to wear a window at all.
|
|
/// - **Boards** and **cards** are `WindowGroup(for:)`s, because their identity is a *value*: opening
|
|
/// with a ref that already has a window focuses it, which is how "one board window per root" and
|
|
/// "at most one card window per card (reopen focuses)" are enforced by the scene rather than by
|
|
/// bookkeeping.
|
|
///
|
|
/// ### Restoration is the registry's, not the system's
|
|
///
|
|
/// Both groups declare `.restorationBehavior(.disabled)`. The app already knows which boards were
|
|
/// open — the registry's open-now flags, which survive a crash and reopen in `lastOpened` order —
|
|
/// and letting AppKit *also* restore windows would produce duplicates, and worse, card windows
|
|
/// restored behind boards that never opened. One mechanism, and it is the one that can explain
|
|
/// itself when a board has moved or gone.
|
|
///
|
|
/// ### Which window appears at launch
|
|
///
|
|
/// Exactly one of welcome and the bootstrap, decided once in `init` and never re-derived: the
|
|
/// preference is read before any scene exists, and `restorables()` costs a bookmark resolution per
|
|
/// known board — a computed property here would pay that on every scene-graph evaluation.
|
|
@main
|
|
struct KanbanApp: App {
|
|
|
|
@NSApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
|
|
|
|
@State private var appModel: AppModel
|
|
|
|
/// Whether this launch restores boards: the preference is on **and** there is something flagged
|
|
/// to restore. Welcome "appears only when nothing restores".
|
|
private let shouldRestoreAtLaunch: Bool
|
|
|
|
init() {
|
|
let model = AppModel()
|
|
_appModel = State(initialValue: model)
|
|
shouldRestoreAtLaunch = AppPreferences.restoreOpenBoardsAtLaunch
|
|
&& !model.boardRegistry.restorables().isEmpty
|
|
// The delegate is constructed by the adaptor before this runs, so this is the one place the
|
|
// app's model and its AppKit half meet.
|
|
appDelegate.appModel = model
|
|
}
|
|
|
|
var body: some Scene {
|
|
Window("Welcome to Lanework", id: WindowID.welcome) {
|
|
WelcomeView()
|
|
.environment(appModel)
|
|
.captureWindowActions(into: appModel)
|
|
}
|
|
.defaultLaunchBehavior(shouldRestoreAtLaunch ? .suppressed : .automatic)
|
|
.restorationBehavior(.disabled)
|
|
.windowResizability(.contentSize)
|
|
// Its automatic Window-menu item is replaced by the explicit command below, so the title is
|
|
// the one 11-command-nexus.md names rather than whatever the scene happens to be called.
|
|
.commandsRemoved()
|
|
|
|
Window("", id: WindowID.restoreBootstrap) {
|
|
RestoreBootstrapView()
|
|
.environment(appModel)
|
|
.captureWindowActions(into: appModel)
|
|
}
|
|
.defaultLaunchBehavior(shouldRestoreAtLaunch ? .presented : .suppressed)
|
|
.restorationBehavior(.disabled)
|
|
.windowStyle(.plain)
|
|
.defaultSize(width: 1, height: 1)
|
|
.commandsRemoved()
|
|
|
|
WindowGroup(id: WindowID.board, for: BoardWindowRef.self) { $ref in
|
|
if let ref {
|
|
BoardWindowHost(ref: ref)
|
|
.environment(appModel)
|
|
.captureWindowActions(into: appModel)
|
|
}
|
|
}
|
|
.restorationBehavior(.disabled)
|
|
.defaultLaunchBehavior(.suppressed)
|
|
.commands { menuCommands }
|
|
|
|
WindowGroup(id: WindowID.card, for: CardWindowRef.self) { $ref in
|
|
if let ref {
|
|
CardWindowHost(ref: ref)
|
|
.environment(appModel)
|
|
.captureWindowActions(into: appModel)
|
|
}
|
|
}
|
|
.restorationBehavior(.disabled)
|
|
.defaultLaunchBehavior(.suppressed)
|
|
|
|
Settings {
|
|
SettingsView()
|
|
.environment(appModel)
|
|
.captureWindowActions(into: appModel)
|
|
}
|
|
}
|
|
|
|
/// The menu items the app has built so far.
|
|
///
|
|
/// **The titles are API** (04-interactions.md ▸ Configurable bindings): macOS's App Shortcuts
|
|
/// mechanism remaps menu items *by title*, so these strings are the keys a user's custom binding
|
|
/// is stored under. They are spelled exactly as 11-command-nexus.md inventories them, and
|
|
/// changing one silently breaks every remap of it.
|
|
@CommandsBuilder
|
|
private var menuCommands: some Commands {
|
|
// The File group, in 11-command-nexus.md's own row order: New Card, New Lane, (New Board…,
|
|
// still owed), Open…, (Open Recent, still owed), Board Info. The creation pair and Board
|
|
// Info all validate against the frontmost board through the focus system, so each is simply
|
|
// absent-of-effect when no board is in front.
|
|
CommandGroup(after: .newItem) {
|
|
BoardCreationCommands()
|
|
|
|
Divider()
|
|
|
|
Button("Open…") {
|
|
appModel.presentOpenPanel()
|
|
}
|
|
.keyboardShortcut("o", modifiers: .command)
|
|
|
|
Divider()
|
|
|
|
BoardInfoCommand()
|
|
}
|
|
|
|
// The Board menu (11-command-nexus.md), in its inventoried order — Rename, then Style…,
|
|
// then the width pair, with Open Card and the Move items still owed. Its items act on the
|
|
// frontmost board window, which they reach through the focus system rather than through the
|
|
// app model — see `BoardCommands.swift`, which also owns their validation.
|
|
CommandMenu("Board") {
|
|
BoardRenameCommand()
|
|
BoardStyleCommand()
|
|
|
|
Divider()
|
|
|
|
LaneWidthCommands()
|
|
}
|
|
|
|
CommandGroup(after: .windowList) {
|
|
// No default chord — "— (no default)" in the Nexus is deliberate, not a gap; it remaps
|
|
// like any other item.
|
|
Button("Welcome to Lanework") {
|
|
appModel.showWelcome()
|
|
}
|
|
}
|
|
}
|
|
}
|