Four scenes (welcome, restore bootstrap, board group, card group) with system restoration disabled in favor of the registry's open-now flags: set when a window actually opens, cleared only on user close, so quit — and crash — leave exactly the restoration set behind. AppModel joins windows to sessions (shared store, registry record, card refs, held security scope); CloseFlushCoordinator pins 02's strict close order as a seam-injected machine (card sessions end, windows drain, store flushes, record stamps, teardown) with named slots where m6/m7 flushes land. HostedWindowController proxies — never replaces — SwiftUI's window delegate to intercept windowShouldClose for the flush, report frames, and place saved frames onto live screens. Card windows are (board path, case-folded card id) values: reopen focuses, and a snapshot-pure fate function dismisses on delete, tombstone, tombstoned lane, or cross-board move. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
143 lines
5.2 KiB
Swift
143 lines
5.2 KiB
Swift
import AppKit
|
|
import SwiftUI
|
|
|
|
/// The welcome window (02-architecture.md § Windows).
|
|
///
|
|
/// ### What this is, and what it is not yet
|
|
///
|
|
/// The settled shape is Xcode's: "branding + actions left, recents right (board icon, name,
|
|
/// location, lane/card counts, sorted by last opened)". This is the left half, plus the one thing
|
|
/// that cannot wait — the list of boards that failed to open, because 02 § Launch and window
|
|
/// lifecycle forbids a launch-time failure from being silently dropped and welcome is where it must
|
|
/// surface.
|
|
///
|
|
/// The layout is therefore already an `HStack` with one column in it. The recents column drops in
|
|
/// beside it; nothing here has to move.
|
|
// m4-welcome: the recents column, New Board… / Open Recent, per-row Forget and Reveal in Finder, and
|
|
// the row-level failure rendering 02 specifies (a failed board's own row carrying fail-fast's
|
|
// specifics, or the unavailable state per Graceful orphaning) all land with the welcome milestone.
|
|
struct WelcomeView: View {
|
|
|
|
@Environment(AppModel.self) private var appModel
|
|
|
|
var body: some View {
|
|
HStack(spacing: 0) {
|
|
branding
|
|
.frame(width: 300)
|
|
.frame(maxHeight: .infinity)
|
|
.padding(32)
|
|
|
|
Divider()
|
|
|
|
failures
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
|
.padding(32)
|
|
}
|
|
// Fixed, with `.windowResizability(.contentSize)` on the scene: welcome is a launcher, not a
|
|
// workspace, and Xcode's — the window this one is modelled on — does not resize either. The
|
|
// one thing that can grow without bound is the failure list, which scrolls.
|
|
.frame(width: 760, height: 460)
|
|
}
|
|
|
|
// MARK: Branding and actions
|
|
|
|
private var branding: some View {
|
|
VStack(alignment: .leading, spacing: 0) {
|
|
Image(nsImage: NSApp.applicationIconImage)
|
|
.resizable()
|
|
.frame(width: 96, height: 96)
|
|
.accessibilityHidden(true)
|
|
|
|
Text("Lanework")
|
|
.font(.system(size: 34, weight: .light))
|
|
.padding(.top, 12)
|
|
|
|
Text(versionSummary)
|
|
.font(.callout)
|
|
.foregroundStyle(.secondary)
|
|
|
|
Spacer(minLength: 24)
|
|
|
|
Button("Open Board…") {
|
|
appModel.presentOpenPanel()
|
|
}
|
|
.controlSize(.large)
|
|
}
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
}
|
|
|
|
private var versionSummary: String {
|
|
let info = Bundle.main.infoDictionary
|
|
let short = info?["CFBundleShortVersionString"] as? String ?? "—"
|
|
let build = info?["CFBundleVersion"] as? String ?? "—"
|
|
return "Version \(short) (\(build))"
|
|
}
|
|
|
|
// MARK: Failed opens
|
|
|
|
@ViewBuilder
|
|
private var failures: some View {
|
|
if appModel.launchFailures.isEmpty {
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
Text("No boards open")
|
|
.font(.title3)
|
|
Text("Open a board folder to get started.")
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
} else {
|
|
VStack(alignment: .leading, spacing: 12) {
|
|
Text("Couldn't open")
|
|
.font(.title3)
|
|
|
|
ScrollView {
|
|
VStack(alignment: .leading, spacing: 12) {
|
|
ForEach(appModel.launchFailures) { failure in
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
Text(failure.displayName)
|
|
.font(.headline)
|
|
Text(failure.message)
|
|
.font(.callout)
|
|
.foregroundStyle(.secondary)
|
|
.fixedSize(horizontal: false, vertical: true)
|
|
Text(failure.path)
|
|
.font(.caption)
|
|
.foregroundStyle(.tertiary)
|
|
.lineLimit(1)
|
|
.truncationMode(.middle)
|
|
}
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
}
|
|
}
|
|
}
|
|
|
|
Button("Clear") {
|
|
appModel.clearLaunchFailures()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Settings
|
|
|
|
/// The app's preferences (⌘, — 11-command-nexus.md).
|
|
///
|
|
/// One control, which is the whole of v1: "Restore open boards at launch". The preference gates only
|
|
/// whether the registry's open-now flags are *consulted* at launch — the flags themselves are
|
|
/// maintained either way, which is what keeps crash recovery working for a user who has restoration
|
|
/// turned off and then turns it back on.
|
|
struct SettingsView: View {
|
|
|
|
@AppStorage(AppPreferences.restoreOpenBoardsAtLaunchKey)
|
|
private var restoreOpenBoardsAtLaunch = true
|
|
|
|
var body: some View {
|
|
Form {
|
|
Toggle("Restore open boards at launch", isOn: $restoreOpenBoardsAtLaunch)
|
|
}
|
|
.formStyle(.grouped)
|
|
.frame(width: 420)
|
|
.fixedSize()
|
|
}
|
|
}
|