From 31c651ecb155654ac882bbb3e1afac4143e08a72 Mon Sep 17 00:00:00 2001 From: Valdemar Lorenzen Date: Thu, 15 Oct 2026 11:16:49 +0200 Subject: [PATCH] style: align translation controls with launcher design --- README.md | 4 +- .../Conjure/LauncherWindowController.swift | 8 +++- Sources/Conjure/TranslationController.swift | 40 ++++++++++++++++--- Tests/ConjureKitTests/TranslationTests.swift | 2 - 4 files changed, 43 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 868e01a..2857fe4 100644 --- a/README.md +++ b/README.md @@ -81,9 +81,9 @@ Applications with identical display names show a muted containing-folder hint be ## Conjure Translate -Search for **Conjure Translate**, labeled **Command**, and open it with Return, double-click, or **Open Command**. The launcher fades into an English → Japanese translation tool; the existing search bar becomes the English input. Escape or **Back** returns to the previous search. +Search for **Conjure Translate**, labeled **Command**, and open it with Return, double-click, or **Open Command**. The launcher fades into an English → Japanese translation tool; the existing search bar becomes the English input and retains the launcher's placeholder styling. Escape returns to the previous search. -Choose **MyMemory (no key)** or **OpenAI · gpt-6-luna** from the provider dropdown. MyMemory is the initial default, and the selection is saved locally. Switching providers cancels pending work and clears the previous translation; there is no automatic fallback. Press Return from the English input or click **Translate** to send text to the selected provider. +Choose **MyMemory (no key)** or **OpenAI · gpt-6-luna** from the provider dropdown. MyMemory is the initial default, and the selection is saved locally. Switching providers cancels pending work and clears the previous translation; there is no automatic fallback. Press Return from the English input or click **Translate** in the bottom-right launcher-style action pill, beside its Return key badge, to send text to the selected provider. [MyMemory](https://mymemory.translated.net/doc/spec.php) requires no account or key. Anonymous use allows 500 UTF-8 bytes per request and [5,000 characters per day](https://mymemory.translated.net/doc/usagelimits.php). Oversized input is rejected before sending; quota and service errors appear in the tool. Text is sent to a third-party translation service: avoid sensitive content. diff --git a/Sources/Conjure/LauncherWindowController.swift b/Sources/Conjure/LauncherWindowController.swift index 3831756..151ecb5 100644 --- a/Sources/Conjure/LauncherWindowController.swift +++ b/Sources/Conjure/LauncherWindowController.swift @@ -776,6 +776,7 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe translation = controller controller.input = { [weak self] in self?.searchField.stringValue ?? "" } let previousQuery = searchField.stringValue + let previousPlaceholder = searchField.placeholderAttributedString controller.closed = { [weak self, weak controller] in guard let self, let controller else { return } controller.view.removeFromSuperview() @@ -785,7 +786,7 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe view.setAccessibilityHidden(false) view.alphaValue = 1 } - self.searchField.placeholderString = "Search apps, calculate, or convert…" + self.searchField.placeholderAttributedString = previousPlaceholder self.searchField.setAccessibilityLabel("Search applications, calculate, or convert units and currencies") self.searchField.stringValue = previousQuery self.openButton.keyEquivalent = "\r" @@ -796,7 +797,10 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe (window as? LauncherPanel)?.resetNumberHints() openButton.keyEquivalent = "" searchField.stringValue = "" - searchField.placeholderString = "Enter English text to translate…" + searchField.placeholderAttributedString = NSAttributedString( + string: "Enter English text to translate…", + attributes: previousPlaceholder?.attributes(at: 0, effectiveRange: nil) ?? [:] + ) searchField.setAccessibilityLabel("English text to translate into Japanese") for view in launcherViews.dropFirst() { view.isHidden = true diff --git a/Sources/Conjure/TranslationController.swift b/Sources/Conjure/TranslationController.swift index 84fa3f3..de72018 100644 --- a/Sources/Conjure/TranslationController.swift +++ b/Sources/Conjure/TranslationController.swift @@ -10,7 +10,7 @@ final class TranslationController: NSViewController { private var usesMyMemory: Bool { provider.indexOfSelectedItem == 0 } private let output = NSTextView() private let status = NSTextField(wrappingLabelWithString: "") - private let translateButton = NSButton(title: "Translate ↵", target: nil, action: nil) + private let translateButton = NSButton(title: "Translate", target: nil, action: nil) private var task: Task? var input: (() -> String)? var closed: (() -> Void)? @@ -22,10 +22,9 @@ final class TranslationController: NSViewController { override func loadView() { view = NSView() - let back = NSButton(title: "‹ Back", target: self, action: #selector(goBack)) let title = NSTextField(labelWithString: "Conjure Translate · English → Japanese") title.font = .systemFont(ofSize: 13, weight: .semibold) - let heading = NSStackView(views: [back, title, NSView()]) + let heading = NSStackView(views: [title, NSView()]) provider.addItems(withTitles: ["MyMemory (no key)", "OpenAI · gpt-6-luna"]) provider.selectItem(at: UserDefaults.standard.string(forKey: "translationProvider") == "openai" ? 1 : 0) provider.setAccessibilityLabel("Translation provider") @@ -41,6 +40,33 @@ final class TranslationController: NSViewController { translateButton.target = self translateButton.action = #selector(translate) translateButton.keyEquivalent = "\r" + translateButton.isBordered = false + translateButton.font = .systemFont(ofSize: 12, weight: .medium) + translateButton.contentTintColor = SunsetPalette.text + translateButton.toolTip = "Translate the English input (Return). Escape returns to the launcher." + let returnKey = ShortcutIndicator("↵") + let actionPill = NSView() + actionPill.wantsLayer = true + actionPill.layer?.cornerRadius = 18 + actionPill.layer?.borderWidth = 0.5 + actionPill.layer?.borderColor = SunsetPalette.accent.withAlphaComponent(0.35).cgColor + actionPill.layer?.backgroundColor = SunsetPalette.background.withAlphaComponent(0.25).cgColor + for control in [translateButton, returnKey] { + control.translatesAutoresizingMaskIntoConstraints = false + actionPill.addSubview(control) + } + NSLayoutConstraint.activate([ + actionPill.heightAnchor.constraint(equalToConstant: 36), + translateButton.leadingAnchor.constraint(equalTo: actionPill.leadingAnchor, constant: 14), + translateButton.centerYAnchor.constraint(equalTo: actionPill.centerYAnchor), + returnKey.leadingAnchor.constraint(equalTo: translateButton.trailingAnchor, constant: 8), + returnKey.centerYAnchor.constraint(equalTo: actionPill.centerYAnchor), + returnKey.trailingAnchor.constraint(equalTo: actionPill.trailingAnchor, constant: -12), + ]) + let footer = NSStackView(views: [NSView(), actionPill]) + footer.orientation = .horizontal + footer.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(footer) output.isEditable = false output.isSelectable = true output.drawsBackground = false @@ -56,7 +82,7 @@ final class TranslationController: NSViewController { output.textContainer?.widthTracksTextView = true status.textColor = SunsetPalette.secondaryText status.font = .systemFont(ofSize: 12) - let stack = NSStackView(views: [heading, provider, credentials, scroll, status, translateButton]) + let stack = NSStackView(views: [heading, provider, credentials, scroll, status]) stack.orientation = .vertical stack.alignment = .leading stack.spacing = 12 @@ -66,7 +92,11 @@ final class TranslationController: NSViewController { stack.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16), stack.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16), stack.topAnchor.constraint(equalTo: view.topAnchor), - stack.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -16), + stack.bottomAnchor.constraint(equalTo: footer.topAnchor, constant: -12), + footer.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16), + footer.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -8), + footer.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -8), + footer.heightAnchor.constraint(equalToConstant: 36), credentials.widthAnchor.constraint(equalTo: stack.widthAnchor), scroll.widthAnchor.constraint(equalTo: stack.widthAnchor), scroll.heightAnchor.constraint(greaterThanOrEqualToConstant: 100), diff --git a/Tests/ConjureKitTests/TranslationTests.swift b/Tests/ConjureKitTests/TranslationTests.swift index 655fad6..fbd3f37 100644 --- a/Tests/ConjureKitTests/TranslationTests.swift +++ b/Tests/ConjureKitTests/TranslationTests.swift @@ -44,9 +44,7 @@ import Testing #expect(search.accessibilityLabel() == "English text to translate into Japanese") search.stringValue = "Good morning" launcher.controlTextDidChange(Notification(name: NSControl.textDidChangeNotification, object: search)) - #expect(descendants(content).contains { ($0 as? NSButton)?.title == "Translate ↵" }) _ = launcher.control(search, textView: NSTextView(), doCommandBy: #selector(NSResponder.cancelOperation(_:))) #expect(search.stringValue == "translate") - #expect(!descendants(content).contains { ($0 as? NSButton)?.title == "Translate ↵" }) #expect(table.numberOfRows == 1) }