diff --git a/README.md b/README.md index 11478c4..d9323b1 100644 --- a/README.md +++ b/README.md @@ -37,9 +37,9 @@ Search is case- and accent-insensitive. Ranking is exact name, prefix, substring ## 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/ConjureKit`: filesystem discovery and search ranking. diff --git a/Sources/Conjure/LauncherBackground.swift b/Sources/Conjure/LauncherBackground.swift new file mode 100644 index 0000000..637b232 --- /dev/null +++ b/Sources/Conjure/LauncherBackground.swift @@ -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() + } +} diff --git a/Sources/Conjure/LauncherWindowController.swift b/Sources/Conjure/LauncherWindowController.swift index c4c1c4e..61724f4 100644 --- a/Sources/Conjure/LauncherWindowController.swift +++ b/Sources/Conjure/LauncherWindowController.swift @@ -47,7 +47,7 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe NSApp.activate() window?.makeKeyAndOrderFront(nil) window?.makeFirstResponder(searchField) - (searchField.currentEditor() as? NSTextView)?.insertionPointColor = .labelColor + (searchField.currentEditor() as? NSTextView)?.insertionPointColor = SunsetPalette.accent refreshApplications() } @@ -66,14 +66,18 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe content.layer?.masksToBounds = true content.layer?.borderWidth = 0.5 window.contentView = content - let tint = NSView(frame: content.bounds) - tint.autoresizingMask = [.width, .height] - tint.wantsLayer = true - content.addSubview(tint) + let background = LauncherBackground(frame: content.bounds) + background.autoresizingMask = [.width, .height] + background.setAccessibilityElement(false) + content.addSubview(background) - searchField.placeholderString = "Search for apps…" - searchField.font = .systemFont(ofSize: 20) - searchField.textColor = .labelColor + let searchFont = NSFont.systemFont(ofSize: 20) + searchField.font = searchFont + searchField.placeholderAttributedString = NSAttributedString( + string: "Search for apps…", + attributes: [.foregroundColor: SunsetPalette.secondaryText, .font: searchFont] + ) + searchField.textColor = SunsetPalette.text searchField.isBordered = false searchField.drawsBackground = false searchField.focusRingType = .none @@ -88,7 +92,7 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe let symbol = NSImageView() symbol.image = NSImage(systemSymbolName: "sparkles", accessibilityDescription: nil) - symbol.contentTintColor = .tertiaryLabelColor + symbol.contentTintColor = SunsetPalette.accent symbol.setAccessibilityElement(false) let header = NSStackView(views: [symbol, searchField]) header.orientation = .horizontal @@ -97,7 +101,7 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe let sectionLabel = NSTextField(labelWithString: "Applications") sectionLabel.font = .systemFont(ofSize: 12, weight: .semibold) - sectionLabel.textColor = .secondaryLabelColor + sectionLabel.textColor = SunsetPalette.secondaryText let column = NSTableColumn(identifier: NSUserInterfaceItemIdentifier("application")) column.resizingMask = .autoresizingMask @@ -134,14 +138,14 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe scrollView.horizontalScrollElasticity = .none statusLabel.font = .systemFont(ofSize: 12) - statusLabel.textColor = .secondaryLabelColor + statusLabel.textColor = SunsetPalette.secondaryText statusLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) - emptyLabel.textColor = .secondaryLabelColor + emptyLabel.textColor = SunsetPalette.secondaryText emptyLabel.alignment = .center openButton.isBordered = false openButton.font = .systemFont(ofSize: 14, weight: .medium) - openButton.contentTintColor = .labelColor + openButton.contentTintColor = SunsetPalette.text openButton.keyEquivalent = "\r" openButton.target = self 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." let returnKey = NSTextField(labelWithString: "↵") returnKey.font = .systemFont(ofSize: 14) - returnKey.textColor = .secondaryLabelColor + returnKey.textColor = SunsetPalette.secondaryText returnKey.alignment = .center returnKey.wantsLayer = true returnKey.layer?.cornerRadius = 5 @@ -208,11 +212,11 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe returnKey.heightAnchor.constraint(equalToConstant: 18), ]) window.effectiveAppearance.performAsCurrentDrawingAppearance { - let border = NSColor.separatorColor.cgColor + let border = SunsetPalette.secondaryText.withAlphaComponent(0.22).cgColor content.layer?.borderColor = border - returnKey.layer?.borderColor = border - actionPill.layer?.borderColor = border - tint.layer?.backgroundColor = NSColor.textBackgroundColor.withAlphaComponent(0.85).cgColor + returnKey.layer?.borderColor = SunsetPalette.secondaryText.withAlphaComponent(0.35).cgColor + actionPill.layer?.borderColor = SunsetPalette.accent.withAlphaComponent(0.35).cgColor + actionPill.layer?.backgroundColor = SunsetPalette.accent.withAlphaComponent(0.08).cgColor } window.initialFirstResponder = searchField } @@ -315,10 +319,11 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe icon.setAccessibilityElement(false) let label = NSTextField(labelWithString: "") label.font = .systemFont(ofSize: 14, weight: .medium) + label.textColor = SunsetPalette.text label.lineBreakMode = .byTruncatingTail let kind = NSTextField(labelWithString: "Application") kind.font = .systemFont(ofSize: 14) - kind.textColor = .secondaryLabelColor + kind.textColor = SunsetPalette.secondaryText kind.setContentCompressionResistancePriority(.required, for: .horizontal) kind.setAccessibilityElement(false) cell.imageView = icon @@ -418,13 +423,12 @@ private final class LauncherPanel: NSPanel { @MainActor private final class LauncherRowView: NSTableRowView { override func drawSelection(in dirtyRect: NSRect) { - let selection = NSBezierPath(roundedRect: bounds, xRadius: 10, yRadius: 10) - NSColor.unemphasizedSelectedContentBackgroundColor.withAlphaComponent(0.55).setFill() + let selection = NSBezierPath(roundedRect: bounds.insetBy(dx: 0.75, dy: 0.75), xRadius: 10, yRadius: 10) + SunsetPalette.selectionFill.setFill() selection.fill() - if NSWorkspace.shared.accessibilityDisplayShouldIncreaseContrast { - NSColor.labelColor.setStroke() - selection.lineWidth = 1 - selection.stroke() - } + let increasedContrast = NSWorkspace.shared.accessibilityDisplayShouldIncreaseContrast + (increasedContrast ? SunsetPalette.accent : SunsetPalette.selectionBorder).setStroke() + selection.lineWidth = increasedContrast ? 1.5 : 0.5 + selection.stroke() } }