Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .claude/rules/theme.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ paths:
- **Chrome never reads a slot.** It asks `ThemeBackdropState.isThemed(in:scope:)` or `isThemed(highContrast:scope:)` whether the backdrop behind it is themed, and answers with a system style:
- A bar over a pane takes `.paneBarBackground(_:ignoresSafeAreaEdges:)` with the native style it drew before (`.bar` by default, or a flat `controlBackgroundColor` or `windowBackgroundColor`). It draws that style while the backdrop is unthemed and nothing while it is themed, because a material over a themed pane reads as a system band. No edges by default, and a top bar keeps that default, so it never reaches under the toolbar.
- A bottom status bar takes `.statusBarChrome()`. `StatusBarChrome` asks `ThemeBackdropState` itself and drops its material over a themed backdrop, so a host sets nothing.
- A list or scroll view filling a glass sidebar or inspector item takes `.splitItemGlassBackground()`. From macOS 26 it hides the list's own content-background material, which never follows the window color, so the item's glass shows through and the pane matches the sidebar; before macOS 26 the list keeps its background. The inspector field list and the agent's proposed statements take it.
- The editor tab strip track draws `secondarySystemFill` over a themed backdrop on macOS 26 and later, where the track sits on the theme's window color, and its unthemed fill otherwise. Never a palette color.
- **A code field takes the editor theme wherever it appears.** Startup Commands, Pre-Connect Script, a favorite's query, Rewind, Agent SQL, the deep-link Startup SQL and the tunnel command preview wear `editor.*`; the form or sheet around them stays system.
- **The slot family follows what the surface shows.** Code, values, value trees and wells: `editor.*`, with tree tones from `editor.syntax.*`. Records, schema objects, diagram nodes, chart plots and states that stand in for a grid: `dataGrid.*`. Diff rows: `dataGrid.inserted`, `modified` and `deleted` over the editor background and text. A region behind panes: `window.background`. Identity, severity and status hues stay system.
Expand Down Expand Up @@ -52,7 +53,7 @@ paths:
- **The four content windows are `ContentWindow`s**: main (`TabWindowController`), Data File, Compare & Sync and the value viewers. Each holds a `ThemeWindowBackdrop`, which on macOS 26 and later sets `NSWindow.backgroundColor` to `window.background` only while the theme sets it and puts the window's own color back when it stops; the sidebar, inspector and toolbar glass take their tone from it. Before macOS 26 the window keeps its own color. `ThemeWindowBackdrop.canReachChrome` is that gate; the titlebar policy, `DetailPaneBackdrop.extendsUnderToolbar` and `EditorTabStripPalette.followsWindowBackdrop` read it rather than an `#available` of their own.
- **One titlebar policy, in `ContentWindow`.** On macOS 26 and later the titlebar is transparent, so the backdrop reaches the toolbar; in full screen with the toolbar hidden it keeps its background, since it then slides down over the content. Before macOS 26 it stays opaque: a transparent titlebar there swaps its material for a flat fill, which fills chrome. A new content window subclasses `ContentWindow`. Toolbar separators stay each window's own.
- **The detail column sits on `window.background` on every macOS version.** `MainSplitViewController` installs `DetailColumnBackdrop` under the detail panes through `WorkspacePaneHost.installBackdrop(_:)`, so Connecting, Unavailable, the agent and every other state get it. Before macOS 26 it stops below the toolbar, which keeps the system color. The backdrop passes clicks through. `DetailPaneBackdropFill` goes only there; a pane that draws nothing shows it. The Compare & Sync and Data File roots fill with `.themeBackground(.windowBackdrop, native:)`.
- **Never fill chrome.** No fill on the sidebar, inspector frame, toolbar or titlebar, and no `.tint` with a theme color. AppKit keeps its own selection unless the theme sets the selection slot. SwiftLint's `pane_hidden_background` bans `.scrollContentBackground(.hidden)` and `.listRowBackground(` in app code, and `window_background_outside_backdrop` bans a window color write outside `ThemeWindowBackdrop`.
- **Never fill chrome.** No fill on the sidebar, inspector frame, toolbar or titlebar, and no `.tint` with a theme color. AppKit keeps its own selection unless the theme sets the selection slot. SwiftLint's `pane_hidden_background` bans `.scrollContentBackground(.hidden)` and `.listRowBackground(` in app code outside `SplitItemGlassBackground`, and `window_background_outside_backdrop` bans a window color write outside `ThemeWindowBackdrop`.

## Engine and catalog

