Skip to content

Commit 41144e4

Browse files
committed
refactor: extract InlineErrorBanner into shared component
- Create Views/Shared/InlineErrorBanner.swift with reusable error banner - Remove duplicated errorBanner code from MainEditorContentView (~50 lines) - Remove duplicated errorBanner code from TableTabContentView (~42 lines) - Add smooth dismiss animation (.easeInOut 0.2s) - Fix misplaced dismissError() in DiscardAction enum that broke build
1 parent 3f91670 commit 41144e4

4 files changed

Lines changed: 102 additions & 106 deletions

File tree

TablePro/Views/Main/Child/MainEditorContentView.swift

Lines changed: 8 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -184,9 +184,13 @@ struct MainEditorContentView: View {
184184
VStack(spacing: 0) {
185185
// Error banner (if query failed)
186186
if let errorMessage = tab.errorMessage, !errorMessage.isEmpty {
187-
errorBanner(errorMessage)
187+
InlineErrorBanner(message: errorMessage) {
188+
if let index = tabManager.selectedTabIndex {
189+
tabManager.tabs[index].errorMessage = nil
190+
}
191+
}
188192
}
189-
193+
190194
if tab.showStructure, let tableName = tab.tableName {
191195
TableStructureView(tableName: tableName, connection: connection)
192196
.frame(maxHeight: .infinity)
@@ -218,6 +222,7 @@ struct MainEditorContentView: View {
218222
}
219223
.frame(minHeight: 150)
220224
.animation(.easeInOut(duration: 0.2), value: filterStateManager.isVisible)
225+
.animation(.easeInOut(duration: 0.2), value: tab.errorMessage)
221226
}
222227

223228
@ViewBuilder
@@ -293,7 +298,7 @@ struct MainEditorContentView: View {
293298
onLastPage: onLastPage,
294299
onLimitChange: onLimitChange,
295300
onOffsetChange: onOffsetChange,
296-
onPaginationGo: onPaginationGo
301+
onPaginationGo: onPaginationGo,
297302
)
298303
}
299304

@@ -329,57 +334,3 @@ struct MainEditorContentView: View {
329334
.frame(maxWidth: .infinity, maxHeight: .infinity)
330335
}
331336
}
332-
333-
// MARK: - Error Banner
334-
335-
extension MainEditorContentView {
336-
@ViewBuilder
337-
private func errorBanner(_ message: String) -> some View {
338-
HStack(alignment: .top, spacing: 10) {
339-
// Native macOS error icon
340-
Image(systemName: "exclamationmark.circle.fill")
341-
.foregroundStyle(DesignConstants.Colors.error)
342-
.font(.system(size: 16))
343-
.symbolRenderingMode(.multicolor)
344-
345-
VStack(alignment: .leading, spacing: 3) {
346-
Text(message)
347-
.font(.system(size: 12))
348-
.foregroundStyle(.primary)
349-
.textSelection(.enabled)
350-
.fixedSize(horizontal: false, vertical: true)
351-
}
352-
353-
Spacer(minLength: 8)
354-
355-
// Dismiss button
356-
Button(action: {
357-
Task { @MainActor in
358-
if let index = tabManager.selectedTabIndex {
359-
tabManager.tabs[index].errorMessage = nil
360-
}
361-
}
362-
}) {
363-
Image(systemName: "xmark")
364-
.font(.system(size: 11, weight: .medium))
365-
.foregroundStyle(.secondary)
366-
}
367-
.buttonStyle(.plain)
368-
.help("Dismiss")
369-
.opacity(0.6)
370-
}
371-
.padding(.horizontal, 12)
372-
.padding(.vertical, 10)
373-
.background(
374-
RoundedRectangle(cornerRadius: 6)
375-
.fill(Color(nsColor: .controlBackgroundColor))
376-
.shadow(color: .black.opacity(0.1), radius: 1, x: 0, y: 0.5)
377-
)
378-
.overlay(
379-
RoundedRectangle(cornerRadius: 6)
380-
.strokeBorder(Color(nsColor: .separatorColor), lineWidth: 0.5)
381-
)
382-
.padding(.horizontal, 12)
383-
.padding(.vertical, 8)
384-
}
385-
}

TablePro/Views/Main/Child/TableTabContentView.swift

