diff --git a/Termind/App/RootView.swift b/Termind/App/RootView.swift index b9abcc0d..211349e2 100644 --- a/Termind/App/RootView.swift +++ b/Termind/App/RootView.swift @@ -19,6 +19,9 @@ struct RootView: View { /// split view's minimum sidebar width are rejected so the tab strip keeps /// the user's last visible boundary. @State private var pinnedTabStripWidth = Metrics.Sidebar.ideal + /// Drives the sidebar's responsive upper bound. Start at the default so the + /// first layout pass already has a sensible value before geometry reports. + @State private var layoutWidth = Metrics.Window.defaultWidth /// `nonisolated` because geometry transforms are `Sendable` closures and /// this coordinate-space name is immutable, not main-actor state. @@ -45,6 +48,11 @@ struct RootView: View { maxHeight: .infinity ) .coordinateSpace(.named(Self.layoutSpace)) + .onGeometryChange(for: CGFloat.self) { + $0.size.width + } action: { + layoutWidth = $0 + } .toolbarBackground(WindowChrome.background(for: colorScheme), for: .windowToolbar) .toolbarBackground(.visible, for: .windowToolbar) // ⌘T and every Connect entry land here. A sheet rather than a page, @@ -123,8 +131,8 @@ struct RootView: View { SidebarView(detailCatalog: .standard(store: app.hostLibrary)) .navigationSplitViewColumnWidth( min: Metrics.Sidebar.min, - ideal: Metrics.Sidebar.ideal, - max: Metrics.Sidebar.max + ideal: min(Metrics.Sidebar.ideal, sidebarMaxWidth), + max: sidebarMaxWidth ) } detail: { VStack(spacing: 0) { @@ -153,11 +161,15 @@ struct RootView: View { VaultSidebarView() .navigationSplitViewColumnWidth( min: Metrics.Sidebar.min, - ideal: Metrics.Sidebar.ideal, - max: Metrics.Sidebar.max + ideal: min(Metrics.Sidebar.ideal, sidebarMaxWidth), + max: sidebarMaxWidth ) } + private var sidebarMaxWidth: CGFloat { + Metrics.Window.sidebarMaxWidth(for: layoutWidth) + } + private func recordPinnedTabStripWidth(_ measuredDetailLeadingEdge: CGFloat) { pinnedTabStripWidth = PinnedTopLevelTabSizing.resolvedStripWidth( current: pinnedTabStripWidth, diff --git a/Termind/Design/DesignTokens.swift b/Termind/Design/DesignTokens.swift index ee57e383..6d9d808e 100644 --- a/Termind/Design/DesignTokens.swift +++ b/Termind/Design/DesignTokens.swift @@ -271,16 +271,30 @@ nonisolated enum Metrics { static let detailMin: CGFloat = 320 static let detailMinHeight: CGFloat = 440 + /// Space to the right of the navigation sidebar in Vault's complete + /// working layout: two dividers, the main surface, and an Edit/New + /// detail panel. + static let trailingContentMinWidth: CGFloat = 1 + + Metrics.VaultContent.min + + 1 + + Metrics.VaultDetail.min + /// Vault's complete working layout sets the application floor: the /// navigation sidebar, main surface, and an Edit/New detail panel, plus /// the two dividers between them. Reserving the third column even while /// it is closed prevents opening an editor inside an already-too-narrow /// window and keeps every host form usable from the moment it appears. - static let minWidth: CGFloat = Metrics.Sidebar.min - + 1 - + Metrics.VaultContent.min - + 1 - + Metrics.VaultDetail.min + static let minWidth: CGFloat = Metrics.Sidebar.min + trailingContentMinWidth + + /// The sidebar may use only width left after the complete Vault work + /// area has been reserved. It grows with the window, but never outside + /// its own usability bounds. + static func sidebarMaxWidth(for availableWidth: CGFloat) -> CGFloat { + min( + Metrics.Sidebar.max, + max(Metrics.Sidebar.min, availableWidth - trailingContentMinWidth) + ) + } /// Root content includes the pinned tab strip and its divider above the /// detail floor. The macOS title bar is outside this measurement.