diff --git a/Sources/Loci/ContentView.swift b/Sources/Loci/ContentView.swift index a392575..8b634c0 100644 --- a/Sources/Loci/ContentView.swift +++ b/Sources/Loci/ContentView.swift @@ -97,100 +97,112 @@ struct LociShell: View { @State private var isShowingCommandPalette = false var body: some View { - ZStack { - LociColor.surface - - HStack(spacing: 0) { - LociSidebar(store: store) - .frame(width: 164) - - MainReferencePane(store: store, namespace: namespace) + workspace + .clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous)) + .animation(AppMotion.instant, value: store.mode) + .animation(AppMotion.toast, value: store.selectedItemIDs) + .animation(AppMotion.hero, value: store.focusedItemID) + .focusable() + .onKeyPress(.leftArrow) { + guard store.focusedItemID != nil else { return .ignored } + withAnimation(AppMotion.smooth) { + _ = store.focusAdjacentVisibleItem(offset: -1) + } + return .handled } - - if showsReferenceChrome { - LociTitle(store: store) - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) - .padding(.top, 22) + .onKeyPress(.rightArrow) { + guard store.focusedItemID != nil else { return .ignored } + withAnimation(AppMotion.smooth) { + _ = store.focusAdjacentVisibleItem(offset: 1) + } + return .handled } - - if showsReferenceChrome { - BottomModeBar(store: store) - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom) - .padding(.bottom, 20) + .onDeleteCommand { + store.sendSelectionToTrash(undoManager: undoManager) } - - if showsReferenceChrome { - UtilityCluster(store: store) - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottomTrailing) - .padding(.trailing, 18) - .padding(.bottom, 21) + .onExitCommand { + if store.focusedItemID != nil { + store.requestFocusDismissal() + } else { + store.selectedItemIDs.removeAll() + store.searchText = "" + } } - - if showsReferenceChrome && !store.selectedItemIDs.isEmpty && store.selectedFilter != .trash { - BatchActionBar(store: store) - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom) - .padding(.bottom, 60) - .transition(AppMotion.bottomToastTransition) + .onReceive(NotificationCenter.default.publisher(for: .lociShowCommandPalette)) { _ in + withAnimation(AppMotion.quick) { + isShowingCommandPalette = true + } } - - if showsReferenceChrome && store.mode == .grid { - ZoomSlider(store: store) - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topTrailing) - .padding(.top, 22) - .padding(.trailing, 18) + .onAppear { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.20) { + store.warmCommonReferenceFilters() + } + startDemoAutoplayIfNeeded() } + } - if isShowingCommandPalette { - Color.black.opacity(0.16) - .ignoresSafeArea() - .onTapGesture { isShowingCommandPalette = false } + private var workspace: some View { + ZStack { + LociColor.surface - CommandPaletteView(store: store, isPresented: $isShowingCommandPalette) - .frame(width: 560) - .transition(.opacity.combined(with: .scale(scale: 0.98))) - .zIndex(30) - } - } - .clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous)) - .animation(AppMotion.instant, value: store.mode) - .animation(AppMotion.toast, value: store.selectedItemIDs) - .animation(AppMotion.hero, value: store.focusedItemID) - .focusable() - .onKeyPress(.leftArrow) { - guard store.focusedItemID != nil else { return .ignored } - withAnimation(AppMotion.smooth) { - _ = store.focusAdjacentVisibleItem(offset: -1) - } - return .handled + libraryWorkspace + referenceChrome + commandPaletteOverlay } - .onKeyPress(.rightArrow) { - guard store.focusedItemID != nil else { return .ignored } - withAnimation(AppMotion.smooth) { - _ = store.focusAdjacentVisibleItem(offset: 1) - } - return .handled + } + + private var libraryWorkspace: some View { + HStack(spacing: 0) { + LociSidebar(store: store) + .frame(width: 164) + + MainReferencePane(store: store, namespace: namespace) } - .onDeleteCommand { - store.sendSelectionToTrash(undoManager: undoManager) + } + + @ViewBuilder + private var referenceChrome: some View { + if showsReferenceChrome { + LociTitle(store: store) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) + .padding(.top, 22) + + BottomModeBar(store: store) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom) + .padding(.bottom, 20) + + UtilityCluster(store: store) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottomTrailing) + .padding(.trailing, 18) + .padding(.bottom, 21) } - .onExitCommand { - if store.focusedItemID != nil { - store.requestFocusDismissal() - } else { - store.selectedItemIDs.removeAll() - store.searchText = "" - } + + if showsReferenceChrome && !store.selectedItemIDs.isEmpty && store.selectedFilter != .trash { + BatchActionBar(store: store) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom) + .padding(.bottom, 60) + .transition(AppMotion.bottomToastTransition) } - .onReceive(NotificationCenter.default.publisher(for: .lociShowCommandPalette)) { _ in - withAnimation(AppMotion.quick) { - isShowingCommandPalette = true - } + + if showsReferenceChrome && store.mode == .grid { + ZoomSlider(store: store) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topTrailing) + .padding(.top, 22) + .padding(.trailing, 18) } - .onAppear { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.20) { - store.warmCommonReferenceFilters() - } - startDemoAutoplayIfNeeded() + } + + @ViewBuilder + private var commandPaletteOverlay: some View { + if isShowingCommandPalette { + Color.black.opacity(0.16) + .ignoresSafeArea() + .onTapGesture { isShowingCommandPalette = false } + + CommandPaletteView(store: store, isPresented: $isShowingCommandPalette) + .frame(width: 560) + .transition(.opacity.combined(with: .scale(scale: 0.98))) + .zIndex(30) } } @@ -2168,7 +2180,7 @@ struct ZoomSlider: View { .animation(AppMotion.quick, value: isDragging) .offset(x: knobOffset) } - .frame(maxHeight: .infinity) + .frame(height: 20) .contentShape(Rectangle()) .gesture( DragGesture(minimumDistance: 0) @@ -2184,7 +2196,7 @@ struct ZoomSlider: View { } ) } - .frame(width: 82) + .frame(width: 82, height: 20) Button { stepZoom(by: 0.08) diff --git a/Sources/Loci/LociApp.swift b/Sources/Loci/LociApp.swift index 38fcf69..abdab99 100644 --- a/Sources/Loci/LociApp.swift +++ b/Sources/Loci/LociApp.swift @@ -27,6 +27,10 @@ final class LociAppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate, private var libraryStore: LibraryStore? func applicationDidFinishLaunching(_ notification: Notification) { + // Loci's palette is authored against light surfaces (see LociDesign); + // pin the appearance so the titlebar and toolbar can't fall out of + // step with the content when the system is in dark mode. + NSApp.appearance = NSAppearance(named: .aqua) LegacyAppMigration.run() LociEnvironment.reload() configureAppIcon() @@ -214,25 +218,26 @@ final class LociAppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate, ) -> NSToolbarItem? { let item = NSToolbarItem(itemIdentifier: itemIdentifier) item.target = self + item.isBordered = true switch itemIdentifier { case .lociCommandPalette: item.label = "Command Palette" item.paletteLabel = "Command Palette" item.toolTip = "Command Palette (⌘K)" - item.image = NSImage(systemSymbolName: "command", accessibilityDescription: "Command Palette") + item.image = Self.toolbarSymbol("command", description: "Command Palette") item.action = #selector(showCommandPalette) case .lociNotebookInspector: item.label = "Ask Loci" item.paletteLabel = "Show or Hide Ask Loci" item.toolTip = "Show or Hide Ask Loci (⌥⌘I)" - item.image = NSImage(systemSymbolName: "sidebar.right", accessibilityDescription: "Show or Hide Ask Loci") + item.image = Self.toolbarSymbol("sidebar.right", description: "Show or Hide Ask Loci") item.action = #selector(toggleNotebookInspector) case .lociSettings: item.label = "Settings" item.paletteLabel = "Settings" item.toolTip = "Settings (⌘,)" - item.image = NSImage(systemSymbolName: "gearshape", accessibilityDescription: "Settings") + item.image = Self.toolbarSymbol("gearshape", description: "Settings") item.action = #selector(openSettings) default: return nil @@ -240,6 +245,13 @@ final class LociAppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate, return item } + /// Symbols sized for the unified-compact toolbar; without an explicit + /// configuration the raw template images render noticeably oversized. + private static func toolbarSymbol(_ name: String, description: String) -> NSImage? { + NSImage(systemSymbolName: name, accessibilityDescription: description)? + .withSymbolConfiguration(NSImage.SymbolConfiguration(pointSize: 13, weight: .regular)) + } + @objc private func openSettings() { if let existing = settingsWindow { existing.makeKeyAndOrderFront(nil) diff --git a/Sources/Loci/LociDesign.swift b/Sources/Loci/LociDesign.swift index 629e39e..4cdc2cb 100644 --- a/Sources/Loci/LociDesign.swift +++ b/Sources/Loci/LociDesign.swift @@ -75,7 +75,9 @@ private struct LociScaledFontModifier: ViewModifier { } func body(content: Content) -> some View { - content.lociFont(size: scaledSize, weight: weight, design: design, relativeTo: .body) + // Do not call `lociFont` here: that would reapply this modifier and + // recurse through SwiftUI's view construction until launch crashes. + content.font(.system(size: scaledSize, weight: weight, design: design)) } }