diff --git a/Microfiche/ContentView.swift b/Microfiche/ContentView.swift index de8fdb4..72486c7 100644 --- a/Microfiche/ContentView.swift +++ b/Microfiche/ContentView.swift @@ -69,9 +69,11 @@ struct ContentView: View { static let minimumWidth: CGFloat = 240 static let idealWidth: CGFloat = 280 static let maximumWidth: CGFloat = 360 - static let autoCollapseWidth: CGFloat = 220 } + @Environment(\.accessibilityReduceMotion) private var reduceMotion + @Environment(\.toggleSidebar) private var toggleSidebar + @State private var selection: Selection? @State private var imageFiles: [ImageFile] = [] @State private var libraryLoadGeneration = UUID() @@ -88,7 +90,7 @@ struct ContentView: View { @State private var scrollToID: UUID? @State private var gridColumnCount: Int = 1 @State private var detailViewFile: ImageFile? - @State private var isMetadataInspectorPresented = true + @AppStorage("isMetadataInspectorPresented") private var isMetadataInspectorPresented = true @State private var splitViewVisibility: NavigationSplitViewVisibility = .all @State private var externalDriveNotice: String? @AppStorage("lastSelectedLibraryFolderID") private var lastSelectedLibraryFolderID = "" @@ -110,7 +112,6 @@ struct ContentView: View { externalVolumes: libraryStorage.rememberedExternalVolumes, contactSheets: contactSheetStorage.contactSheets, selection: selection, - onWidthChange: handleSidebarWidthChange, onLinkFolder: linkFolder, onSelect: { newSelection in selection = newSelection @@ -140,7 +141,7 @@ struct ContentView: View { max: SidebarLayout.maximumWidth ) } detail: { - ZStack { + NavigationStack { MainContentView( imageFiles: imageFiles, unavailableLocation: unavailableSelectedFolder, @@ -157,20 +158,14 @@ struct ContentView: View { contactSheets: contactSheetStorage.contactSheets, onAddToContactSheet: handleAddToContactSheet ) - .opacity(detailViewFile == nil ? 1 : 0) - .allowsHitTesting(detailViewFile == nil) - .accessibilityHidden(detailViewFile != nil) - - if let detailFile = detailViewFile { + .navigationDestination(item: $detailViewFile) { file in ImageDetailView( - file: detailFile, + file: file, isInspectorPresented: $isMetadataInspectorPresented, onBack: closeImageDetail ) - .transition(.opacity) } } - .animation(MicroficheMotion.transition, value: detailViewFile?.id) .inspector(isPresented: $isMetadataInspectorPresented) { Group { if let focusedImageFile { @@ -185,13 +180,22 @@ struct ContentView: View { } } .frame(maxWidth: .infinity, maxHeight: .infinity) - .microficheSidebarChrome() .inspectorColumnWidth(min: 280, ideal: 320, max: 420) } } .navigationTitle("") .toolbar { if detailViewFile == nil { + ToolbarItem { + Button(action: toggleSidebar) { + Image(systemName: "sidebar.left") + } + .help("Toggle Sidebar") + .accessibilityLabel("Toggle sidebar") + .accessibilityIdentifier("sidebar.toggle") + } + .hideSharedBackgroundIfAvailable() + ToolbarItem(placement: .principal) { if viewMode == .grid { HStack(spacing: 6) { @@ -229,6 +233,8 @@ struct ContentView: View { Image(systemName: "sidebar.right") } .help(isMetadataInspectorPresented ? "Hide Info" : "Show Info") + .accessibilityLabel(isMetadataInspectorPresented ? "Hide inspector" : "Show inspector") + .accessibilityIdentifier("inspector.toggle") } .hideSharedBackgroundIfAvailable() } @@ -455,16 +461,6 @@ struct ContentView: View { } } - private func handleSidebarWidthChange(_ width: CGFloat) { - guard splitViewVisibility != .detailOnly else { return } - - if width < SidebarLayout.autoCollapseWidth { - withAnimation(MicroficheMotion.transition) { - splitViewVisibility = .detailOnly - } - } - } - // MARK: - Contact Sheets private func handleDropToContactSheet(sheetID: UUID, urls: [URL]) { @@ -554,16 +550,12 @@ struct ContentView: View { isQuickPreviewPresented = false selectedImageFileIDs = [fileID] focusedImageFileID = fileID - withAnimation(MicroficheMotion.transition) { - detailViewFile = file - // Keep the metadata inspector available for editing Finder labels/tags. - isMetadataInspectorPresented = true - } + detailViewFile = file } } private func closeImageDetail() { - withAnimation(MicroficheMotion.transition) { + withAnimation(MicroficheMotion.transition(reducedMotion: reduceMotion)) { detailViewFile = nil } requestScrollToFocusedImage() diff --git a/Microfiche/Views/SidebarView.swift b/Microfiche/Views/SidebarView.swift index 1ff417f..001c6e8 100644 --- a/Microfiche/Views/SidebarView.swift +++ b/Microfiche/Views/SidebarView.swift @@ -15,7 +15,6 @@ struct SidebarView: View { let externalVolumes: [RememberedExternalVolume] let contactSheets: [ContactSheet] let selection: Selection? - let onWidthChange: (CGFloat) -> Void let onLinkFolder: () -> Void let onSelect: (Selection) -> Void let onRemoveFolder: (UUID) -> Void @@ -115,7 +114,7 @@ struct SidebarView: View { } .scrollIndicators(.hidden) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) - .background(WidthReader(onChange: onWidthChange)) + .accessibilityIdentifier("library.sidebar") } private var folderSectionDetail: String { diff --git a/Microfiche/Views/WidthReader.swift b/Microfiche/Views/WidthReader.swift deleted file mode 100644 index 59726d2..0000000 --- a/Microfiche/Views/WidthReader.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// WidthReader.swift -// Microfiche -// -// Created by David Hoang on 6/8/25. -// - -import SwiftUI - -struct WidthReader: View { - let onChange: (CGFloat) -> Void - - var body: some View { - GeometryReader { geo in - Color.clear - .onAppear { onChange(geo.size.width) } - .onChange(of: geo.size.width) { _, newWidth in - onChange(newWidth) - } - } - } -} diff --git a/design.md b/design.md index efc2f13..d923969 100644 --- a/design.md +++ b/design.md @@ -53,9 +53,9 @@ Rules derived from `ContentView`, `SidebarView`, and `ImageDetailView`. 1. **Two-column shell + inspector** — Use `NavigationSplitView` (sidebar + detail). Metadata lives in a detail-attached `.inspector`, not a third permanent split column. 2. **Sidebar owns library location** — Selection is a single enum: All Images, Folder, or Contact Sheet. External drive rows are status only — not navigation targets. 3. **Location change clears browsing state** — On library selection change, clear image selection, focus, quick preview, and detail view. -4. **Detail is a canvas overlay** — Double-click opens `ImageDetailView` over the library detail column (opacity / hit-testing), not a pushed navigation destination or new column. +4. **Detail pushes on the detail stack** — Double-click opens `ImageDetailView` via `NavigationStack` + `navigationDestination`, not an opacity overlay or new split column. 5. **Inspector stays with detail** — Entering detail keeps the metadata inspector available so Finder labels, tags, and comments can be edited while viewing. -6. **Sidebar is collapsible** — Width 240–360 (ideal 280). Auto-collapse to `.detailOnly` below 220 pt measured width. +6. **Sidebar is collapsible** — Width 240–360 (ideal 280). Use the system split-view divider and sidebar toggle; do not programmatically override `columnVisibility` during resize. 7. **Unified chrome** — Window uses unified toolbar with no title. Keep `.navigationTitle("")` unless a mode truly needs a title. 8. **Window scale** — Minimum about 1100×700; default launch size stays large enough for sidebar + grid + inspector. @@ -190,7 +190,7 @@ Disable animations while the grid size slider is dragging. Prefer `MicroficheMot ### Sidebar - **File:** `Microfiche/Views/SidebarView.swift` -- Custom scroll sections; width reported via `WidthReader` +- Custom scroll sections; collapsible via the system split-view divider and sidebar toggle ### Library canvas - **File:** `Microfiche/Views/MainContentView.swift`