Fix transcript prominent button contrast
This commit is contained in:
1 parent
aa3c6498de
commit
e2feadc201
5 files changed
+163
-12
No files matched your search
@@ -337,6 +337,9 @@ enum TermindColor {
|
||||
static var selectedForeground: Color {
|
||||
Color(nsColor: .alternateSelectedControlTextColor)
|
||||
}
|
||||
static var transcriptProminentForeground: Color {
|
||||
Color(nsColor: nativeTranscriptProminentForeground)
|
||||
}
|
||||
static var separator: Color { Color(nsColor: .separatorColor) }
|
||||
static var shadow: Color { Color(nsColor: .shadowColor) }
|
||||
|
||||
@@ -348,7 +351,15 @@ enum TermindColor {
|
||||
static var danger: Color { Color(nsColor: .systemRed) }
|
||||
|
||||
static var nativeAccent: NSColor { .controlAccentColor }
|
||||
static var nativeSelectedForeground: NSColor { .alternateSelectedControlTextColor }
|
||||
static var nativeTranscriptProminentForeground: NSColor {
|
||||
NSColor(name: nil) { appearance in
|
||||
resolvedTranscriptProminentForeground(for: appearance)
|
||||
}
|
||||
}
|
||||
|
||||
static func resolvedTranscriptProminentForeground(for appearance: NSAppearance) -> NSColor {
|
||||
appearance.bestMatch(from: [.aqua, .darkAqua]) == .darkAqua ? .black : .white
|
||||
}
|
||||
|
||||
/// CALayer stores a concrete `CGColor`; assigning a named dynamic NSColor
|
||||
/// directly can resolve it against the wrong appearance (or to clear while
|
||||
|
||||
@@ -7,6 +7,13 @@ import TermindRemoteInfrastructure
|
||||
|
||||
import struct TermindDomain.Host
|
||||
|
||||
extension View {
|
||||
fileprivate func transcriptProminentButtonStyle() -> some View {
|
||||
buttonStyle(.borderedProminent)
|
||||
.foregroundStyle(TermindColor.transcriptProminentForeground)
|
||||
}
|
||||
}
|
||||
|
||||
private struct ConversationAttachmentDropModifier: ViewModifier {
|
||||
let conversation: ConversationModel
|
||||
let isEnabled: Bool
|
||||
@@ -198,7 +205,7 @@ struct ConversationView: View {
|
||||
Button("Unarchive") {
|
||||
window.setAssistantArchived(conversation.id, archived: false)
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.transcriptProminentButtonStyle()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -572,7 +579,7 @@ private struct AgentApprovalCard: View {
|
||||
Button("Allow Once") {
|
||||
conversation.respondToApproval(request.id, allow: true)
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.transcriptProminentButtonStyle()
|
||||
.accessibilityIdentifier("AssistantApproval.AllowOnce")
|
||||
}
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
@@ -772,7 +779,7 @@ private struct AgentUserInputCard: View {
|
||||
Button(isLastQuestion ? "Submit" : "Next") {
|
||||
conversation.advanceUserInputQuestion()
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.transcriptProminentButtonStyle()
|
||||
.keyboardShortcut(.defaultAction)
|
||||
.disabled(!conversation.canAdvanceUserInput)
|
||||
}
|
||||
|
||||
@@ -1057,6 +1057,7 @@ private enum NativeTranscriptFactory {
|
||||
bordered: true,
|
||||
prominent: true
|
||||
)
|
||||
implement.identifier = NSUserInterfaceItemIdentifier("Transcript.PlanImplement")
|
||||
implement.isEnabled = !context.configuration.isConversationStreaming
|
||||
implement.handler = { context.actions.implementPlan(card) }
|
||||
actions.addArrangedSubview(discard)
|
||||
@@ -1698,6 +1699,7 @@ private final class NativeMarkdownView: NSView {
|
||||
bordered: true,
|
||||
prominent: true
|
||||
)
|
||||
run.identifier = NSUserInterfaceItemIdentifier("Transcript.MarkdownShellRun")
|
||||
run.isEnabled = actions.enabled
|
||||
run.bezelColor = assessment.risk == .destructive ? .systemRed : .controlAccentColor
|
||||
run.handler = { [weak run] in
|
||||
@@ -2485,6 +2487,7 @@ private final class NativeMessageEditor: NSView, NSTextViewDelegate {
|
||||
cancel.contentTintColor = .secondaryLabelColor
|
||||
cancel.handler = { [weak self] in self?.onCancel() }
|
||||
let send = NativeButton(title: String(localized: "Send"), bordered: true, prominent: true)
|
||||
send.identifier = NSUserInterfaceItemIdentifier("Transcript.UserEditorSend")
|
||||
send.handler = { [weak self] in
|
||||
guard let self else { return }
|
||||
onCommit(textView.string)
|
||||
@@ -2805,7 +2808,10 @@ private final class NativeCard: NSView {
|
||||
@MainActor
|
||||
private class NativeButton: NSButton {
|
||||
var handler: (() -> Void)?
|
||||
private let usesProminentStyle: Bool
|
||||
|
||||
init(title: String = "", bordered: Bool = false, prominent: Bool = false) {
|
||||
usesProminentStyle = prominent
|
||||
super.init(frame: .zero)
|
||||
self.title = title
|
||||
bezelStyle = bordered ? .rounded : .inline
|
||||
@@ -2813,14 +2819,7 @@ private class NativeButton: NSButton {
|
||||
controlSize = .small
|
||||
if prominent {
|
||||
bezelColor = .controlAccentColor
|
||||
contentTintColor = TermindColor.nativeSelectedForeground
|
||||
attributedTitle = NSAttributedString(
|
||||
string: title,
|
||||
attributes: [
|
||||
.font: font ?? NSFont.systemFont(ofSize: NSFont.systemFontSize),
|
||||
.foregroundColor: TermindColor.nativeSelectedForeground,
|
||||
]
|
||||
)
|
||||
updateProminentForeground()
|
||||
}
|
||||
target = self
|
||||
action = #selector(run)
|
||||
@@ -2836,6 +2835,25 @@ private class NativeButton: NSButton {
|
||||
}
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) { fatalError() }
|
||||
|
||||
override func viewDidChangeEffectiveAppearance() {
|
||||
super.viewDidChangeEffectiveAppearance()
|
||||
if usesProminentStyle { updateProminentForeground() }
|
||||
}
|
||||
|
||||
private func updateProminentForeground() {
|
||||
let foreground = TermindColor.resolvedTranscriptProminentForeground(
|
||||
for: effectiveAppearance
|
||||
)
|
||||
let attributes: [NSAttributedString.Key: Any] = [
|
||||
.font: font ?? NSFont.systemFont(ofSize: NSFont.systemFontSize),
|
||||
.foregroundColor: foreground,
|
||||
]
|
||||
contentTintColor = foreground
|
||||
attributedTitle = NSAttributedString(string: title, attributes: attributes)
|
||||
attributedAlternateTitle = NSAttributedString(string: title, attributes: attributes)
|
||||
}
|
||||
|
||||
@objc private func run() { handler?() }
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,26 @@ import Testing
|
||||
|
||||
@MainActor
|
||||
struct AppAppearanceTests {
|
||||
@Test
|
||||
func transcriptProminentForegroundIsWhiteInAquaAndBlackInDarkAqua() throws {
|
||||
for (appearanceName, expectedWhite) in [
|
||||
(NSAppearance.Name.aqua, true),
|
||||
(.darkAqua, false),
|
||||
] {
|
||||
let appearance = try #require(NSAppearance(named: appearanceName))
|
||||
let color = try #require(
|
||||
TermindColor.resolvedTranscriptProminentForeground(for: appearance)
|
||||
.usingColorSpace(.deviceRGB)
|
||||
)
|
||||
let expected: CGFloat = expectedWhite ? 1 : 0
|
||||
|
||||
#expect(abs(color.redComponent - expected) < 0.001)
|
||||
#expect(abs(color.greenComponent - expected) < 0.001)
|
||||
#expect(abs(color.blueComponent - expected) < 0.001)
|
||||
#expect(abs(color.alphaComponent - 1) < 0.001)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func missingAndInvalidPreferencesFollowSystem() throws {
|
||||
try withDefaults { defaults in
|
||||
|
||||
@@ -648,6 +648,77 @@ struct TranscriptListViewBridgeTests {
|
||||
#expect(run.isBordered)
|
||||
}
|
||||
|
||||
@Test
|
||||
func nativeTranscriptProminentButtonsInvertTheirForegroundWithAppearance() throws {
|
||||
let coordinator = TranscriptListView.Coordinator()
|
||||
let listView = coordinator.makeListView()
|
||||
let presentation = TranscriptPresentationState()
|
||||
let userID = UUID()
|
||||
let plan = AgentPlan(
|
||||
title: "Implementation plan",
|
||||
body: "Apply the requested change.",
|
||||
scratchPath: "/tmp/plan.md"
|
||||
)
|
||||
let commandID = UUID()
|
||||
let markdownID = UUID()
|
||||
listView.appearance = NSAppearance(named: .aqua)
|
||||
listView.frame = CGRect(x: 0, y: 0, width: 520, height: 2_000)
|
||||
presentation.beginEditingMessage(userID, text: "Edited message")
|
||||
|
||||
update(
|
||||
coordinator,
|
||||
listView,
|
||||
entries: [
|
||||
user(id: userID, text: "Original message"),
|
||||
TranscriptEntry(
|
||||
id: plan.id,
|
||||
content: .plan(AgentPlanCard(plan: plan))
|
||||
),
|
||||
TranscriptEntry(
|
||||
id: commandID,
|
||||
content: .command(
|
||||
SuggestedCommand(
|
||||
id: commandID,
|
||||
command: "pwd",
|
||||
explanation: "Show the current directory."
|
||||
)
|
||||
)
|
||||
),
|
||||
assistant(
|
||||
id: markdownID,
|
||||
text: "```bash\nrm -rf /tmp/termind-prominent-button-test\n```"
|
||||
),
|
||||
],
|
||||
presentation: presentation,
|
||||
isCompact: false,
|
||||
showsTerminalActions: true,
|
||||
actions: .init(canDeliverCommand: { true })
|
||||
)
|
||||
listView.layoutSubtreeIfNeeded()
|
||||
|
||||
let buttonLocations = [
|
||||
(userID, "Transcript.UserEditorSend"),
|
||||
(plan.id, "Transcript.PlanImplement"),
|
||||
(commandID, "Transcript.CommandCardRun"),
|
||||
(markdownID, "Transcript.MarkdownShellRun"),
|
||||
]
|
||||
let buttons = try buttonLocations.map { rowID, identifier in
|
||||
let row = try #require(listView.rowView(for: rowID))
|
||||
return try #require(descendant(in: row, identifier: identifier) as? NSButton)
|
||||
}
|
||||
|
||||
for button in buttons {
|
||||
try expectProminentForeground(button, component: 1)
|
||||
}
|
||||
|
||||
listView.appearance = NSAppearance(named: .darkAqua)
|
||||
listView.layoutSubtreeIfNeeded()
|
||||
|
||||
for button in buttons {
|
||||
try expectProminentForeground(button, component: 0)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func userImagesAndChipsSitOutsideAndAboveTheTextBubble() throws {
|
||||
let coordinator = TranscriptListView.Coordinator()
|
||||
@@ -1278,6 +1349,30 @@ struct TranscriptListViewBridgeTests {
|
||||
return nil
|
||||
}
|
||||
|
||||
private func expectProminentForeground(_ button: NSButton, component: CGFloat) throws {
|
||||
let titleColor = try #require(
|
||||
button.attributedTitle.attribute(
|
||||
.foregroundColor,
|
||||
at: 0,
|
||||
effectiveRange: nil
|
||||
) as? NSColor
|
||||
)
|
||||
let alternateTitleColor = try #require(
|
||||
button.attributedAlternateTitle.attribute(
|
||||
.foregroundColor,
|
||||
at: 0,
|
||||
effectiveRange: nil
|
||||
) as? NSColor
|
||||
)
|
||||
for color in [try #require(button.contentTintColor), titleColor, alternateTitleColor] {
|
||||
let rgb = try #require(color.usingColorSpace(.deviceRGB))
|
||||
#expect(abs(rgb.redComponent - component) < 0.001)
|
||||
#expect(abs(rgb.greenComponent - component) < 0.001)
|
||||
#expect(abs(rgb.blueComponent - component) < 0.001)
|
||||
#expect(abs(rgb.alphaComponent - 1) < 0.001)
|
||||
}
|
||||
}
|
||||
|
||||
private func work(id: UUID, state: AgentRunState) -> TranscriptEntry {
|
||||
let start = Date(timeIntervalSince1970: 1_000)
|
||||
return TranscriptEntry(
|
||||
|
||||
Reference in new issue
Block a user