@@ -25,6 +25,7 @@ struct QueryEditorView: View {
2525 let connectionId : UUID
2626 let historyStorage : QueryHistoryStorage
2727 @State private var showHistory = false
28+ @State private var showClearHistoryConfirmation = false
2829 @FocusState private var editorFocused : Bool
2930
3031 var body : some View {
@@ -134,58 +135,70 @@ struct QueryEditorView: View {
134135 }
135136 }
136137
137- // Native iOS pattern: List with rows, each row shows column:value pairs
138138 private func resultList( _ result: QueryResult ) -> some View {
139139 List {
140140 ForEach ( Array ( result. rows. enumerated ( ) ) , id: \. offset) { rowIndex, row in
141- Section {
142- ForEach ( Array ( result. columns. enumerated ( ) ) , id: \. offset) { colIndex, col in
143- let value = colIndex < row. count ? row [ colIndex] : nil
144- HStack ( alignment: . top) {
145- Text ( verbatim: col. name)
146- . font ( . caption)
147- . foregroundStyle ( . secondary)
148- . frame ( width: 90 , alignment: . leading)
149- Text ( verbatim: value ?? " NULL " )
150- . font ( . system( . body, design: . monospaced) )
151- . foregroundStyle ( value == nil ? . secondary : . primary)
152- . frame ( maxWidth: . infinity, alignment: . leading)
153- . lineLimit ( 3 )
154- . textSelection ( . enabled)
155- }
156- . contextMenu {
157- if let value {
158- Button {
159- UIPasteboard . general. string = value
160- } label: {
161- Label ( " Copy Value " , systemImage: " doc.on.doc " )
162- }
163- }
164- Button {
165- UIPasteboard . general. string = col. name
166- } label: {
167- Label ( " Copy Column Name " , systemImage: " textformat " )
168- }
169- Divider ( )
170- Menu ( " Copy Row " ) {
171- ForEach ( ExportFormat . allCases) { format in
172- Button ( format. rawValue) {
173- let text = ClipboardExporter . exportRow (
174- columns: result. columns, row: row,
175- format: format
176- )
177- ClipboardExporter . copyToClipboard ( text)
178- }
179- }
180- }
181- }
182- }
183- } header: {
184- Text ( verbatim: " Row \( rowIndex + 1 ) " )
141+ NavigationLink {
142+ RowDetailView (
143+ columns: result. columns,
144+ rows: result. rows,
145+ initialIndex: rowIndex
146+ )
147+ } label: {
148+ resultRowCard ( columns: result. columns, row: row)
149+ }
150+ . contextMenu {
151+ resultRowContextMenu ( columns: result. columns, row: row)
152+ }
153+ }
154+ }
155+ . listStyle ( . plain)
156+ }
157+
158+ private func resultRowCard( columns: [ ColumnInfo ] , row: [ String ? ] ) -> some View {
159+ let preview = Array ( zip ( columns, row) . prefix ( 4 ) )
160+ return VStack ( alignment: . leading, spacing: 4 ) {
161+ ForEach ( Array ( preview. enumerated ( ) ) , id: \. offset) { index, pair in
162+ HStack ( spacing: 6 ) {
163+ Text ( pair. 0 . name)
164+ . font ( . caption2)
165+ . foregroundStyle ( . tertiary)
166+ Text ( verbatim: pair. 1 ?? " NULL " )
167+ . font ( index == 0 ? . subheadline : . caption)
168+ . fontWeight ( index == 0 ? . medium : . regular)
169+ . foregroundStyle ( pair. 1 == nil ? . secondary : . primary)
170+ . lineLimit ( 1 )
171+ }
172+ }
173+ if columns. count > 4 {
174+ Text ( " + \( columns. count - 4 ) more columns " )
175+ . font ( . caption2)
176+ . foregroundStyle ( . quaternary)
177+ }
178+ }
179+ . padding ( . vertical, 2 )
180+ }
181+
182+ @ViewBuilder
183+ private func resultRowContextMenu( columns: [ ColumnInfo ] , row: [ String ? ] ) -> some View {
184+ if let firstValue = row. first, let value = firstValue {
185+ Button {
186+ UIPasteboard . general. string = value
187+ } label: {
188+ Label ( " Copy Value " , systemImage: " doc.on.doc " )
189+ }
190+ }
191+ Menu ( " Copy Row " ) {
192+ ForEach ( ExportFormat . allCases) { format in
193+ Button ( format. rawValue) {
194+ let text = ClipboardExporter . exportRow (
195+ columns: columns, row: row,
196+ format: format
197+ )
198+ ClipboardExporter . copyToClipboard ( text)
185199 }
186200 }
187201 }
188- . listStyle ( . insetGrouped)
189202 }
190203
191204 // MARK: - Toolbar
@@ -287,23 +300,29 @@ struct QueryEditorView: View {
287300 }
288301 queryHistory = historyStorage. load ( for: connectionId)
289302 }
303+
304+ if !queryHistory. isEmpty {
305+ Section {
306+ Button ( " Clear All History " , role: . destructive) {
307+ showClearHistoryConfirmation = true
308+ }
309+ }
310+ }
290311 }
291312 . listStyle ( . insetGrouped)
292313 . navigationTitle ( " Query History " )
293314 . navigationBarTitleDisplayMode ( . inline)
294315 . toolbar {
295- ToolbarItem ( placement: . cancellationAction) {
296- if !queryHistory. isEmpty {
297- Button ( " Clear All " , role: . destructive) {
298- historyStorage. clearAll ( for: connectionId)
299- queryHistory = [ ]
300- }
301- }
302- }
303316 ToolbarItem ( placement: . confirmationAction) {
304317 Button ( " Done " ) { showHistory = false }
305318 }
306319 }
320+ . confirmationDialog ( " Clear History " , isPresented: $showClearHistoryConfirmation) {
321+ Button ( " Clear All " , role: . destructive) {
322+ historyStorage. clearAll ( for: connectionId)
323+ queryHistory = [ ]
324+ }
325+ }
307326 . overlay {
308327 if queryHistory. isEmpty {
309328 ContentUnavailableView (
0 commit comments