Expand Down
4 changes: 2 additions & 2 deletions apple/.swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -397,9 +397,9 @@ custom_rules:
pane_hidden_background:
name: "Pane Keeps Its Background"
regex: '\.scrollContentBackground\(\s*\.hidden\s*\)|\b[Tt]hemedContent|\.listRowBackground\('
excluded: ['.*/(Packages|Plugins)/']
excluded: ['.*/(Packages|Plugins)/', '.*/TablePro/Theme/SplitItemGlassBackground\.swift$']
excluded_match_kinds: [comment, doccomment]
message: "Let the List, Form or ScrollView draw its own background and rows."
message: "Let the List, Form or ScrollView draw its own background and rows. Inside a glass sidebar or inspector, use .splitItemGlassBackground()."
severity: error
window_background_outside_backdrop:
name: "Window Background Outside The Backdrop"
Expand Down
26 changes: 26 additions & 0 deletions apple/TablePro/Theme/SplitItemGlassBackground.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import SwiftUI

/// From macOS 26 a sidebar or inspector split item is glass, which takes its tone from the window
/// color. A `List` puts its own content-background material over that glass, and that material
/// never follows the window color, so it reads as a second surface inside the pane. Apple's
/// guidance for the new design is to remove such effect views so the glass shows through.
///
/// The condition is fixed for the process: SwiftUI never puts the material back when a list's
/// scroll background goes from hidden to automatic at runtime, measured.
private struct SplitItemGlassBackground: ViewModifier {
@ViewBuilder
func body(content: Content) -> some View {
if ThemeWindowBackdrop.canReachChrome {
content.scrollContentBackground(.hidden)
} else {
content
}
}
}

internal extension View {
/// For a list or scroll view filling a sidebar or inspector split item.
func splitItemGlassBackground() -> some View {
modifier(SplitItemGlassBackground())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ internal struct AgentProposedStatementsView: View {
}
}
.listStyle(.inset)
.splitItemGlassBackground()
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ internal struct InspectorFieldListView: View {
/// `listRowInsets` is consulted, so the pane's content sat at 24pt while its own header sat
/// at 10. `.plain` lands at half of `intercellSpacing` and can be corrected onto the edge.
.listStyle(.plain)
.splitItemGlassBackground()
.modifier(InspectorFieldKeyShortcuts(
moveFocus: { forward in moveFocus(within: fields, forward: forward) },
applyStateShortcut: applyStateShortcut
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import AppKit
import SwiftUI
@testable import TablePro
import Testing

// A list's content-background material never takes the window color, so inside the glass
// inspector it drew a second, untinted surface behind the fields.
@MainActor
struct SplitItemGlassBackgroundTests {
private static let size = NSSize(width: 280, height: 360)

private static func contentBackgrounds(in view: NSView?) -> [NSVisualEffectView] {
guard let view else { return [] }
let own = (view as? NSVisualEffectView).map { $0.material == .contentBackground ? [$0] : [] } ?? []
return own + view.subviews.flatMap { contentBackgrounds(in: $0) }
}

private func drawsListMaterial(_ view: some View) -> Bool {
let window = ThemeTestFixture.host(view, size: Self.size)
defer { window.orderOut(nil) }
return !Self.contentBackgrounds(in: window.contentView).isEmpty
}

private func fieldList() -> InspectorFieldListView {
let state = MultiRowEditState()
state.configure(
selectedRowIndices: [0],
rowIDs: [.existing(0)],
allRows: [["1", "Still"]],
absentCells: [[]],
columns: ["id", "name"],
columnTypes: [.integer(rawType: "INTEGER"), .text(rawType: "TEXT")],
externallyModifiedColumns: []
)
return InspectorFieldListView(editState: state, isEditable: true, databaseType: .sqlite, userDefinedTypeScope: nil)
}

@Test("A plain list keeps its material where split items are not glass and drops it where they are")
func modifierFollowsTheGlassRelease() {
let list = List { Text(verbatim: "row") }.listStyle(.plain)
#expect(drawsListMaterial(list))
#expect(drawsListMaterial(list.splitItemGlassBackground()) == !ThemeWindowBackdrop.canReachChrome)
}

@Test("The inspector's field list lets the inspector glass show through on macOS 26 and later")
func inspectorFieldListShowsTheGlass() {
#expect(drawsListMaterial(fieldList()) == !ThemeWindowBackdrop.canReachChrome)
}

@Test("The agent's proposed statements let the trailing pane's glass show through on macOS 26 and later")
func proposedStatementsShowTheGlass() {
let statements = [ProposedStatement(id: "1", sql: "SELECT 1", toolName: "run_query", state: .ran)]
#expect(drawsListMaterial(AgentProposedStatementsView(statements: statements)) == !ThemeWindowBackdrop.canReachChrome)
}
}
1 change: 1 addition & 0 deletions apple/changelog.d/3590.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The row inspector's field area drew a gray surface that ignored the theme instead of the inspector's own background.
Loading