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() } }