Lines changed: 4 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ struct TableTabContentView: View {
3737
let onLimitChange: (Int) -> Void
3838
let onOffsetChange: (Int) -> Void
3939
let onPaginationGo: () -> Void
40+
let onDismissError: () -> Void
4041

4142
@Binding var sortState: SortState
4243
@Binding var showStructure: Bool
@@ -45,9 +46,9 @@ struct TableTabContentView: View {
4546
VStack(spacing: 0) {
4647
// Error banner (if query failed)
4748
if let errorMessage = tab.errorMessage, !errorMessage.isEmpty {
48-
errorBanner(errorMessage)
49+
InlineErrorBanner(message: errorMessage, onDismiss: onDismissError)
4950
}
50-
51+
5152
// Show structure view or data view based on toggle
5253
if showStructure, let tableName = tab.tableName {
5354
TableStructureView(tableName: tableName, connection: connection)
@@ -105,52 +106,6 @@ struct TableTabContentView: View {
105106
onPaginationGo: onPaginationGo
106107
)
107108
}
108-
}
109-
110-
// MARK: - Error Banner
111-
112-
private func errorBanner(_ message: String) -> some View {
113-
HStack(alignment: .top, spacing: 10) {
114-
// Native macOS error icon
115-
Image(systemName: "exclamationmark.circle.fill")
116-
.foregroundStyle(DesignConstants.Colors.error)
117-
.font(.system(size: 16))
118-
.symbolRenderingMode(.multicolor)
119-
120-
VStack(alignment: .leading, spacing: 3) {
121-
Text(message)
122-
.font(.system(size: 12))
123-
.foregroundStyle(.primary)
124-
.textSelection(.enabled)
125-
.fixedSize(horizontal: false, vertical: true)
126-
}
127-
128-
Spacer(minLength: 8)
129-
130-
// Dismiss button - needs to be wired to coordinator
131-
Button(action: {
132-
tab.errorMessage = nil
133-
}) {
134-
Image(systemName: "xmark")
135-
.font(.system(size: 11, weight: .medium))
136-
.foregroundStyle(.secondary)
137-
}
138-
.buttonStyle(.plain)
139-
.help("Dismiss")
140-
.opacity(0.6)
141-
}
142-
.padding(.horizontal, 12)
143-
.padding(.vertical, 10)
144-
.background(
145-
RoundedRectangle(cornerRadius: 6)
146-
.fill(Color(nsColor: .controlBackgroundColor))
147-
.shadow(color: .black.opacity(0.1), radius: 1, x: 0, y: 0.5)
148-
)
149-
.overlay(
150-
RoundedRectangle(cornerRadius: 6)
151-
.strokeBorder(Color(nsColor: .separatorColor), lineWidth: 0.5)
152-
)
153-
.padding(.horizontal, 12)
154-
.padding(.vertical, 8)
109+
.animation(.easeInOut(duration: 0.2), value: tab.errorMessage)
155110
}
156111
}

TablePro/Views/Main/MainContentCoordinator.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1459,3 +1459,4 @@ final class MainContentCoordinator: ObservableObject {
14591459
}
14601460
}
14611461
}
1462+
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
//
2+
// InlineErrorBanner.swift
3+
// TablePro
4+
//
5+
// Native macOS-style inline error banner following Apple Human Interface Guidelines.
6+
// Replaces blocking alert dialogs with non-blocking inline notifications.
7+
//
8+
9+
import SwiftUI
10+
11+
/// Native macOS-style inline error banner
12+
///
13+
/// Design follows Apple HIG:
14+
/// - Icon: `exclamationmark.circle.fill` with multicolor rendering
15+
/// - Background: System `controlBackgroundColor` (adapts to light/dark mode)
16+
/// - Border: 0.5px hairline using `separatorColor`
17+
/// - Corners: 6px rounded (macOS standard)
18+
/// - Text: 12pt in `.primary` color for readability
19+
struct InlineErrorBanner: View {
20+
let message: String
21+
let onDismiss: () -> Void
22+
23+
var body: some View {
24+
HStack(alignment: .top, spacing: 10) {
25+
// Native macOS error icon
26+
Image(systemName: "exclamationmark.circle.fill")
27+
.foregroundStyle(.red)
28+
.font(.system(size: 16))
29+
.symbolRenderingMode(.multicolor)
30+
31+
VStack(alignment: .leading, spacing: 3) {
32+
Text(message)
33+
.font(.system(size: 12))
34+
.foregroundStyle(.primary)
35+
.textSelection(.enabled)
36+
.fixedSize(horizontal: false, vertical: true)
37+
}
38+
39+
Spacer(minLength: 8)
40+
41+
// Dismiss button
42+
Button(action: onDismiss) {
43+
Image(systemName: "xmark")
44+
.font(.system(size: 11, weight: .medium))
45+
.foregroundStyle(.secondary)
46+
}
47+
.buttonStyle(.plain)
48+
.help("Dismiss")
49+
.opacity(0.6)
50+
}
51+
.padding(.horizontal, 12)
52+
.padding(.vertical, 10)
53+
.background(
54+
RoundedRectangle(cornerRadius: 6)
55+
.fill(Color(nsColor: .controlBackgroundColor))
56+
.shadow(color: .black.opacity(0.1), radius: 1, x: 0, y: 0.5)
57+
)
58+
.overlay(
59+
RoundedRectangle(cornerRadius: 6)
60+
.strokeBorder(Color(nsColor: .separatorColor), lineWidth: 0.5)
61+
)
62+
.padding(.horizontal, 12)
63+
.padding(.vertical, 8)
64+
.transition(.move(edge: .top).combined(with: .opacity))
65+
}
66+
}
67+
68+
#Preview("Light Mode") {
69+
VStack {
70+
InlineErrorBanner(
71+
message: "Table 'users' doesn't exist",
72+
onDismiss: {}
73+
)
74+
Spacer()
75+
}
76+
.frame(width: 500, height: 200)
77+
}
78+
79+
#Preview("Dark Mode") {
80+
VStack {
81+
InlineErrorBanner(
82+
message: "You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'SELEC' at line 1",
83+
onDismiss: {}
84+
)
85+
Spacer()
86+
}
87+
.frame(width: 500, height: 200)
88+
.preferredColorScheme(.dark)
89+
}

0 commit comments

Comments
 (0)