From fa691322ff70260d6fa3ee99e3c3d8a4727a508c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ng=C3=B4=20Qu=E1=BB=91c=20=C4=90=E1=BA=A1t?= Date: Mon, 6 Apr 2026 13:51:40 +0700 Subject: [PATCH 1/8] refactor: replace event monitor with .onKeyPress() in connection switcher --- .../Toolbar/ConnectionSwitcherPopover.swift | 76 ++++++------------- 1 file changed, 24 insertions(+), 52 deletions(-) diff --git a/TablePro/Views/Toolbar/ConnectionSwitcherPopover.swift b/TablePro/Views/Toolbar/ConnectionSwitcherPopover.swift index 27e634c4..cce0b2d8 100644 --- a/TablePro/Views/Toolbar/ConnectionSwitcherPopover.swift +++ b/TablePro/Views/Toolbar/ConnectionSwitcherPopover.swift @@ -15,7 +15,6 @@ struct ConnectionSwitcherPopover: View { @State private var savedConnections: [DatabaseConnection] = [] @State private var isConnecting: UUID? @State private var selectedIndex: Int = 0 - @State private var keyMonitor: Any? @Environment(\.openWindow) private var openWindow @@ -157,61 +156,34 @@ struct ConnectionSwitcherPopover: View { selectedIndex = idx } } - installKeyMonitor() } - .onDisappear { - removeKeyMonitor() - } - } - - // MARK: - Keyboard Navigation - - private func installKeyMonitor() { - keyMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { event in + .onExitCommand { onDismiss?() } + .onKeyPress(.return) { let items = allItems - switch event.keyCode { - case KeyCode.upArrow.rawValue: - if selectedIndex > 0 { - selectedIndex -= 1 - } - return nil - case KeyCode.downArrow.rawValue: - if selectedIndex < items.count - 1 { - selectedIndex += 1 - } - return nil - case KeyCode.return.rawValue: - guard selectedIndex >= 0, selectedIndex < items.count else { return event } - switch items[selectedIndex] { - case .session(let session): - switchToSession(session.id) - case .saved(let connection): - connectToSaved(connection) - } - return nil - case KeyCode.escape.rawValue: - onDismiss?() - return nil - case KeyCode.j.rawValue where event.modifierFlags.intersection(.deviceIndependentFlagsMask).contains(.control): - if selectedIndex < items.count - 1 { - selectedIndex += 1 - } - return nil - case KeyCode.k.rawValue where event.modifierFlags.contains(.control): - if selectedIndex > 0 { - selectedIndex -= 1 - } - return nil - default: - return event + guard selectedIndex >= 0, selectedIndex < items.count else { return .ignored } + switch items[selectedIndex] { + case .session(let session): switchToSession(session.id) + case .saved(let connection): connectToSaved(connection) } + return .handled } - } - - private func removeKeyMonitor() { - if let monitor = keyMonitor { - NSEvent.removeMonitor(monitor) - keyMonitor = nil + .onKeyPress(.upArrow) { + if selectedIndex > 0 { selectedIndex -= 1 } + return .handled + } + .onKeyPress(.downArrow) { + if selectedIndex < allItems.count - 1 { selectedIndex += 1 } + return .handled + } + .onKeyPress(characters: .init(charactersIn: "j"), phases: [.down, .repeat]) { keyPress in + guard keyPress.modifiers.contains(.control) else { return .ignored } + if selectedIndex < allItems.count - 1 { selectedIndex += 1 } + return .handled + } + .onKeyPress(characters: .init(charactersIn: "k"), phases: [.down, .repeat]) { keyPress in + guard keyPress.modifiers.contains(.control) else { return .ignored } + if selectedIndex > 0 { selectedIndex -= 1 } + return .handled } } From 4730be643862039e4d89ce5c58ff6af722ec2640 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ng=C3=B4=20Qu=E1=BB=91c=20=C4=90=E1=BA=A1t?= Date: Mon, 6 Apr 2026 13:51:47 +0700 Subject: [PATCH 2/8] refactor: extract reusable SearchFieldView from 4 custom search field implementations --- .../Views/Components/SearchFieldView.swift | 36 +++++++++++++++++++ .../DatabaseSwitcherSheet.swift | 28 +++------------ .../QuickSwitcher/QuickSwitcherView.swift | 27 +++----------- .../Views/RightSidebar/RightSidebarView.swift | 24 +++---------- .../Views/Settings/KeyboardSettingsView.swift | 22 +++--------- 5 files changed, 55 insertions(+), 82 deletions(-) create mode 100644 TablePro/Views/Components/SearchFieldView.swift diff --git a/TablePro/Views/Components/SearchFieldView.swift b/TablePro/Views/Components/SearchFieldView.swift new file mode 100644 index 00000000..5128edd7 --- /dev/null +++ b/TablePro/Views/Components/SearchFieldView.swift @@ -0,0 +1,36 @@ +// +// SearchFieldView.swift +// TablePro +// + +import SwiftUI + +struct SearchFieldView: View { + let placeholder: String + @Binding var text: String + var fontSize: CGFloat = ThemeEngine.shared.activeTheme.typography.body + + var body: some View { + HStack(spacing: 6) { + Image(systemName: "magnifyingglass") + .font(.system(size: fontSize)) + .foregroundStyle(.tertiary) + + TextField(placeholder, text: $text) + .textFieldStyle(.plain) + .font(.system(size: fontSize)) + + if !text.isEmpty { + Button { text = "" } label: { + Image(systemName: "xmark.circle.fill") + .foregroundStyle(.secondary) + } + .buttonStyle(.plain) + } + } + .padding(.horizontal, 8) + .padding(.vertical, 6) + .background(Color(nsColor: .controlBackgroundColor)) + .clipShape(RoundedRectangle(cornerRadius: 6)) + } +} diff --git a/TablePro/Views/DatabaseSwitcher/DatabaseSwitcherSheet.swift b/TablePro/Views/DatabaseSwitcher/DatabaseSwitcherSheet.swift index 91366f5c..2ad78ed4 100644 --- a/TablePro/Views/DatabaseSwitcher/DatabaseSwitcherSheet.swift +++ b/TablePro/Views/DatabaseSwitcher/DatabaseSwitcherSheet.swift @@ -155,31 +155,13 @@ struct DatabaseSwitcherSheet: View { private var toolbar: some View { HStack(spacing: 8) { // Search - HStack(spacing: 6) { - Image(systemName: "magnifyingglass") - .font(.system(size: ThemeEngine.shared.activeTheme.typography.body)) - .foregroundStyle(.tertiary) - - TextField(isSchemaMode + SearchFieldView( + placeholder: isSchemaMode ? String(localized: "Search schemas...") : String(localized: "Search databases..."), - text: $viewModel.searchText) - .textFieldStyle(.plain) - .font(.system(size: ThemeEngine.shared.activeTheme.typography.body)) - .focused($focus, equals: .search) - - if !viewModel.searchText.isEmpty { - Button(action: { viewModel.searchText = "" }) { - Image(systemName: "xmark.circle.fill") - .foregroundStyle(.secondary) - } - .buttonStyle(.plain) - } - } - .padding(.horizontal, 8) - .padding(.vertical, 6) - .background(Color(nsColor: .controlBackgroundColor)) - .cornerRadius(6) + text: $viewModel.searchText + ) + .focused($focus, equals: .search) // Refresh Button(action: { diff --git a/TablePro/Views/QuickSwitcher/QuickSwitcherView.swift b/TablePro/Views/QuickSwitcher/QuickSwitcherView.swift index 8b7a5242..2ecbcf38 100644 --- a/TablePro/Views/QuickSwitcher/QuickSwitcherView.swift +++ b/TablePro/Views/QuickSwitcher/QuickSwitcherView.swift @@ -95,28 +95,11 @@ internal struct QuickSwitcherSheet: View { // MARK: - Search Toolbar private var searchToolbar: some View { - HStack(spacing: 6) { - Image(systemName: "magnifyingglass") - .font(.system(size: ThemeEngine.shared.activeTheme.typography.body)) - .foregroundStyle(.tertiary) - - TextField("Search tables, views, databases...", text: $viewModel.searchText) - .textFieldStyle(.plain) - .font(.system(size: ThemeEngine.shared.activeTheme.typography.body)) - .focused($focus, equals: .search) - - if !viewModel.searchText.isEmpty { - Button(action: { viewModel.searchText = "" }) { - Image(systemName: "xmark.circle.fill") - .foregroundStyle(.secondary) - } - .buttonStyle(.plain) - } - } - .padding(.horizontal, 8) - .padding(.vertical, 6) - .background(Color(nsColor: .controlBackgroundColor)) - .cornerRadius(6) + SearchFieldView( + placeholder: "Search tables, views, databases...", + text: $viewModel.searchText + ) + .focused($focus, equals: .search) .padding(.horizontal, 12) .padding(.vertical, 8) } diff --git a/TablePro/Views/RightSidebar/RightSidebarView.swift b/TablePro/Views/RightSidebar/RightSidebarView.swift index ff3527ff..48d767f8 100644 --- a/TablePro/Views/RightSidebar/RightSidebarView.swift +++ b/TablePro/Views/RightSidebar/RightSidebarView.swift @@ -146,26 +146,12 @@ struct RightSidebarView: View { return VStack(spacing: 0) { // Inline search field - HStack(spacing: 6) { - Image(systemName: "magnifyingglass") - .foregroundStyle(.tertiary) - .font(.system(size: ThemeEngine.shared.activeTheme.typography.small)) - TextField("Search for field...", text: $searchText) - .textFieldStyle(.plain) - .font(.system(size: ThemeEngine.shared.activeTheme.typography.small)) - if !searchText.isEmpty { - Button { - searchText = "" - } label: { - Image(systemName: "xmark.circle.fill") - .foregroundStyle(.tertiary) - .font(.system(size: ThemeEngine.shared.activeTheme.typography.small)) - } - .buttonStyle(.plain) - } - } + SearchFieldView( + placeholder: "Search for field...", + text: $searchText, + fontSize: ThemeEngine.shared.activeTheme.typography.small + ) .padding(.horizontal, 10) - .padding(.vertical, 6) Divider() diff --git a/TablePro/Views/Settings/KeyboardSettingsView.swift b/TablePro/Views/Settings/KeyboardSettingsView.swift index 6fc2b086..d716c410 100644 --- a/TablePro/Views/Settings/KeyboardSettingsView.swift +++ b/TablePro/Views/Settings/KeyboardSettingsView.swift @@ -19,24 +19,10 @@ struct KeyboardSettingsView: View { var body: some View { VStack(spacing: 0) { // Search bar - HStack { - Image(systemName: "magnifyingglass") - .foregroundStyle(.secondary) - TextField("Search shortcuts...", text: $searchText) - .textFieldStyle(.plain) - if !searchText.isEmpty { - Button { - searchText = "" - } label: { - Image(systemName: "xmark.circle.fill") - .foregroundStyle(.secondary) - } - .buttonStyle(.plain) - } - } - .padding(8) - .background(.quaternary) - .clipShape(RoundedRectangle(cornerRadius: 8)) + SearchFieldView( + placeholder: "Search shortcuts...", + text: $searchText + ) .padding(.horizontal, 20) .padding(.top, 16) .padding(.bottom, 8) From 93e266df83f0e6bc604b961f581e80e1a7d21a75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ng=C3=B4=20Qu=E1=BB=91c=20=C4=90=E1=BA=A1t?= Date: Mon, 6 Apr 2026 13:51:55 +0700 Subject: [PATCH 3/8] refactor: replace custom resize handle with NSSplitView for inspector panel --- TablePro/ContentView.swift | 32 ++-- .../Components/HorizontalSplitView.swift | 146 ++++++++++++++++++ .../Views/Components/PanelResizeHandle.swift | 40 ----- 3 files changed, 161 insertions(+), 57 deletions(-) create mode 100644 TablePro/Views/Components/HorizontalSplitView.swift delete mode 100644 TablePro/Views/Components/PanelResizeHandle.swift diff --git a/TablePro/ContentView.swift b/TablePro/ContentView.swift index ac877299..f82e7679 100644 --- a/TablePro/ContentView.swift +++ b/TablePro/ContentView.swift @@ -226,7 +226,13 @@ struct ContentView: View { } detail: { // MARK: - Detail (Main workspace with optional right sidebar) if let currentSession = currentSession, let rightPanelState, let sessionState { - HStack(spacing: 0) { + HorizontalSplitView( + isTrailingCollapsed: !rightPanelState.isPresented, + trailingWidth: Bindable(rightPanelState).panelWidth, + minTrailingWidth: RightPanelState.minWidth, + maxTrailingWidth: RightPanelState.maxWidth, + autosaveName: "InspectorSplit" + ) { MainContentView( connection: currentSession.connection, payload: payload, @@ -244,23 +250,15 @@ struct ContentView: View { toolbarState: sessionState.toolbarState, coordinator: sessionState.coordinator ) - .frame(maxWidth: .infinity) - - if rightPanelState.isPresented { - PanelResizeHandle(panelWidth: Bindable(rightPanelState).panelWidth) - Divider() - UnifiedRightPanelView( - state: rightPanelState, - inspectorContext: inspectorContext, - connection: currentSession.connection, - tables: currentSession.tables - ) - .frame(width: rightPanelState.panelWidth) - .background(Color(nsColor: .windowBackgroundColor)) - .transition(.move(edge: .trailing)) - } + } trailing: { + UnifiedRightPanelView( + state: rightPanelState, + inspectorContext: inspectorContext, + connection: currentSession.connection, + tables: currentSession.tables + ) + .background(Color(nsColor: .windowBackgroundColor)) } - .animation(.easeInOut(duration: 0.2), value: rightPanelState.isPresented) } else { VStack(spacing: 16) { ProgressView() diff --git a/TablePro/Views/Components/HorizontalSplitView.swift b/TablePro/Views/Components/HorizontalSplitView.swift new file mode 100644 index 00000000..9662c503 --- /dev/null +++ b/TablePro/Views/Components/HorizontalSplitView.swift @@ -0,0 +1,146 @@ +// +// HorizontalSplitView.swift +// TablePro +// + +import AppKit +import SwiftUI + +struct HorizontalSplitView: NSViewRepresentable { + var isTrailingCollapsed: Bool + @Binding var trailingWidth: CGFloat + var minTrailingWidth: CGFloat + var maxTrailingWidth: CGFloat + var autosaveName: String + @ViewBuilder var leading: Leading + @ViewBuilder var trailing: Trailing + + func makeCoordinator() -> Coordinator { + Coordinator(trailingWidth: $trailingWidth) + } + + func makeNSView(context: Context) -> NSSplitView { + let splitView = NSSplitView() + splitView.isVertical = true + splitView.dividerStyle = .thin + splitView.autosaveName = autosaveName + splitView.delegate = context.coordinator + + let leadingHosting = NSHostingView(rootView: leading) + leadingHosting.sizingOptions = [.minSize] + + let trailingHosting = NSHostingView(rootView: trailing) + trailingHosting.sizingOptions = [.minSize] + + splitView.addArrangedSubview(leadingHosting) + splitView.addArrangedSubview(trailingHosting) + + context.coordinator.leadingHosting = leadingHosting + context.coordinator.trailingHosting = trailingHosting + context.coordinator.lastCollapsedState = isTrailingCollapsed + context.coordinator.minWidth = minTrailingWidth + context.coordinator.maxWidth = maxTrailingWidth + + if isTrailingCollapsed { + trailingHosting.isHidden = true + } + + return splitView + } + + func updateNSView(_ splitView: NSSplitView, context: Context) { + context.coordinator.leadingHosting?.rootView = leading + context.coordinator.trailingHosting?.rootView = trailing + context.coordinator.minWidth = minTrailingWidth + context.coordinator.maxWidth = maxTrailingWidth + + guard let trailingView = context.coordinator.trailingHosting else { return } + let wasCollapsed = context.coordinator.lastCollapsedState + + if isTrailingCollapsed != wasCollapsed { + context.coordinator.lastCollapsedState = isTrailingCollapsed + if isTrailingCollapsed { + if splitView.subviews.count >= 2 { + context.coordinator.savedDividerPosition = splitView.subviews[1].frame.width + } + splitView.setPosition(splitView.bounds.width, ofDividerAt: 0) + trailingView.isHidden = true + splitView.display() + } else { + trailingView.isHidden = false + splitView.adjustSubviews() + if let saved = context.coordinator.savedDividerPosition { + splitView.setPosition(splitView.bounds.width - saved, ofDividerAt: 0) + } + splitView.display() + } + } + } + + final class Coordinator: NSObject, NSSplitViewDelegate { + var leadingHosting: NSHostingView? + var trailingHosting: NSHostingView? + var lastCollapsedState = false + var savedDividerPosition: CGFloat? + var minWidth: CGFloat = 200 + var maxWidth: CGFloat = 600 + var trailingWidth: Binding + + init(trailingWidth: Binding) { + self.trailingWidth = trailingWidth + } + + func splitView( + _ splitView: NSSplitView, + constrainMinCoordinate proposedMinimumPosition: CGFloat, + ofSubviewAt dividerIndex: Int + ) -> CGFloat { + splitView.bounds.width - maxWidth + } + + func splitView( + _ splitView: NSSplitView, + constrainMaxCoordinate proposedMaximumPosition: CGFloat, + ofSubviewAt dividerIndex: Int + ) -> CGFloat { + splitView.bounds.width - minWidth + } + + func splitView( + _ splitView: NSSplitView, + canCollapseSubview subview: NSView + ) -> Bool { + subview == trailingHosting + } + + func splitView( + _ splitView: NSSplitView, + effectiveRect proposedEffectiveRect: NSRect, + forDrawnRect drawnRect: NSRect, + ofDividerAt dividerIndex: Int + ) -> NSRect { + if trailingHosting?.isHidden == true { + return .zero + } + return proposedEffectiveRect + } + + func splitView( + _ splitView: NSSplitView, + shouldHideDividerAt dividerIndex: Int + ) -> Bool { + trailingHosting?.isHidden == true + } + + func splitViewDidResizeSubviews(_ notification: Notification) { + guard let splitView = notification.object as? NSSplitView, + splitView.subviews.count >= 2, + trailingHosting?.isHidden != true + else { return } + let width = splitView.subviews[1].frame.width + if width > 0 { + trailingWidth.wrappedValue = width + } + } + } +} diff --git a/TablePro/Views/Components/PanelResizeHandle.swift b/TablePro/Views/Components/PanelResizeHandle.swift deleted file mode 100644 index 6f8531c6..00000000 --- a/TablePro/Views/Components/PanelResizeHandle.swift +++ /dev/null @@ -1,40 +0,0 @@ -// -// PanelResizeHandle.swift -// TablePro -// -// Draggable resize handle for the right panel. -// - -import SwiftUI - -struct PanelResizeHandle: View { - @Binding var panelWidth: CGFloat - - @State private var isDragging = false - - var body: some View { - Rectangle() - .fill(Color.clear) - .frame(width: 5) - .contentShape(Rectangle()) - .onHover { hovering in - if hovering { - NSCursor.resizeLeftRight.push() - } else { - NSCursor.pop() - } - } - .gesture( - DragGesture(minimumDistance: 1) - .onChanged { value in - isDragging = true - // Dragging left increases panel width (handle is on the leading edge) - let newWidth = panelWidth - value.translation.width - panelWidth = min(max(newWidth, RightPanelState.minWidth), RightPanelState.maxWidth) - } - .onEnded { _ in - isDragging = false - } - ) - } -} From f47fec36b098ae0f708e4a5b1743de371a9a6a03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ng=C3=B4=20Qu=E1=BB=91c=20=C4=90=E1=BA=A1t?= Date: Mon, 6 Apr 2026 13:52:00 +0700 Subject: [PATCH 4/8] docs: update CHANGELOG for medium audit fixes --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d1b223f8..0f7f0017 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Use semantic selected-text color instead of hardcoded white in selected rows - Use proper CommandGroup for full-screen shortcut instead of event monitor - Use sheet presentation for all file open/save panels instead of free-floating dialogs +- Replace event monitor with native SwiftUI .onKeyPress() in connection switcher +- Extract reusable SearchFieldView component from 4 custom search field implementations +- Replace custom resize handle with native NSSplitView for inspector panel ### Added From 037a9e4f6732d7cf77b2ac70c51c254a071df64f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ng=C3=B4=20Qu=E1=BB=91c=20=C4=90=E1=BA=A1t?= Date: Mon, 6 Apr 2026 13:56:27 +0700 Subject: [PATCH 5/8] fix: add smooth animation for inspector panel collapse/expand --- .../Components/HorizontalSplitView.swift | 30 +++++++++++-------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/TablePro/Views/Components/HorizontalSplitView.swift b/TablePro/Views/Components/HorizontalSplitView.swift index 9662c503..8667c802 100644 --- a/TablePro/Views/Components/HorizontalSplitView.swift +++ b/TablePro/Views/Components/HorizontalSplitView.swift @@ -59,20 +59,24 @@ struct HorizontalSplitView: NSViewRepresentable { if isTrailingCollapsed != wasCollapsed { context.coordinator.lastCollapsedState = isTrailingCollapsed - if isTrailingCollapsed { - if splitView.subviews.count >= 2 { - context.coordinator.savedDividerPosition = splitView.subviews[1].frame.width + NSAnimationContext.runAnimationGroup { ctx in + ctx.duration = 0.2 + ctx.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) + ctx.allowsImplicitAnimation = true + + if isTrailingCollapsed { + if splitView.subviews.count >= 2 { + context.coordinator.savedDividerPosition = splitView.subviews[1].frame.width + } + splitView.setPosition(splitView.bounds.width, ofDividerAt: 0) + trailingView.isHidden = true + } else { + trailingView.isHidden = false + splitView.adjustSubviews() + if let saved = context.coordinator.savedDividerPosition { + splitView.setPosition(splitView.bounds.width - saved, ofDividerAt: 0) + } } - splitView.setPosition(splitView.bounds.width, ofDividerAt: 0) - trailingView.isHidden = true - splitView.display() - } else { - trailingView.isHidden = false - splitView.adjustSubviews() - if let saved = context.coordinator.savedDividerPosition { - splitView.setPosition(splitView.bounds.width - saved, ofDividerAt: 0) - } - splitView.display() } } } From 7e20e554d806d951527a0aa156295ae1242a9488 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ng=C3=B4=20Qu=E1=BB=91c=20=C4=90=E1=BA=A1t?= Date: Mon, 6 Apr 2026 14:03:38 +0700 Subject: [PATCH 6/8] fix: restore working collapse/expand for inspector panel split view --- .../Components/HorizontalSplitView.swift | 30 ++++++++----------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/TablePro/Views/Components/HorizontalSplitView.swift b/TablePro/Views/Components/HorizontalSplitView.swift index 8667c802..9662c503 100644 --- a/TablePro/Views/Components/HorizontalSplitView.swift +++ b/TablePro/Views/Components/HorizontalSplitView.swift @@ -59,24 +59,20 @@ struct HorizontalSplitView: NSViewRepresentable { if isTrailingCollapsed != wasCollapsed { context.coordinator.lastCollapsedState = isTrailingCollapsed - NSAnimationContext.runAnimationGroup { ctx in - ctx.duration = 0.2 - ctx.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) - ctx.allowsImplicitAnimation = true - - if isTrailingCollapsed { - if splitView.subviews.count >= 2 { - context.coordinator.savedDividerPosition = splitView.subviews[1].frame.width - } - splitView.setPosition(splitView.bounds.width, ofDividerAt: 0) - trailingView.isHidden = true - } else { - trailingView.isHidden = false - splitView.adjustSubviews() - if let saved = context.coordinator.savedDividerPosition { - splitView.setPosition(splitView.bounds.width - saved, ofDividerAt: 0) - } + if isTrailingCollapsed { + if splitView.subviews.count >= 2 { + context.coordinator.savedDividerPosition = splitView.subviews[1].frame.width } + splitView.setPosition(splitView.bounds.width, ofDividerAt: 0) + trailingView.isHidden = true + splitView.display() + } else { + trailingView.isHidden = false + splitView.adjustSubviews() + if let saved = context.coordinator.savedDividerPosition { + splitView.setPosition(splitView.bounds.width - saved, ofDividerAt: 0) + } + splitView.display() } } } From 5ff471193e0a14e559e2bbe155b3e1e924bc8d3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ng=C3=B4=20Qu=E1=BB=91c=20=C4=90=E1=BA=A1t?= Date: Mon, 6 Apr 2026 14:05:38 +0700 Subject: [PATCH 7/8] fix: use trailingWidth as fallback when expanding inspector for the first time --- TablePro/Views/Components/HorizontalSplitView.swift | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/TablePro/Views/Components/HorizontalSplitView.swift b/TablePro/Views/Components/HorizontalSplitView.swift index 9662c503..bd3c7922 100644 --- a/TablePro/Views/Components/HorizontalSplitView.swift +++ b/TablePro/Views/Components/HorizontalSplitView.swift @@ -68,10 +68,9 @@ struct HorizontalSplitView: NSViewRepresentable { splitView.display() } else { trailingView.isHidden = false + let targetWidth = context.coordinator.savedDividerPosition ?? trailingWidth splitView.adjustSubviews() - if let saved = context.coordinator.savedDividerPosition { - splitView.setPosition(splitView.bounds.width - saved, ofDividerAt: 0) - } + splitView.setPosition(splitView.bounds.width - targetWidth, ofDividerAt: 0) splitView.display() } } From 5ddf1b2bf730f5bab785c270c7ec50134744d206 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ng=C3=B4=20Qu=E1=BB=91c=20=C4=90=E1=BA=A1t?= Date: Mon, 6 Apr 2026 14:15:46 +0700 Subject: [PATCH 8/8] refactor: cleanup code quality issues from review - SearchFieldView: resolve theme font size inside body for reactivity - HorizontalSplitView: refresh binding in updateNSView to prevent stale reference - HorizontalSplitView: add 0.5pt threshold to resize callback to reduce re-renders - HorizontalSplitView: remove redundant splitView.display() calls - ConnectionSwitcherPopover: extract moveSelection helper to avoid repeated allItems computation - ContentView: remove dead saveCurrentSessionState stub --- TablePro/ContentView.swift | 4 ---- .../Components/HorizontalSplitView.swift | 12 +++++------- .../Views/Components/SearchFieldView.swift | 7 ++++--- .../Toolbar/ConnectionSwitcherPopover.swift | 19 +++++++++++-------- 4 files changed, 20 insertions(+), 22 deletions(-) diff --git a/TablePro/ContentView.swift b/TablePro/ContentView.swift index f82e7679..becd2062 100644 --- a/TablePro/ContentView.swift +++ b/TablePro/ContentView.swift @@ -429,10 +429,6 @@ struct ContentView: View { } } - private func saveCurrentSessionState() { - // State is automatically saved through bindings - } - // MARK: - Persistence private func deleteConnection(_ connection: DatabaseConnection) { diff --git a/TablePro/Views/Components/HorizontalSplitView.swift b/TablePro/Views/Components/HorizontalSplitView.swift index bd3c7922..2217fdad 100644 --- a/TablePro/Views/Components/HorizontalSplitView.swift +++ b/TablePro/Views/Components/HorizontalSplitView.swift @@ -51,6 +51,7 @@ struct HorizontalSplitView: NSViewRepresentable { func updateNSView(_ splitView: NSSplitView, context: Context) { context.coordinator.leadingHosting?.rootView = leading context.coordinator.trailingHosting?.rootView = trailing + context.coordinator.trailingWidth = $trailingWidth context.coordinator.minWidth = minTrailingWidth context.coordinator.maxWidth = maxTrailingWidth @@ -65,13 +66,11 @@ struct HorizontalSplitView: NSViewRepresentable { } splitView.setPosition(splitView.bounds.width, ofDividerAt: 0) trailingView.isHidden = true - splitView.display() } else { trailingView.isHidden = false let targetWidth = context.coordinator.savedDividerPosition ?? trailingWidth splitView.adjustSubviews() splitView.setPosition(splitView.bounds.width - targetWidth, ofDividerAt: 0) - splitView.display() } } } @@ -81,8 +80,8 @@ struct HorizontalSplitView: NSViewRepresentable { var trailingHosting: NSHostingView? var lastCollapsedState = false var savedDividerPosition: CGFloat? - var minWidth: CGFloat = 200 - var maxWidth: CGFloat = 600 + var minWidth: CGFloat = 0 + var maxWidth: CGFloat = 0 var trailingWidth: Binding init(trailingWidth: Binding) { @@ -137,9 +136,8 @@ struct HorizontalSplitView: NSViewRepresentable { trailingHosting?.isHidden != true else { return } let width = splitView.subviews[1].frame.width - if width > 0 { - trailingWidth.wrappedValue = width - } + guard width > 0, abs(width - trailingWidth.wrappedValue) > 0.5 else { return } + trailingWidth.wrappedValue = width } } } diff --git a/TablePro/Views/Components/SearchFieldView.swift b/TablePro/Views/Components/SearchFieldView.swift index 5128edd7..5b9770e4 100644 --- a/TablePro/Views/Components/SearchFieldView.swift +++ b/TablePro/Views/Components/SearchFieldView.swift @@ -8,17 +8,18 @@ import SwiftUI struct SearchFieldView: View { let placeholder: String @Binding var text: String - var fontSize: CGFloat = ThemeEngine.shared.activeTheme.typography.body + var fontSize: CGFloat? var body: some View { + let resolvedSize = fontSize ?? ThemeEngine.shared.activeTheme.typography.body HStack(spacing: 6) { Image(systemName: "magnifyingglass") - .font(.system(size: fontSize)) + .font(.system(size: resolvedSize)) .foregroundStyle(.tertiary) TextField(placeholder, text: $text) .textFieldStyle(.plain) - .font(.system(size: fontSize)) + .font(.system(size: resolvedSize)) if !text.isEmpty { Button { text = "" } label: { diff --git a/TablePro/Views/Toolbar/ConnectionSwitcherPopover.swift b/TablePro/Views/Toolbar/ConnectionSwitcherPopover.swift index cce0b2d8..3ccc08e2 100644 --- a/TablePro/Views/Toolbar/ConnectionSwitcherPopover.swift +++ b/TablePro/Views/Toolbar/ConnectionSwitcherPopover.swift @@ -168,22 +168,18 @@ struct ConnectionSwitcherPopover: View { return .handled } .onKeyPress(.upArrow) { - if selectedIndex > 0 { selectedIndex -= 1 } - return .handled + moveSelection(by: -1) } .onKeyPress(.downArrow) { - if selectedIndex < allItems.count - 1 { selectedIndex += 1 } - return .handled + moveSelection(by: 1) } .onKeyPress(characters: .init(charactersIn: "j"), phases: [.down, .repeat]) { keyPress in guard keyPress.modifiers.contains(.control) else { return .ignored } - if selectedIndex < allItems.count - 1 { selectedIndex += 1 } - return .handled + return moveSelection(by: 1) } .onKeyPress(characters: .init(charactersIn: "k"), phases: [.down, .repeat]) { keyPress in guard keyPress.modifiers.contains(.control) else { return .ignored } - if selectedIndex > 0 { selectedIndex -= 1 } - return .handled + return moveSelection(by: -1) } } @@ -194,6 +190,13 @@ struct ConnectionSwitcherPopover: View { case saved(DatabaseConnection) } + private func moveSelection(by offset: Int) -> KeyPress.Result { + let newIndex = selectedIndex + offset + guard newIndex >= 0, newIndex < allItems.count else { return .handled } + selectedIndex = newIndex + return .handled + } + // MARK: - Subviews private func connectionRow(