style: add sunset palette and soft background glows

This commit is contained in:
2026-10-11 19:46:20 +02:00
parent 62e813f58f
commit c7e35cec44
3 changed files with 77 additions and 28 deletions
+2 -2
View File
@@ -37,9 +37,9 @@ Search is case- and accent-insensitive. Ranking is exact name, prefix, substring
## Native UI ## Native UI
The launcher now follows the Raycast reference: a 750 × 474-point, titlebar-free dark panel with rounded corners, native translucency and shadow, a large borderless search field, compact icon rows, neutral rounded selection, right-aligned application labels, and a bottom-right launch pill with a Return key hint. It deliberately uses dark appearance for this initial visual direction. The launcher keeps its Raycast-inspired 750 × 474-point, titlebar-free rounded panel, large borderless search field, compact icon rows, right-aligned application labels, and bottom-right launch pill. Its Sunset palette uses a dark brown background, soft amber and rust glows, warm cream text, and orange accents on the caret, selection, and action outline.
AppKit still provides text editing, table navigation, scrolling, application icons, controls, menus, fonts, semantic colors, and visual-effect materials. Custom styling is limited to layout, rounded surfaces, and selection drawing; increased-contrast settings add a selection outline. No third-party UI dependencies, copied Raycast branding, or nonfunctional Actions menu. AppKit still provides text editing, table navigation, scrolling, application icons, controls, menus, fonts, and visual-effect materials. The background uses cached native radial gradients that fade to transparent: static soft blobs, without animations, timers, image assets, blur filters, or third-party UI dependencies. Decorative drawing does not intercept clicks. Increased-contrast settings strengthen the selection outline, and text colors are tuned for readability across the glows and selection.
- `Sources/Conjure`: application lifecycle, native window, and application launching. - `Sources/Conjure`: application lifecycle, native window, and application launching.
- `Sources/ConjureKit`: filesystem discovery and search ranking. - `Sources/ConjureKit`: filesystem discovery and search ranking.
+45
View File
@@ -0,0 +1,45 @@
import AppKit
@MainActor
enum SunsetPalette {
static let background = NSColor(srgbRed: 0.12, green: 0.09, blue: 0.08, alpha: 0.96)
static let text = NSColor(srgbRed: 1.00, green: 0.89, blue: 0.71, alpha: 1)
static let secondaryText = NSColor(srgbRed: 0.85, green: 0.78, blue: 0.65, alpha: 1)
static let accent = NSColor(srgbRed: 0.95, green: 0.58, blue: 0.23, alpha: 1)
static let selectionFill = accent.withAlphaComponent(0.12)
static let selectionBorder = accent.withAlphaComponent(0.30)
static let ember = NSColor(srgbRed: 0.76, green: 0.27, blue: 0.18, alpha: 1)
}
@MainActor
final class LauncherBackground: NSView {
private let amber = NSGradient(colors: [
SunsetPalette.accent.withAlphaComponent(0.18),
SunsetPalette.accent.withAlphaComponent(0.065),
SunsetPalette.accent.withAlphaComponent(0),
])!
private let ember = NSGradient(colors: [
SunsetPalette.ember.withAlphaComponent(0.26),
SunsetPalette.ember.withAlphaComponent(0.10),
SunsetPalette.ember.withAlphaComponent(0),
])!
override func hitTest(_ point: NSPoint) -> NSView? { nil }
override func draw(_ dirtyRect: NSRect) {
SunsetPalette.background.setFill()
dirtyRect.fill()
let upperGlow = NSPoint(x: bounds.width * 0.58, y: bounds.height * 0.94)
ember.draw(fromCenter: upperGlow, radius: 0, toCenter: upperGlow,
radius: bounds.width * 0.40, options: [])
let lowerGlow = NSPoint(x: bounds.width * 0.88, y: bounds.height * 0.10)
amber.draw(fromCenter: lowerGlow, radius: 0, toCenter: lowerGlow,
radius: bounds.width * 0.55, options: [])
let leftGlow = NSPoint(x: bounds.width * 0.08, y: bounds.height * 0.65)
NSGraphicsContext.saveGraphicsState()
NSGraphicsContext.current?.cgContext.setAlpha(0.45)
amber.draw(fromCenter: leftGlow, radius: 0, toCenter: leftGlow,
radius: bounds.width * 0.32, options: [])
NSGraphicsContext.restoreGraphicsState()
}
}
+29 -25
View File
@@ -47,7 +47,7 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
NSApp.activate() NSApp.activate()
window?.makeKeyAndOrderFront(nil) window?.makeKeyAndOrderFront(nil)
window?.makeFirstResponder(searchField) window?.makeFirstResponder(searchField)
(searchField.currentEditor() as? NSTextView)?.insertionPointColor = .labelColor (searchField.currentEditor() as? NSTextView)?.insertionPointColor = SunsetPalette.accent
refreshApplications() refreshApplications()
} }
@@ -66,14 +66,18 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
content.layer?.masksToBounds = true content.layer?.masksToBounds = true
content.layer?.borderWidth = 0.5 content.layer?.borderWidth = 0.5
window.contentView = content window.contentView = content
let tint = NSView(frame: content.bounds) let background = LauncherBackground(frame: content.bounds)
tint.autoresizingMask = [.width, .height] background.autoresizingMask = [.width, .height]
tint.wantsLayer = true background.setAccessibilityElement(false)
content.addSubview(tint) content.addSubview(background)
searchField.placeholderString = "Search for apps…" let searchFont = NSFont.systemFont(ofSize: 20)
searchField.font = .systemFont(ofSize: 20) searchField.font = searchFont
searchField.textColor = .labelColor searchField.placeholderAttributedString = NSAttributedString(
string: "Search for apps…",
attributes: [.foregroundColor: SunsetPalette.secondaryText, .font: searchFont]
)
searchField.textColor = SunsetPalette.text
searchField.isBordered = false searchField.isBordered = false
searchField.drawsBackground = false searchField.drawsBackground = false
searchField.focusRingType = .none searchField.focusRingType = .none
@@ -88,7 +92,7 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
let symbol = NSImageView() let symbol = NSImageView()
symbol.image = NSImage(systemSymbolName: "sparkles", accessibilityDescription: nil) symbol.image = NSImage(systemSymbolName: "sparkles", accessibilityDescription: nil)
symbol.contentTintColor = .tertiaryLabelColor symbol.contentTintColor = SunsetPalette.accent
symbol.setAccessibilityElement(false) symbol.setAccessibilityElement(false)
let header = NSStackView(views: [symbol, searchField]) let header = NSStackView(views: [symbol, searchField])
header.orientation = .horizontal header.orientation = .horizontal
@@ -97,7 +101,7 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
let sectionLabel = NSTextField(labelWithString: "Applications") let sectionLabel = NSTextField(labelWithString: "Applications")
sectionLabel.font = .systemFont(ofSize: 12, weight: .semibold) sectionLabel.font = .systemFont(ofSize: 12, weight: .semibold)
sectionLabel.textColor = .secondaryLabelColor sectionLabel.textColor = SunsetPalette.secondaryText
let column = NSTableColumn(identifier: NSUserInterfaceItemIdentifier("application")) let column = NSTableColumn(identifier: NSUserInterfaceItemIdentifier("application"))
column.resizingMask = .autoresizingMask column.resizingMask = .autoresizingMask
@@ -134,14 +138,14 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
scrollView.horizontalScrollElasticity = .none scrollView.horizontalScrollElasticity = .none
statusLabel.font = .systemFont(ofSize: 12) statusLabel.font = .systemFont(ofSize: 12)
statusLabel.textColor = .secondaryLabelColor statusLabel.textColor = SunsetPalette.secondaryText
statusLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) statusLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
emptyLabel.textColor = .secondaryLabelColor emptyLabel.textColor = SunsetPalette.secondaryText
emptyLabel.alignment = .center emptyLabel.alignment = .center
openButton.isBordered = false openButton.isBordered = false
openButton.font = .systemFont(ofSize: 14, weight: .medium) openButton.font = .systemFont(ofSize: 14, weight: .medium)
openButton.contentTintColor = .labelColor openButton.contentTintColor = SunsetPalette.text
openButton.keyEquivalent = "\r" openButton.keyEquivalent = "\r"
openButton.target = self openButton.target = self
openButton.action = #selector(openSelection) openButton.action = #selector(openSelection)
@@ -149,7 +153,7 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
openButton.toolTip = "Open the selected application (Return). Use ↑ and ↓ to select." openButton.toolTip = "Open the selected application (Return). Use ↑ and ↓ to select."
let returnKey = NSTextField(labelWithString: "↵") let returnKey = NSTextField(labelWithString: "↵")
returnKey.font = .systemFont(ofSize: 14) returnKey.font = .systemFont(ofSize: 14)
returnKey.textColor = .secondaryLabelColor returnKey.textColor = SunsetPalette.secondaryText
returnKey.alignment = .center returnKey.alignment = .center
returnKey.wantsLayer = true returnKey.wantsLayer = true
returnKey.layer?.cornerRadius = 5 returnKey.layer?.cornerRadius = 5
@@ -208,11 +212,11 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
returnKey.heightAnchor.constraint(equalToConstant: 18), returnKey.heightAnchor.constraint(equalToConstant: 18),
]) ])
window.effectiveAppearance.performAsCurrentDrawingAppearance { window.effectiveAppearance.performAsCurrentDrawingAppearance {
let border = NSColor.separatorColor.cgColor let border = SunsetPalette.secondaryText.withAlphaComponent(0.22).cgColor
content.layer?.borderColor = border content.layer?.borderColor = border
returnKey.layer?.borderColor = border returnKey.layer?.borderColor = SunsetPalette.secondaryText.withAlphaComponent(0.35).cgColor
actionPill.layer?.borderColor = border actionPill.layer?.borderColor = SunsetPalette.accent.withAlphaComponent(0.35).cgColor
tint.layer?.backgroundColor = NSColor.textBackgroundColor.withAlphaComponent(0.85).cgColor actionPill.layer?.backgroundColor = SunsetPalette.accent.withAlphaComponent(0.08).cgColor
} }
window.initialFirstResponder = searchField window.initialFirstResponder = searchField
} }
@@ -315,10 +319,11 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
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: 14, weight: .medium)
label.textColor = SunsetPalette.text
label.lineBreakMode = .byTruncatingTail label.lineBreakMode = .byTruncatingTail
let kind = NSTextField(labelWithString: "Application") let kind = NSTextField(labelWithString: "Application")
kind.font = .systemFont(ofSize: 14) kind.font = .systemFont(ofSize: 14)
kind.textColor = .secondaryLabelColor kind.textColor = SunsetPalette.secondaryText
kind.setContentCompressionResistancePriority(.required, for: .horizontal) kind.setContentCompressionResistancePriority(.required, for: .horizontal)
kind.setAccessibilityElement(false) kind.setAccessibilityElement(false)
cell.imageView = icon cell.imageView = icon
@@ -418,13 +423,12 @@ private final class LauncherPanel: NSPanel {
@MainActor @MainActor
private final class LauncherRowView: NSTableRowView { private final class LauncherRowView: NSTableRowView {
override func drawSelection(in dirtyRect: NSRect) { override func drawSelection(in dirtyRect: NSRect) {
let selection = NSBezierPath(roundedRect: bounds, xRadius: 10, yRadius: 10) let selection = NSBezierPath(roundedRect: bounds.insetBy(dx: 0.75, dy: 0.75), xRadius: 10, yRadius: 10)
NSColor.unemphasizedSelectedContentBackgroundColor.withAlphaComponent(0.55).setFill() SunsetPalette.selectionFill.setFill()
selection.fill() selection.fill()
if NSWorkspace.shared.accessibilityDisplayShouldIncreaseContrast { let increasedContrast = NSWorkspace.shared.accessibilityDisplayShouldIncreaseContrast
NSColor.labelColor.setStroke() (increasedContrast ? SunsetPalette.accent : SunsetPalette.selectionBorder).setStroke()
selection.lineWidth = 1 selection.lineWidth = increasedContrast ? 1.5 : 0.5
selection.stroke() selection.stroke()
} }
}
} }