style: match action icons and input text to application rows

This commit is contained in:
2026-10-12 16:48:14 +02:00
parent 5537bc85b8
commit ff8407e771
3 changed files with 31 additions and 14 deletions
+1 -1
View File
@@ -79,7 +79,7 @@ Applications with identical display names show a muted containing-folder hint be
Select an application and open **Actions ⌘K**. The red **Uninstall application** action opens a review sheet with file checkboxes, paths, sizes, and a selected-item count. Filtering by name or path does not change selections. Up/Down highlights rows; Space toggles the highlighted eligible item when the file list has focus. Back or Escape returns to the launcher. Return or the uninstall button opens a final confirmation; nothing is moved merely by choosing the action. Select an application and open **Actions ⌘K**. The red **Uninstall application** action opens a review sheet with file checkboxes, paths, sizes, and a selected-item count. Filtering by name or path does not change selections. Up/Down highlights rows; Space toggles the highlighted eligible item when the file list has focus. Back or Escape returns to the launcher. Return or the uninstall button opens a final confirmation; nothing is moved merely by choosing the action.
The actions selector is an arrowless panel inside the launcher, aligned to the bottom and right edges of the footer action pill, replacing it visually while open rather than floating above it. It shares the launcher's frosted Sunset background, 38-point selection row height, 14-point action text, rounded selection styling, and Return key badge; selection width follows the panel. The application name appears at the top and borderless action search sits eight points from the bottom, without a divider. The transition is direct, without a morph animation. Escape, Command-K, an outside click, or switching away closes it and restores the pill. Closing while Conjure remains active restores launcher search focus; switching away does not steal focus back. Return in action search chooses the visible action rather than launching the underlying application. The actions selector is an arrowless panel inside the launcher, aligned to the bottom and right edges of the footer action pill, replacing it visually while open rather than floating above it. It shares the launcher's frosted Sunset background, 38-point selection row height, 22×22-point icon boxes with 12-point icon-to-text spacing, 14-point action and search text, rounded selection styling, and Return key badge; selection width follows the panel. Application and command rows use the same sizing constants. SF Symbol optical alignment insets are removed so command icons render inside the same bounds as application icons. The application name appears at the top and borderless action search sits eight points from the bottom, without a divider. The transition is direct, without a morph animation. Escape, Command-K, an outside click, or switching away closes it and restores the pill. Closing while Conjure remains active restores launcher search focus; switching away does not steal focus back. Return in action search chooses the visible action rather than launching the underlying application.
Discovery includes the application bundle and exact bundle-ID paths in your Library: Preferences, Caches, Application Support, Logs, Saved Application State, Application Scripts, and Containers. Embedded extension identifiers are included. App-group entitlements are read from validated native code signatures; related shared Group Containers and Application Scripts remain unchecked because other applications may use them. Conjure does not guess name-based leftovers or include package receipts. This is conservative discovery, not a guarantee that every associated file is found. Discovery includes the application bundle and exact bundle-ID paths in your Library: Preferences, Caches, Application Support, Logs, Saved Application State, Application Scripts, and Containers. Embedded extension identifiers are included. App-group entitlements are read from validated native code signatures; related shared Group Containers and Application Scripts remain unchecked because other applications may use them. Conjure does not guess name-based leftovers or include package receipts. This is conservative discovery, not a guarantee that every associated file is found.
@@ -38,16 +38,19 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat
applicationLabel.lineBreakMode = .byTruncatingTail applicationLabel.lineBreakMode = .byTruncatingTail
applicationLabel.font = .systemFont(ofSize: 12, weight: .medium) applicationLabel.font = .systemFont(ofSize: 12, weight: .medium)
row.isSelected = true row.isSelected = true
row.addGestureRecognizer(NSClickGestureRecognizer(target: self, action: #selector(chooseAction)))
action.isBordered = false action.isBordered = false
action.alignment = .left action.alignment = .left
action.font = .systemFont(ofSize: 14, weight: .medium) action.font = .systemFont(ofSize: LauncherRowView.textSize, weight: .medium)
action.contentTintColor = .systemRed action.contentTintColor = .systemRed
action.attributedTitle = NSAttributedString(string: action.title, attributes: [ action.attributedTitle = NSAttributedString(string: action.title, attributes: [
.foregroundColor: NSColor.systemRed, .font: NSFont.systemFont(ofSize: 14, weight: .medium) .foregroundColor: NSColor.systemRed, .font: NSFont.systemFont(ofSize: LauncherRowView.textSize, weight: .medium)
]) ])
action.image = NSImage(systemSymbolName: "trash", accessibilityDescription: nil)? let icon = ActionIconView()
.withSymbolConfiguration(NSImage.SymbolConfiguration(pointSize: 18, weight: .regular)) icon.image = NSImage(systemSymbolName: "trash", accessibilityDescription: nil)
action.imagePosition = .imageLeading icon.contentTintColor = .systemRed
icon.imageScaling = .scaleProportionallyDown
icon.setAccessibilityElement(false)
action.target = self action.target = self
action.action = #selector(chooseAction) action.action = #selector(chooseAction)
action.setAccessibilityLabel("Uninstall application") action.setAccessibilityLabel("Uninstall application")
@@ -59,10 +62,10 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat
returnKey.layer?.cornerRadius = 5 returnKey.layer?.cornerRadius = 5
returnKey.layer?.borderWidth = 1 returnKey.layer?.borderWidth = 1
returnKey.setAccessibilityElement(false) returnKey.setAccessibilityElement(false)
search.font = .systemFont(ofSize: 16) search.font = .systemFont(ofSize: LauncherRowView.textSize)
search.textColor = SunsetPalette.text search.textColor = SunsetPalette.text
search.placeholderAttributedString = NSAttributedString(string: "Search for actions…", attributes: [ search.placeholderAttributedString = NSAttributedString(string: "Search for actions…", attributes: [
.foregroundColor: SunsetPalette.secondaryText, .font: NSFont.systemFont(ofSize: 16) .foregroundColor: SunsetPalette.secondaryText, .font: NSFont.systemFont(ofSize: LauncherRowView.textSize)
]) ])
search.isBordered = false search.isBordered = false
search.drawsBackground = false search.drawsBackground = false
@@ -84,7 +87,7 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat
child.translatesAutoresizingMaskIntoConstraints = false child.translatesAutoresizingMaskIntoConstraints = false
if child !== background { content.addSubview(child) } if child !== background { content.addSubview(child) }
} }
for child in [action, returnKey] { for child in [icon, action, returnKey] {
child.translatesAutoresizingMaskIntoConstraints = false child.translatesAutoresizingMaskIntoConstraints = false
row.addSubview(child) row.addSubview(child)
} }
@@ -106,7 +109,11 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat
row.trailingAnchor.constraint(equalTo: content.trailingAnchor, constant: -8), row.trailingAnchor.constraint(equalTo: content.trailingAnchor, constant: -8),
row.topAnchor.constraint(equalTo: applicationLabel.bottomAnchor, constant: 10), row.topAnchor.constraint(equalTo: applicationLabel.bottomAnchor, constant: 10),
row.heightAnchor.constraint(equalToConstant: LauncherRowView.rowHeight), row.heightAnchor.constraint(equalToConstant: LauncherRowView.rowHeight),
action.leadingAnchor.constraint(equalTo: row.leadingAnchor, constant: 8), icon.leadingAnchor.constraint(equalTo: row.leadingAnchor, constant: 8),
icon.centerYAnchor.constraint(equalTo: row.centerYAnchor),
icon.widthAnchor.constraint(equalToConstant: LauncherRowView.iconSize),
icon.heightAnchor.constraint(equalToConstant: LauncherRowView.iconSize),
action.leadingAnchor.constraint(equalTo: icon.trailingAnchor, constant: LauncherRowView.iconToTextSpacing),
action.centerYAnchor.constraint(equalTo: row.centerYAnchor), action.centerYAnchor.constraint(equalTo: row.centerYAnchor),
action.trailingAnchor.constraint(equalTo: returnKey.leadingAnchor, constant: -12), action.trailingAnchor.constraint(equalTo: returnKey.leadingAnchor, constant: -12),
returnKey.trailingAnchor.constraint(equalTo: row.trailingAnchor, constant: -12), returnKey.trailingAnchor.constraint(equalTo: row.trailingAnchor, constant: -12),
@@ -186,11 +193,18 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat
} }
@objc private func chooseAction() { @objc private func chooseAction() {
guard isShown else { return }
close() close()
uninstall?() uninstall?()
} }
} }
@MainActor
private final class ActionIconView: NSImageView {
// SF Symbol optical insets otherwise enlarge a 22-point icon box beyond application icon sizing.
override var alignmentRectInsets: NSEdgeInsets { NSEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) }
}
@MainActor @MainActor
private final class ActionsSearchField: NSSearchField { private final class ActionsSearchField: NSSearchField {
var closeActions: (() -> Void)? var closeActions: (() -> Void)?
@@ -453,7 +453,7 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
icon.imageScaling = .scaleProportionallyDown icon.imageScaling = .scaleProportionallyDown
icon.setAccessibilityElement(false) icon.setAccessibilityElement(false)
let label = NSTextField(labelWithString: "") let label = NSTextField(labelWithString: "")
label.font = .systemFont(ofSize: 14, weight: .medium) label.font = .systemFont(ofSize: LauncherRowView.textSize, weight: .medium)
label.textColor = SunsetPalette.text label.textColor = SunsetPalette.text
label.lineBreakMode = .byTruncatingTail label.lineBreakMode = .byTruncatingTail
let location = cell.locationLabel let location = cell.locationLabel
@@ -480,9 +480,9 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
icon.leadingAnchor.constraint(equalTo: cell.leadingAnchor, constant: 8), icon.leadingAnchor.constraint(equalTo: cell.leadingAnchor, constant: 8),
icon.centerYAnchor.constraint(equalTo: cell.centerYAnchor), icon.centerYAnchor.constraint(equalTo: cell.centerYAnchor),
icon.widthAnchor.constraint(equalToConstant: 22), icon.widthAnchor.constraint(equalToConstant: LauncherRowView.iconSize),
icon.heightAnchor.constraint(equalToConstant: 22), icon.heightAnchor.constraint(equalToConstant: LauncherRowView.iconSize),
title.leadingAnchor.constraint(equalTo: icon.trailingAnchor, constant: 12), title.leadingAnchor.constraint(equalTo: icon.trailingAnchor, constant: LauncherRowView.iconToTextSpacing),
title.centerYAnchor.constraint(equalTo: cell.centerYAnchor), title.centerYAnchor.constraint(equalTo: cell.centerYAnchor),
title.trailingAnchor.constraint(lessThanOrEqualTo: kind.leadingAnchor, constant: -12), title.trailingAnchor.constraint(lessThanOrEqualTo: kind.leadingAnchor, constant: -12),
kind.centerYAnchor.constraint(equalTo: cell.centerYAnchor), kind.centerYAnchor.constraint(equalTo: cell.centerYAnchor),
@@ -604,6 +604,9 @@ private final class LauncherPanel: NSPanel {
@MainActor @MainActor
final class LauncherRowView: NSTableRowView { final class LauncherRowView: NSTableRowView {
static let rowHeight: CGFloat = 38 static let rowHeight: CGFloat = 38
static let textSize: CGFloat = 14
static let iconSize: CGFloat = 22
static let iconToTextSpacing: CGFloat = 12
override func drawSelection(in dirtyRect: NSRect) { override func drawSelection(in dirtyRect: NSRect) {
let selection = NSBezierPath(roundedRect: bounds.insetBy(dx: 0.75, dy: 0.75), xRadius: 10, yRadius: 10) let selection = NSBezierPath(roundedRect: bounds.insetBy(dx: 0.75, dy: 0.75), xRadius: 10, yRadius: 10)