diff --git a/clients/macos/vellum-assistant/App/AppDelegate.swift b/clients/macos/vellum-assistant/App/AppDelegate.swift index 041c51d964a..d75727562cb 100644 --- a/clients/macos/vellum-assistant/App/AppDelegate.swift +++ b/clients/macos/vellum-assistant/App/AppDelegate.swift @@ -116,8 +116,10 @@ public final class AppDelegate: NSObject, NSApplicationDelegate { private var cachedSkills: [SkillInfo] = [] private var refreshSkillsTask: Task? + @AppStorage("themePreference") private var themePreference: String = "system" + public func applicationDidFinishLaunching(_ notification: Notification) { - NSApp.appearance = NSAppearance(named: .darkAqua) + applyThemePreference() registerBundledFonts() #if DEBUG @@ -147,6 +149,38 @@ public final class AppDelegate: NSObject, NSApplicationDelegate { showMainWindow() } + /// Applies the user's theme preference to the app appearance. + /// Called on launch and whenever the setting changes. + func applyThemePreference() { + let pref = UserDefaults.standard.string(forKey: "themePreference") ?? "system" + let appearance: NSAppearance? + switch pref { + case "light": + appearance = NSAppearance(named: .aqua) + case "dark": + appearance = NSAppearance(named: .darkAqua) + default: + appearance = nil // follow system + } + NSApp.appearance = appearance + // Propagate to all existing windows so the change takes effect immediately + for window in NSApp.windows { + window.appearance = appearance + window.invalidateShadow() + window.contentView?.needsDisplay = true + window.displayIfNeeded() + } + // Force SwiftUI to re-evaluate adaptive colors by toggling the appearance + DispatchQueue.main.async { + for window in NSApp.windows { + window.contentView?.effectiveAppearance.performAsCurrentDrawingAppearance { + window.contentView?.needsLayout = true + window.contentView?.needsDisplay = true + } + } + } + } + private func setupDaemonClient() { // Show macOS notification when a reminder fires daemonClient.onReminderFired = { msg in diff --git a/clients/macos/vellum-assistant/Features/Chat/ChatView.swift b/clients/macos/vellum-assistant/Features/Chat/ChatView.swift index 4eab2622916..b2607ffe14f 100644 --- a/clients/macos/vellum-assistant/Features/Chat/ChatView.swift +++ b/clients/macos/vellum-assistant/Features/Chat/ChatView.swift @@ -158,6 +158,8 @@ struct ChatView: View { ) } + @Environment(\.colorScheme) private var colorScheme + @ViewBuilder private var chatBackground: some View { if let url = ResourceBundle.bundle.url(forResource: "background", withExtension: "png"), @@ -165,6 +167,7 @@ struct ChatView: View { Image(nsImage: nsImage) .resizable() .scaledToFit() + .opacity(colorScheme == .light ? 0 : 1.0) .allowsHitTesting(false) } } @@ -311,7 +314,7 @@ struct ChatView: View { } if isThinking { - ThinkingIndicator() + ThinkingIndicator(label: messages.count <= 1 ? "Waking up..." : "Thinking") .id("thinking-indicator") .transition(.opacity.combined(with: .move(edge: .bottom))) } @@ -839,12 +842,19 @@ private struct ChatBubble: View { // MARK: - Thinking Indicator private struct ThinkingIndicator: View { + var label: String = "Thinking" @State private var phase: Int = 0 @State private var timer: Timer? var body: some View { HStack(spacing: VSpacing.xs) { - Text("Thinking") + Image("OwlIcon") + .resizable() + .scaledToFit() + .frame(width: 12, height: 12) + .foregroundColor(VColor.textSecondary) + + Text(label) .font(VFont.caption) .foregroundColor(VColor.textSecondary) diff --git a/clients/macos/vellum-assistant/Features/MainWindow/MainWindow.swift b/clients/macos/vellum-assistant/Features/MainWindow/MainWindow.swift index 81e418fb678..9cebb2aaef2 100644 --- a/clients/macos/vellum-assistant/Features/MainWindow/MainWindow.swift +++ b/clients/macos/vellum-assistant/Features/MainWindow/MainWindow.swift @@ -86,8 +86,8 @@ final class MainWindow { let hostingController = NSHostingController(rootView: MainWindowView(threadManager: threadManager, zoomManager: zoomManager, traceStore: traceStore, daemonClient: daemonClient, surfaceManager: surfaceManager, ambientAgent: ambientAgent, settingsStore: services.settingsStore, windowState: windowState, onMicrophoneToggle: onMicrophoneToggle ?? {})) let screenFrame = NSScreen.main?.visibleFrame ?? NSScreen.screens.first?.visibleFrame ?? NSRect(x: 0, y: 0, width: 1440, height: 900) - let windowWidth = min(screenFrame.width * 0.8, 1200) - let windowHeight = min(screenFrame.height * 0.85, 900) + let windowWidth: CGFloat = 780 + let windowHeight: CGFloat = 700 let windowRect = NSRect( x: screenFrame.midX - windowWidth / 2, y: screenFrame.midY - windowHeight / 2, @@ -107,7 +107,7 @@ final class MainWindow { window.titlebarAppearsTransparent = true window.backgroundColor = NSColor(VColor.background) window.isReleasedWhenClosed = false - window.contentMinSize = NSSize(width: 800, height: 600) + window.contentMinSize = NSSize(width: 500, height: 400) window.setFrame(windowRect, display: false) window.setFrameAutosaveName("MainWindow") diff --git a/clients/macos/vellum-assistant/Features/MainWindow/MainWindowView.swift b/clients/macos/vellum-assistant/Features/MainWindow/MainWindowView.swift index 77b54b2ca5d..8084c2a3265 100644 --- a/clients/macos/vellum-assistant/Features/MainWindow/MainWindowView.swift +++ b/clients/macos/vellum-assistant/Features/MainWindow/MainWindowView.swift @@ -296,7 +296,7 @@ struct MainWindowView: View { } .padding(.horizontal, VSpacing.sm) .padding(.vertical, VSpacing.xs) - .background(isSelected || isHoveredThread == thread.id || menuOpen ? Color.white.opacity(0.08) : Color.clear) + .background(isSelected || isHoveredThread == thread.id || menuOpen ? VColor.hoverOverlay.opacity(0.08) : Color.clear) .clipShape(RoundedRectangle(cornerRadius: VRadius.md)) .overlay(alignment: .topTrailing) { if menuOpen { @@ -349,10 +349,10 @@ struct MainWindowView: View { Spacer() - // Parental Controls + // Control Center VColor.surfaceBorder.frame(height: 1) - ParentalControlsMenuButton( + ControlCenterMenuButton( onSettings: { windowState.togglePanel(.settings) }, onSkills: { windowState.togglePanel(.agent) }, onDirectory: { windowState.togglePanel(.directory) }, @@ -453,73 +453,118 @@ struct MainWindowView: View { } ) } + } else if let panel = windowState.activePanel, panel != .activity { + // Full-window panels: settings, skills, debug, doctor + fullWindowPanel(panel) } else { - VSplitView(panelWidth: $sidePanelWidth, showPanel: windowState.activePanel != nil, main: { - if let viewModel = threadManager.activeViewModel { - ChatView( - messages: viewModel.messages, - inputText: Binding( - get: { viewModel.inputText }, - set: { viewModel.inputText = $0 } - ), - hasAPIKey: windowState.hasAPIKey, - isThinking: viewModel.isThinking, - isSending: viewModel.isSending, - errorText: viewModel.errorText, - pendingQueuedCount: viewModel.pendingQueuedCount, - suggestion: viewModel.suggestion, - pendingAttachments: viewModel.pendingAttachments, - isRecording: viewModel.isRecording, - onOpenSettings: { - // Always provide an immediate, visible fallback. - windowState.activePanel = .settings - Self.openSettings() - }, - onSend: viewModel.sendMessage, - onStop: viewModel.stopGenerating, - onDismissError: viewModel.dismissError, - onAcceptSuggestion: viewModel.acceptSuggestion, - onAttach: { Self.openFilePicker(viewModel: viewModel) }, - onRemoveAttachment: { viewModel.removeAttachment(id: $0) }, - onDropFiles: { urls in urls.forEach { viewModel.addAttachment(url: $0) } }, - onDropImageData: { data, name in - let filename: String - if let name { - let basename = (name as NSString).lastPathComponent - let base = (basename as NSString).deletingPathExtension - filename = base.isEmpty ? "Dropped Image.png" : "\(base).png" - } else { - filename = "Dropped Image.png" - } - viewModel.addAttachment(imageData: data, filename: filename) - }, - onPaste: { viewModel.addAttachmentFromPasteboard() }, - onMicrophoneToggle: onMicrophoneToggle, - onConfirmationAllow: { requestId in viewModel.respondToConfirmation(requestId: requestId, decision: "allow") }, - onConfirmationDeny: { requestId in viewModel.respondToConfirmation(requestId: requestId, decision: "deny") }, - onAddTrustRule: { toolName, pattern, scope, decision in return viewModel.addTrustRule(toolName: toolName, pattern: pattern, scope: scope, decision: decision) }, - onSurfaceAction: { surfaceId, actionId, data in viewModel.sendSurfaceAction(surfaceId: surfaceId, actionId: actionId, data: data) }, - onRegenerate: { viewModel.regenerateLastMessage() }, - sessionError: viewModel.sessionError, - onRetry: { viewModel.retryAfterSessionError() }, - onDismissSessionError: { viewModel.dismissSessionError() }, - onCopyDebugInfo: { viewModel.copySessionErrorDebugDetails() }, - watchSession: ambientAgent.activeWatchSession, - onStopWatch: { viewModel.stopWatchSession() }, - onOpenActivity: { messageId in - print("DEBUG: onOpenActivity called with message ID: \(messageId)") - windowState.toggleActivityPanel(with: messageId) - print("DEBUG: activePanel is now \(String(describing: windowState.activePanel))") - }, - isActivityPanelOpen: windowState.activePanel == .activity + VSplitView(panelWidth: $sidePanelWidth, showPanel: windowState.activePanel == .activity, main: { + chatView + }, panel: { + if windowState.activePanel == .activity { + ActivityPanel( + toolCalls: windowState.activityToolCalls, + onClose: { windowState.activePanel = nil } ) } - }, panel: { - panelContent }) } } + @ViewBuilder + private var chatView: some View { + if let viewModel = threadManager.activeViewModel { + ChatView( + messages: viewModel.messages, + inputText: Binding( + get: { viewModel.inputText }, + set: { viewModel.inputText = $0 } + ), + hasAPIKey: windowState.hasAPIKey, + isThinking: viewModel.isThinking, + isSending: viewModel.isSending, + errorText: viewModel.errorText, + pendingQueuedCount: viewModel.pendingQueuedCount, + suggestion: viewModel.suggestion, + pendingAttachments: viewModel.pendingAttachments, + isRecording: viewModel.isRecording, + onOpenSettings: { + windowState.activePanel = .settings + }, + onSend: viewModel.sendMessage, + onStop: viewModel.stopGenerating, + onDismissError: viewModel.dismissError, + onAcceptSuggestion: viewModel.acceptSuggestion, + onAttach: { Self.openFilePicker(viewModel: viewModel) }, + onRemoveAttachment: { viewModel.removeAttachment(id: $0) }, + onDropFiles: { urls in urls.forEach { viewModel.addAttachment(url: $0) } }, + onDropImageData: { data, name in + let filename: String + if let name { + let basename = (name as NSString).lastPathComponent + let base = (basename as NSString).deletingPathExtension + filename = base.isEmpty ? "Dropped Image.png" : "\(base).png" + } else { + filename = "Dropped Image.png" + } + viewModel.addAttachment(imageData: data, filename: filename) + }, + onPaste: { viewModel.addAttachmentFromPasteboard() }, + onMicrophoneToggle: onMicrophoneToggle, + onConfirmationAllow: { requestId in viewModel.respondToConfirmation(requestId: requestId, decision: "allow") }, + onConfirmationDeny: { requestId in viewModel.respondToConfirmation(requestId: requestId, decision: "deny") }, + onAddTrustRule: { toolName, pattern, scope, decision in return viewModel.addTrustRule(toolName: toolName, pattern: pattern, scope: scope, decision: decision) }, + onSurfaceAction: { surfaceId, actionId, data in viewModel.sendSurfaceAction(surfaceId: surfaceId, actionId: actionId, data: data) }, + onRegenerate: { viewModel.regenerateLastMessage() }, + sessionError: viewModel.sessionError, + onRetry: { viewModel.retryAfterSessionError() }, + onDismissSessionError: { viewModel.dismissSessionError() }, + onCopyDebugInfo: { viewModel.copySessionErrorDebugDetails() }, + watchSession: ambientAgent.activeWatchSession, + onStopWatch: { viewModel.stopWatchSession() }, + onOpenActivity: { toolCalls in + windowState.toggleActivityPanel(with: toolCalls) + }, + isActivityPanelOpen: windowState.activePanel == .activity + ) + } + } + + @ViewBuilder + private func fullWindowPanel(_ panel: SidePanelType) -> some View { + switch panel { + case .settings: + SettingsPanel(onClose: { windowState.activePanel = nil }, store: settingsStore, daemonClient: daemonClient, threadManager: threadManager) + case .agent: + AgentPanel(onClose: { windowState.activePanel = nil }, onInvokeSkill: { skill in + if threadManager.activeViewModel == nil { + threadManager.createThread() + } + if let viewModel = threadManager.activeViewModel { + viewModel.pendingSkillInvocation = SkillInvocationData( + name: skill.name, + emoji: skill.emoji, + description: skill.description + ) + viewModel.inputText = "Use the \(skill.name) skill" + viewModel.sendMessage() + viewModel.pendingSkillInvocation = nil + } + windowState.activePanel = nil + }, daemonClient: daemonClient) + case .debug: + DebugPanel( + traceStore: traceStore, + daemonClient: daemonClient, + activeSessionId: threadManager.activeViewModel?.sessionId, + onClose: { windowState.activePanel = nil } + ) + case .doctor: + DoctorPanel(onClose: { windowState.activePanel = nil }) + default: + EmptyView() + } + } + @MainActor private static func openFilePicker(viewModel: ChatViewModel) { let panel = NSOpenPanel() @@ -535,22 +580,6 @@ struct MainWindowView: View { } } - @MainActor - private static func openSettings() { - NSApp.setActivationPolicy(.regular) - - let selector = Selector(("showSettingsWindow:")) - if let delegate = NSApp.delegate as? NSObject, delegate.responds(to: selector) { - _ = delegate.perform(selector, with: nil) - } else { - _ = NSApp.sendAction(selector, to: nil, from: nil) - } - - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - NSApp.activate(ignoringOtherApps: true) - } - } - // MARK: - Dynamic Workspace /// Height reserved at the bottom so HTML content scrolls past the floating composer. @@ -838,59 +867,6 @@ struct MainWindowView: View { } } - @ViewBuilder - private var panelContent: some View { - if let panel = windowState.activePanel { - switch panel { - case .generated: - Color.clear.frame(width: 0, height: 0) - .onAppear { windowState.activePanel = nil } - case .agent: - AgentPanel(onClose: { windowState.activePanel = nil }, onInvokeSkill: { skill in - if threadManager.activeViewModel == nil { - threadManager.createThread() - } - if let viewModel = threadManager.activeViewModel { - viewModel.pendingSkillInvocation = SkillInvocationData( - name: skill.name, - emoji: skill.emoji, - description: skill.description - ) - viewModel.inputText = "Use the \(skill.name) skill" - viewModel.sendMessage() - // Clear leaked metadata if sendMessage() returned early - viewModel.pendingSkillInvocation = nil - } - }, daemonClient: daemonClient) - case .settings: - SettingsPanel(onClose: { windowState.activePanel = nil }, store: settingsStore, daemonClient: daemonClient, threadManager: threadManager) - case .directory: - Color.clear.frame(width: 0, height: 0) - .onAppear { /* handled full-screen in chatContentView */ } - case .debug: - DebugPanel( - traceStore: traceStore, - daemonClient: daemonClient, - activeSessionId: threadManager.activeViewModel?.sessionId, - onClose: { windowState.activePanel = nil } - ) - case .doctor: - DoctorPanel(onClose: { windowState.activePanel = nil }) - case .activity: - if let viewModel = threadManager.activeViewModel, - let messageId = windowState.activityMessageId { - ActivityPanel( - viewModel: viewModel, - messageId: messageId, - onClose: { windowState.activePanel = nil } - ) - } else { - Color.clear.frame(width: 0, height: 0) - .onAppear { windowState.activePanel = nil } - } - } - } - } } private struct ZoomIndicatorView: View { @@ -933,7 +909,7 @@ private struct NewConversationButton: View { .frame(maxWidth: .infinity, alignment: .leading) .padding(.horizontal, VSpacing.sm) .padding(.vertical, VSpacing.sm) - .background(VColor.surface) + .background(isHovered ? VColor.hoverOverlay.opacity(0.06) : Color.clear) .clipShape(RoundedRectangle(cornerRadius: VRadius.md)) .overlay( RoundedRectangle(cornerRadius: VRadius.md) @@ -951,7 +927,7 @@ private struct NewConversationButton: View { } } -private struct ParentalControlsMenuButton: View { +private struct ControlCenterMenuButton: View { let onSettings: () -> Void let onSkills: () -> Void let onDirectory: () -> Void @@ -967,14 +943,13 @@ private struct ParentalControlsMenuButton: View { } } label: { HStack(spacing: VSpacing.md) { - Text("P") - .font(.system(size: 13, weight: .semibold)) - .foregroundColor(.white) - .frame(width: 32, height: 32) - .background(VColor.textMuted) - .clipShape(Circle()) - - Text("Parental Controls") + Image("OwlIcon") + .resizable() + .aspectRatio(contentMode: .fit) + .foregroundColor(VColor.textSecondary) + .frame(width: 20, height: 20) + + Text("Control Center") .font(.system(size: 13, weight: .medium)) .foregroundColor(VColor.textPrimary) @@ -986,7 +961,7 @@ private struct ParentalControlsMenuButton: View { } .padding(.horizontal, VSpacing.lg) .padding(.vertical, VSpacing.md) - .background(isHovered || showDrawer ? Color.white.opacity(0.05) : Color.clear) + .background(isHovered || showDrawer ? VColor.hoverOverlay.opacity(0.05) : Color.clear) .contentShape(Rectangle()) .onHover { hovering in isHovered = hovering @@ -1065,7 +1040,7 @@ private struct DrawerMenuItem: View { } .padding(.horizontal, VSpacing.lg) .padding(.vertical, VSpacing.sm) - .background(isHovered ? Color.white.opacity(0.06) : Color.clear) + .background(isHovered ? VColor.hoverOverlay.opacity(0.06) : Color.clear) .contentShape(Rectangle()) } .buttonStyle(.plain) @@ -1091,7 +1066,7 @@ private struct ArchivePopup: View { .foregroundColor(VColor.textPrimary) .padding(.horizontal, VSpacing.md) .padding(.vertical, VSpacing.xs + 2) - .background(isHovered ? Color.white.opacity(0.12) : VColor.surface) + .background(isHovered ? VColor.hoverOverlay.opacity(0.12) : VColor.surface) .clipShape(RoundedRectangle(cornerRadius: VRadius.md)) .overlay( RoundedRectangle(cornerRadius: VRadius.md) diff --git a/clients/macos/vellum-assistant/Features/MainWindow/Panels/AgentPanel.swift b/clients/macos/vellum-assistant/Features/MainWindow/Panels/AgentPanel.swift index 038d0ef4914..39b9e2c8d37 100644 --- a/clients/macos/vellum-assistant/Features/MainWindow/Panels/AgentPanel.swift +++ b/clients/macos/vellum-assistant/Features/MainWindow/Panels/AgentPanel.swift @@ -70,7 +70,7 @@ struct AgentPanel: View { .foregroundColor(VColor.textMuted) .padding(.horizontal, VSpacing.sm) .padding(.vertical, VSpacing.xxs) - .background(Slate._800) + .background(VColor.backgroundSubtle) .clipShape(RoundedRectangle(cornerRadius: VRadius.sm)) } @@ -184,7 +184,7 @@ struct AgentPanel: View { } } .padding(VSpacing.md) - .background(Slate._800) + .background(VColor.backgroundSubtle) .clipShape(RoundedRectangle(cornerRadius: VRadius.md)) // Sort picker @@ -292,7 +292,7 @@ struct AgentPanel: View { .foregroundColor(Emerald._400) } .padding(VSpacing.lg) - .background(Slate._900) + .background(VColor.surfaceSubtle) .clipShape(RoundedRectangle(cornerRadius: VRadius.md)) .overlay( RoundedRectangle(cornerRadius: VRadius.md) @@ -385,7 +385,7 @@ struct AgentPanel: View { .padding(.horizontal, VSpacing.lg) .padding(.vertical, VSpacing.sm) .foregroundColor(isHovered ? Slate._900 : Emerald._400) - .background(isHovered ? Emerald._400 : Slate._800) + .background(isHovered ? Emerald._400 : VColor.backgroundSubtle) .clipShape(RoundedRectangle(cornerRadius: VRadius.md)) .overlay( RoundedRectangle(cornerRadius: VRadius.md) @@ -436,7 +436,7 @@ struct AgentPanel: View { .padding(.leading, 24 + VSpacing.md) } .padding(VSpacing.lg) - .background(Slate._900) + .background(VColor.surfaceSubtle) .clipShape(RoundedRectangle(cornerRadius: VRadius.md)) .overlay( RoundedRectangle(cornerRadius: VRadius.md) @@ -601,7 +601,7 @@ struct AgentPanel: View { } .padding(VSpacing.md) .frame(maxWidth: .infinity, alignment: .leading) - .background(Slate._900) + .background(VColor.surfaceSubtle) .clipShape(RoundedRectangle(cornerRadius: VRadius.md)) } @@ -621,7 +621,7 @@ struct AgentPanel: View { .padding(VSpacing.md) } .frame(maxHeight: 250) - .background(Slate._900) + .background(VColor.surfaceSubtle) .clipShape(RoundedRectangle(cornerRadius: VRadius.md)) .overlay( RoundedRectangle(cornerRadius: VRadius.md) @@ -712,7 +712,7 @@ struct AgentPanel: View { .frame(maxWidth: .infinity) .padding(.vertical, VSpacing.md) .foregroundColor(isSuccess ? Emerald._400 : (hoveredDetailInstall && !isInstalling ? Slate._900 : Emerald._400)) - .background(isSuccess ? Emerald._400.opacity(0.15) : (hoveredDetailInstall && !isInstalling ? Emerald._400 : Slate._800)) + .background(isSuccess ? Emerald._400.opacity(0.15) : (hoveredDetailInstall && !isInstalling ? Emerald._400 : VColor.backgroundSubtle)) .clipShape(RoundedRectangle(cornerRadius: VRadius.md)) .overlay( RoundedRectangle(cornerRadius: VRadius.md) @@ -818,7 +818,7 @@ struct AgentPanel: View { .padding(.horizontal, VSpacing.lg) .padding(.vertical, VSpacing.sm) .foregroundColor(useHovered ? Slate._900 : Emerald._400) - .background(useHovered ? Emerald._400 : Slate._800) + .background(useHovered ? Emerald._400 : VColor.backgroundSubtle) .clipShape(RoundedRectangle(cornerRadius: VRadius.md)) .overlay( RoundedRectangle(cornerRadius: VRadius.md) @@ -852,11 +852,11 @@ struct AgentPanel: View { .padding(.horizontal, VSpacing.md) .padding(.vertical, VSpacing.xs) .foregroundColor(viewHovered ? VColor.textPrimary : VColor.textMuted) - .background(viewHovered ? Slate._700 : Slate._800) + .background(viewHovered ? VColor.ghostHover : VColor.backgroundSubtle) .clipShape(RoundedRectangle(cornerRadius: VRadius.md)) .overlay( RoundedRectangle(cornerRadius: VRadius.md) - .stroke(Slate._600.opacity(0.7), lineWidth: 1) + .stroke(VColor.surfaceBorder.opacity(0.7), lineWidth: 1) ) } .buttonStyle(.plain) @@ -876,11 +876,11 @@ struct AgentPanel: View { .padding(.horizontal, VSpacing.sm) .padding(.vertical, VSpacing.xs) .foregroundColor(deleteHovered ? Rose._400 : VColor.textMuted) - .background(deleteHovered ? Rose._400.opacity(0.15) : Slate._800) + .background(deleteHovered ? Rose._400.opacity(0.15) : VColor.backgroundSubtle) .clipShape(RoundedRectangle(cornerRadius: VRadius.md)) .overlay( RoundedRectangle(cornerRadius: VRadius.md) - .stroke(deleteHovered ? Rose._500.opacity(0.6) : Slate._600.opacity(0.7), lineWidth: 1) + .stroke(deleteHovered ? Rose._500.opacity(0.6) : VColor.surfaceBorder.opacity(0.7), lineWidth: 1) ) } .buttonStyle(.plain) @@ -929,7 +929,7 @@ struct AgentPanel: View { .frame(maxWidth: .infinity, alignment: .leading) } .frame(maxHeight: 300) - .background(Slate._800) + .background(VColor.backgroundSubtle) .clipShape(RoundedRectangle(cornerRadius: VRadius.md)) .overlay( RoundedRectangle(cornerRadius: VRadius.md) @@ -938,7 +938,7 @@ struct AgentPanel: View { } } .padding(VSpacing.lg) - .background(Slate._900) + .background(VColor.surfaceSubtle) .clipShape(RoundedRectangle(cornerRadius: VRadius.md)) .overlay( RoundedRectangle(cornerRadius: VRadius.md) diff --git a/clients/macos/vellum-assistant/Features/MainWindow/Panels/AppDirectoryView.swift b/clients/macos/vellum-assistant/Features/MainWindow/Panels/AppDirectoryView.swift index c261078391f..c9e209728ac 100644 --- a/clients/macos/vellum-assistant/Features/MainWindow/Panels/AppDirectoryView.swift +++ b/clients/macos/vellum-assistant/Features/MainWindow/Panels/AppDirectoryView.swift @@ -8,7 +8,6 @@ struct AppDirectoryView: View { let onOpenApp: (UiSurfaceShowMessage) -> Void @State private var searchText = "" - @State private var isSearchExpanded = false @State private var displayItems: [DirectoryAppItem] = [] @State private var isLoading = false @State private var hoveredAppId: String? @@ -18,148 +17,83 @@ struct AppDirectoryView: View { @State private var pendingResponses = 0 private let columns = [ - GridItem(.flexible(), spacing: VSpacing.lg), - GridItem(.flexible(), spacing: VSpacing.lg), - GridItem(.flexible(), spacing: VSpacing.lg), + GridItem(.flexible(minimum: 200), spacing: VSpacing.lg, alignment: .top), + GridItem(.flexible(minimum: 200), spacing: VSpacing.lg, alignment: .top), ] var body: some View { - ZStack { - VColor.background.ignoresSafeArea() - - VStack(spacing: 0) { - // Top bar - HStack { - backButton - - Spacer() - - Text("App Directory") - .font(.system(size: 18, weight: .semibold)) + VSidePanel(title: "Directory", onClose: onBack, pinnedContent: { + // Search bar (only when there are items to search) + if !displayItems.isEmpty || !searchText.isEmpty { + HStack(spacing: VSpacing.sm) { + Image(systemName: "magnifyingglass") + .font(.system(size: 12, weight: .medium)) + .foregroundColor(VColor.textMuted) + + TextField("Search apps...", text: $searchText) + .textFieldStyle(.plain) + .font(VFont.body) .foregroundColor(VColor.textPrimary) - Spacer() - - // Collapsible search - if !displayItems.isEmpty || !searchText.isEmpty { - HStack(spacing: VSpacing.sm) { - if isSearchExpanded { - TextField("Search apps...", text: $searchText) - .textFieldStyle(.plain) - .font(VFont.body) - .foregroundColor(VColor.textPrimary) - .frame(width: 160) - - if !searchText.isEmpty { - Button(action: { searchText = "" }) { - Image(systemName: "xmark.circle.fill") - .font(.system(size: 12)) - .foregroundColor(VColor.textMuted) - } - .buttonStyle(.plain) - } - } - - Button(action: { - withAnimation(VAnimation.fast) { - isSearchExpanded.toggle() - if !isSearchExpanded { - searchText = "" - } - } - }) { - Image(systemName: "magnifyingglass") - .font(.system(size: 13, weight: .medium)) - .foregroundColor(VColor.textSecondary) - } - .buttonStyle(.plain) + if !searchText.isEmpty { + Button(action: { searchText = "" }) { + Image(systemName: "xmark.circle.fill") + .font(.system(size: 12)) + .foregroundColor(VColor.textMuted) } - .padding(.horizontal, isSearchExpanded ? VSpacing.md : VSpacing.sm) - .padding(.vertical, VSpacing.sm) - .background( - isSearchExpanded - ? AnyShapeStyle(VColor.surface) - : AnyShapeStyle(.clear) - ) - .clipShape(Capsule()) - .overlay( - isSearchExpanded - ? Capsule().stroke(VColor.surfaceBorder, lineWidth: 1) - : nil - ) - } else { - // Balance the back button width when no search - Color.clear.frame(width: 80, height: 1) + .buttonStyle(.plain) } } - .padding(.horizontal, VSpacing.xl) - .padding(.top, VSpacing.md) - .padding(.bottom, VSpacing.lg) - - // Content - ScrollView { - if isLoading { - HStack { - Spacer() - ProgressView() - .controlSize(.regular) - Spacer() - } - .frame(height: 300) - } else if displayItems.isEmpty { - VEmptyState( - title: "No apps yet", - subtitle: "Apps built with your assistant will appear here", - icon: "square.grid.2x2" - ) - .frame(maxWidth: .infinity) - .padding(.top, VSpacing.xxxl) - } else if filteredItems.isEmpty { - VEmptyState( - title: "No results", - subtitle: "No apps matched \"\(searchText)\"", - icon: "magnifyingglass" - ) - .frame(maxWidth: .infinity) - .padding(.top, VSpacing.xxxl) - } else { - LazyVGrid(columns: columns, spacing: VSpacing.lg) { - ForEach(filteredItems) { item in - appCard(item) - } - } - .padding(.horizontal, VSpacing.xl) - .padding(.bottom, VSpacing.xl) + .padding(.horizontal, VSpacing.md) + .padding(.vertical, VSpacing.sm) + .background(VColor.surface) + .clipShape(RoundedRectangle(cornerRadius: VRadius.md)) + .overlay( + RoundedRectangle(cornerRadius: VRadius.md) + .stroke(VColor.surfaceBorder, lineWidth: 1) + ) + .padding(.horizontal, VSpacing.lg) + .padding(.vertical, VSpacing.md) + + Divider().background(VColor.surfaceBorder) + } + }) { + if isLoading { + HStack { + Spacer() + ProgressView() + .controlSize(.regular) + Spacer() + } + .frame(height: 300) + } else if displayItems.isEmpty { + VEmptyState( + title: "No apps yet", + subtitle: "Apps built with your assistant will appear here", + icon: "square.grid.2x2" + ) + .frame(maxWidth: .infinity) + .padding(.top, VSpacing.xxxl) + } else if filteredItems.isEmpty { + VEmptyState( + title: "No results", + subtitle: "No apps matched \"\(searchText)\"", + icon: "magnifyingglass" + ) + .frame(maxWidth: .infinity) + .padding(.top, VSpacing.xxxl) + } else { + LazyVGrid(columns: columns, spacing: VSpacing.lg) { + ForEach(filteredItems) { item in + appCard(item) } } + .padding(.bottom, VSpacing.md) } } .onAppear { fetchApps() } } - // MARK: - Back Button - - private var backButton: some View { - Button(action: onBack) { - HStack(spacing: VSpacing.xs) { - Image(systemName: "chevron.left") - .font(.system(size: 12, weight: .semibold)) - Text("Chat") - .font(VFont.bodyMedium) - } - .foregroundColor(VColor.textPrimary) - .padding(.horizontal, VSpacing.md) - .padding(.vertical, VSpacing.sm) - .background( - Capsule() - .fill(VColor.surface.opacity(0.85)) - .overlay(Capsule().stroke(VColor.surfaceBorder, lineWidth: 1)) - ) - } - .buttonStyle(.plain) - .accessibilityLabel("Back to chat") - } - // MARK: - App Card private func appCard(_ item: DirectoryAppItem) -> some View { @@ -235,12 +169,13 @@ struct AppDirectoryView: View { } .padding(VSpacing.md) } - .background(isHovered ? Slate._800 : VColor.surface) + .background(isHovered ? VColor.ghostHover : VColor.surface) .clipShape(RoundedRectangle(cornerRadius: VRadius.lg)) .overlay( RoundedRectangle(cornerRadius: VRadius.lg) - .stroke(isHovered ? VColor.surfaceBorder.opacity(0.8) : VColor.surfaceBorder.opacity(0.4), lineWidth: 1) + .stroke(VColor.surfaceBorder, lineWidth: 1) ) + .shadow(color: .black.opacity(0.06), radius: 4, y: 2) .scaleEffect(isHovered ? 1.02 : 1.0) .animation(VAnimation.fast, value: isHovered) .contentShape(Rectangle()) diff --git a/clients/macos/vellum-assistant/Features/MainWindow/Panels/SettingsPanel.swift b/clients/macos/vellum-assistant/Features/MainWindow/Panels/SettingsPanel.swift index 9c2692645bf..1ec5fc77be3 100644 --- a/clients/macos/vellum-assistant/Features/MainWindow/Panels/SettingsPanel.swift +++ b/clients/macos/vellum-assistant/Features/MainWindow/Panels/SettingsPanel.swift @@ -14,6 +14,7 @@ struct SettingsPanel: View { @State private var showingScheduledTasks = false @State private var showingReminders = false @AppStorage("useThreadDrawer") private var useThreadDrawer: Bool = false + @AppStorage("themePreference") private var themePreference: String = "system" var body: some View { VSidePanel(title: "Settings", onClose: onClose) { @@ -68,7 +69,7 @@ struct SettingsPanel: View { } } .padding(VSpacing.lg) - .vCard(background: Slate._900) + .vCard(background: VColor.surfaceSubtle) // BRAVE SEARCH section VStack(alignment: .leading, spacing: VSpacing.md) { @@ -120,7 +121,7 @@ struct SettingsPanel: View { } } .padding(VSpacing.lg) - .vCard(background: Slate._900) + .vCard(background: VColor.surfaceSubtle) // COMPUTER USAGE section VStack(alignment: .leading, spacing: VSpacing.md) { @@ -144,7 +145,7 @@ struct SettingsPanel: View { VSlider(value: $store.maxSteps, range: 1...100, step: 10, showTickMarks: true) } .padding(VSpacing.lg) - .vCard(background: Slate._900) + .vCard(background: VColor.surfaceSubtle) // AMBIENT AGENT section VStack(alignment: .leading, spacing: VSpacing.md) { @@ -164,7 +165,7 @@ struct SettingsPanel: View { } } .padding(VSpacing.lg) - .vCard(background: Slate._900) + .vCard(background: VColor.surfaceSubtle) // DISPLAY section VStack(alignment: .leading, spacing: VSpacing.md) { @@ -172,6 +173,30 @@ struct SettingsPanel: View { .font(VFont.sectionTitle) .foregroundColor(VColor.textPrimary) + HStack { + Text("Theme") + .font(VFont.body) + .foregroundColor(VColor.textSecondary) + Spacer() + Picker("", selection: Binding( + get: { themePreference }, + set: { newValue in + themePreference = newValue + UserDefaults.standard.set(newValue, forKey: "themePreference") + UserDefaults.standard.synchronize() + if let delegate = NSApp.delegate as? AppDelegate { + delegate.applyThemePreference() + } + } + )) { + Text("System").tag("system") + Text("Light").tag("light") + Text("Dark").tag("dark") + } + .pickerStyle(.segmented) + .frame(width: 200) + } + HStack { VStack(alignment: .leading, spacing: VSpacing.xs) { Text("Show thread list drawer") @@ -186,7 +211,7 @@ struct SettingsPanel: View { } } .padding(VSpacing.lg) - .vCard(background: Slate._900) + .vCard(background: VColor.surfaceSubtle) // ARCHIVED THREADS section if !threadManager.archivedThreads.isEmpty { @@ -214,7 +239,7 @@ struct SettingsPanel: View { } } .padding(VSpacing.lg) - .vCard(background: Slate._900) + .vCard(background: VColor.surfaceSubtle) } // PERMISSIONS section @@ -229,7 +254,7 @@ struct SettingsPanel: View { granted: PermissionManager.accessibilityStatus() == .granted ) .padding(VSpacing.md) - .vCard(background: Slate._900) + .vCard(background: VColor.surfaceSubtle) permissionRow( emoji: "\u{1F355}", @@ -237,10 +262,10 @@ struct SettingsPanel: View { granted: PermissionManager.screenRecordingStatus() == .granted ) .padding(VSpacing.md) - .vCard(background: Slate._900) + .vCard(background: VColor.surfaceSubtle) } .padding(VSpacing.lg) - .vCard(background: Slate._900) + .vCard(background: VColor.surfaceSubtle) // SCHEDULED TASKS section if daemonClient != nil { @@ -265,7 +290,7 @@ struct SettingsPanel: View { } } .padding(VSpacing.lg) - .vCard(background: Slate._900) + .vCard(background: VColor.surfaceSubtle) } // REMINDERS section @@ -291,7 +316,7 @@ struct SettingsPanel: View { } } .padding(VSpacing.lg) - .vCard(background: Slate._900) + .vCard(background: VColor.surfaceSubtle) } // TRUST RULES section @@ -319,7 +344,7 @@ struct SettingsPanel: View { } } .padding(VSpacing.lg) - .vCard(background: Slate._900) + .vCard(background: VColor.surfaceSubtle) } // PRIVACY & SECURITY section @@ -339,7 +364,7 @@ struct SettingsPanel: View { } } .padding(VSpacing.lg) - .vCard(background: Slate._900) + .vCard(background: VColor.surfaceSubtle) } } .onAppear { diff --git a/clients/macos/vellum-assistant/Features/Onboarding/APIKeyStepView.swift b/clients/macos/vellum-assistant/Features/Onboarding/APIKeyStepView.swift index 8971b2a96d9..76aef6b251f 100644 --- a/clients/macos/vellum-assistant/Features/Onboarding/APIKeyStepView.swift +++ b/clients/macos/vellum-assistant/Features/Onboarding/APIKeyStepView.swift @@ -6,98 +6,209 @@ struct APIKeyStepView: View { @Bindable var state: OnboardingState @State private var apiKey: String = "" + @State private var hasExistingKey = false + @State private var isEditing = false + @State private var showIcon = false + @State private var showTitle = false @State private var showContent = false - @State private var alreadyConfigured = false @FocusState private var keyFieldFocused: Bool var body: some View { - VStack(spacing: VSpacing.xxl) { - VStack(spacing: VSpacing.md) { - Text("Connect to Claude") - .font(VFont.onboardingTitle) - .foregroundColor(VColor.textPrimary) - - Text("Enter your Anthropic API key to get started.") - .font(VFont.onboardingSubtitle) - .foregroundColor(VColor.textSecondary) - .multilineTextAlignment(.center) - .frame(maxWidth: 400) - } - .opacity(showContent ? 1 : 0) + VStack(spacing: 0) { + Spacer() - if alreadyConfigured { - HStack(spacing: VSpacing.sm) { - Image(systemName: "checkmark.circle.fill") - .foregroundColor(VColor.success) - Text("API key already configured") - .font(VFont.bodyMedium) - .foregroundColor(VColor.textSecondary) + // Icon + Group { + if let url = ResourceBundle.bundle.url(forResource: "stage-3", withExtension: "png"), + let nsImage = NSImage(contentsOf: url) { + Image(nsImage: nsImage) + .resizable() + .interpolation(.none) + .aspectRatio(contentMode: .fit) + } else { + Image("VellyLogo") + .resizable() + .interpolation(.none) + .aspectRatio(contentMode: .fit) } - .opacity(showContent ? 1 : 0) + } + .frame(width: 128, height: 128) + .opacity(showIcon ? 1 : 0) + .scaleEffect(showIcon ? 1 : 0.8) + .padding(.bottom, VSpacing.xxl) - OnboardingButton(title: "Continue", style: .primary) { - state.advance() - } - .opacity(showContent ? 1 : 0) - } else { - SecureField("sk-ant-\u{2026}", text: $apiKey) - .textFieldStyle(.plain) - .font(.system(size: 18, weight: .medium, design: .monospaced)) - .foregroundColor(VColor.textPrimary) - .multilineTextAlignment(.center) - .padding(.horizontal, 20) - .padding(.vertical, VSpacing.lg) - .background( - RoundedRectangle(cornerRadius: VRadius.md) - .fill(VColor.surface.opacity(0.5)) - .overlay( - RoundedRectangle(cornerRadius: VRadius.md) - .stroke(VColor.surfaceBorder.opacity(0.5), lineWidth: 1) + // Title + Text("Add your API key") + .font(.system(size: 32, weight: .regular, design: .serif)) + .foregroundColor(VColor.textPrimary) + .opacity(showTitle ? 1 : 0) + .offset(y: showTitle ? 0 : 8) + .padding(.bottom, VSpacing.md) + + // Subtitle + Text("Enter your Anthropic API key to get started.") + .font(.system(size: 16)) + .foregroundColor(VColor.textSecondary) + .opacity(showTitle ? 1 : 0) + .offset(y: showTitle ? 0 : 8) + + Spacer() + + // Content + VStack(spacing: VSpacing.md) { + if hasExistingKey && !isEditing { + // Show masked key: first 4 chars + dots + last 3 chars + Text(maskedKey) + .font(.system(size: 16, weight: .medium, design: .monospaced)) + .foregroundColor(VColor.textPrimary) + .frame(maxWidth: .infinity) + .padding(.horizontal, 20) + .padding(.vertical, VSpacing.lg) + .background( + RoundedRectangle(cornerRadius: VRadius.lg) + .stroke(VColor.surfaceBorder, lineWidth: 1) + ) + .onTapGesture { + isEditing = true + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + keyFieldFocused = true + } + } + } else { + SecureField("sk-ant-\u{2026}", text: $apiKey) + .textFieldStyle(.plain) + .font(.system(size: 16, weight: .medium, design: .monospaced)) + .foregroundColor(VColor.textPrimary) + .multilineTextAlignment(.center) + .padding(.horizontal, 20) + .padding(.vertical, VSpacing.lg) + .background( + RoundedRectangle(cornerRadius: VRadius.lg) + .stroke(VColor.surfaceBorder, lineWidth: 1) ) - ) - .frame(maxWidth: 360) - .focused($keyFieldFocused) - .opacity(showContent ? 1 : 0) - .onSubmit { - saveAndContinue() + .focused($keyFieldFocused) + .onSubmit { + saveAndContinue() + } } - OnboardingButton( - title: "Save & Continue", - style: .primary, - disabled: apiKey.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty - ) { - saveAndContinue() - } - .opacity(showContent ? 1 : 0) + Button(action: { saveAndContinue() }) { + Text("Save & Continue") + .font(.system(size: 15, weight: .medium)) + .foregroundColor(adaptiveColor( + light: .white, + dark: .white + )) + .frame(maxWidth: .infinity) + .padding(.vertical, VSpacing.lg) + .background( + RoundedRectangle(cornerRadius: VRadius.lg) + .fill(apiKey.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty + ? adaptiveColor( + light: Color(nsColor: NSColor(red: 0.12, green: 0.12, blue: 0.12, alpha: 0.3)), + dark: Violet._600.opacity(0.3) + ) + : adaptiveColor( + light: Color(nsColor: NSColor(red: 0.12, green: 0.12, blue: 0.12, alpha: 1)), + dark: Violet._600 + ) + ) + ) + } + .buttonStyle(.plain) + .disabled(apiKey.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) + .onHover { hovering in + if hovering { NSCursor.pointingHand.push() } else { NSCursor.pop() } + } - Link(destination: URL(string: "https://console.anthropic.com/settings/keys")!) { - Text("Get your API key at console.anthropic.com") - .font(VFont.caption) - .foregroundColor(VColor.accent) - } - .opacity(showContent ? 1 : 0) + HStack(spacing: VSpacing.lg) { + Link(destination: URL(string: "https://console.anthropic.com/settings/keys")!) { + Text("Get an API key") + .font(.system(size: 13)) + .foregroundColor(adaptiveColor(light: VColor.accent, dark: .white)) + } + .onHover { hovering in + if hovering { NSCursor.pointingHand.push() } else { NSCursor.pop() } + } - OnboardingButton(title: "Skip", style: .ghost) { - state.advance() - } - .opacity(showContent ? 1 : 0) + Button(action: { goBack() }) { + Text("Back") + .font(.system(size: 13)) + .foregroundColor(VColor.textMuted) + } + .buttonStyle(.plain) + .onHover { hovering in + if hovering { NSCursor.pointingHand.push() } else { NSCursor.pop() } + } + } + .padding(.top, VSpacing.xs) } + .padding(.horizontal, VSpacing.xxl) + .padding(.bottom, VSpacing.xxl) + .opacity(showContent ? 1 : 0) + .offset(y: showContent ? 0 : 12) } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background( + ZStack { + VColor.background + + // Purple glow at bottom + RadialGradient( + colors: [ + Violet._600.opacity(0.15), + Violet._700.opacity(0.05), + Color.clear + ], + center: .bottom, + startRadius: 20, + endRadius: 350 + ) + + // Subtle secondary glow offset to bottom-right + RadialGradient( + colors: [ + Violet._400.opacity(0.08), + Color.clear + ], + center: UnitPoint(x: 0.7, y: 1.0), + startRadius: 10, + endRadius: 250 + ) + } + .ignoresSafeArea() + ) .onAppear { - if APIKeyManager.getKey() != nil { - alreadyConfigured = true + if let existingKey = APIKeyManager.getKey() { + apiKey = existingKey + hasExistingKey = true } - DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { - withAnimation(.easeOut(duration: 0.5)) { - showContent = true - } - if !alreadyConfigured { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { - keyFieldFocused = true - } - } + withAnimation(.easeOut(duration: 0.5).delay(0.2)) { + showIcon = true } + withAnimation(.easeOut(duration: 0.5).delay(0.5)) { + showTitle = true + } + withAnimation(.easeOut(duration: 0.5).delay(0.8)) { + showContent = true + } + DispatchQueue.main.asyncAfter(deadline: .now() + 1.3) { + keyFieldFocused = true + } + } + } + + private var maskedKey: String { + guard apiKey.count > 7 else { return String(repeating: "\u{2022}", count: apiKey.count) } + let prefix = String(apiKey.prefix(4)) + let suffix = String(apiKey.suffix(3)) + let dots = String(repeating: "\u{2022}", count: min(apiKey.count - 7, 20)) + return prefix + dots + suffix + } + + private func goBack() { + withAnimation(.spring(duration: 0.6, bounce: 0.15)) { + state.currentStep = 0 } } @@ -111,12 +222,12 @@ struct APIKeyStepView: View { #Preview { ZStack { - VColor.background + VColor.background.ignoresSafeArea() APIKeyStepView(state: { let s = OnboardingState() s.currentStep = 2 return s }()) } - .frame(width: 520, height: 400) + .frame(width: 460, height: 520) } diff --git a/clients/macos/vellum-assistant/Features/Onboarding/MeadowBackground.swift b/clients/macos/vellum-assistant/Features/Onboarding/MeadowBackground.swift index 0b34dd2d7b9..79638424114 100644 --- a/clients/macos/vellum-assistant/Features/Onboarding/MeadowBackground.swift +++ b/clients/macos/vellum-assistant/Features/Onboarding/MeadowBackground.swift @@ -13,6 +13,7 @@ struct MeadowBackground: View { .resizable() .interpolation(.none) .aspectRatio(contentMode: .fill) + .frame(maxHeight: .infinity, alignment: .bottom) } } } diff --git a/clients/macos/vellum-assistant/Features/Onboarding/OnboardingFlowView.swift b/clients/macos/vellum-assistant/Features/Onboarding/OnboardingFlowView.swift index cb0ab5ce860..ce68c620ace 100644 --- a/clients/macos/vellum-assistant/Features/Onboarding/OnboardingFlowView.swift +++ b/clients/macos/vellum-assistant/Features/Onboarding/OnboardingFlowView.swift @@ -10,109 +10,101 @@ struct OnboardingFlowView: View { var body: some View { GeometryReader { geometry in - ZStack { - VColor.background - .ignoresSafeArea() + ZStack { + VColor.background.ignoresSafeArea() - // Dimmed mock chrome — gives the "chat UI behind" effect + if state.currentStep == 0 { + // Step 0: Full-window welcome screen + WakeUpStepView(state: state) + .transition( + .asymmetric( + insertion: .opacity.combined(with: .offset(y: 12)), + removal: .opacity.combined(with: .offset(y: -8)) + ) + ) + .id(state.currentStep) + } else if state.currentStep == 2 { + // Step 2: Full-window API key screen + APIKeyStepView(state: state) + .transition( + .asymmetric( + insertion: .opacity.combined(with: .offset(y: 12)), + removal: .opacity.combined(with: .offset(y: -8)) + ) + ) + .id(state.currentStep) + } else if state.currentStep <= 7 { + // Steps 1-7: Egg + content panel layout VStack(spacing: 0) { - mockToolbar - Spacer() - mockInputBar - } - .opacity(0.25) - .allowsHitTesting(false) - - if state.currentStep <= 7 { - // Vertical card layout (steps 0-7) - VStack(spacing: 0) { - // TOP: Meadow background + stage image - ZStack { - MeadowBackground() - - OnboardingStageImage(currentStep: state.currentStep) - .frame(maxWidth: .infinity, maxHeight: .infinity) - .padding(VSpacing.xxl) - } - .frame(height: 350) - .clipped() + // TOP: Stage image (egg) + OnboardingStageImage(currentStep: state.currentStep) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .padding(.horizontal, VSpacing.xxl) - // BOTTOM: Dark content panel - VStack(spacing: VSpacing.lg) { - Group { - switch state.currentStep { - case 0: - WakeUpStepView(state: state) - case 1: - NamingStepView(state: state) - case 2: - APIKeyStepView(state: state) - case 3: - FnKeyStepView(state: state) - case 4: - SpeechPermissionStepView(state: state) - case 5: - AccessibilityPermissionStepView(state: state) - case 6: - ScreenPermissionStepView(state: state) - case 7: - AliveStepView( - state: state, - onComplete: onComplete, - onOpenSettings: onOpenSettings - ) - default: - EmptyView() - } - } - .transition( - .asymmetric( - insertion: .opacity.combined(with: .offset(y: 12)), - removal: .opacity.combined(with: .offset(y: -8)) + // BOTTOM: Content panel + VStack(spacing: VSpacing.lg) { + Group { + switch state.currentStep { + case 1: + NamingStepView(state: state) + case 2: + APIKeyStepView(state: state) + case 3: + FnKeyStepView(state: state) + case 4: + SpeechPermissionStepView(state: state) + case 5: + AccessibilityPermissionStepView(state: state) + case 6: + ScreenPermissionStepView(state: state) + case 7: + AliveStepView( + state: state, + onComplete: onComplete, + onOpenSettings: onOpenSettings ) - ) - .id(state.currentStep) - - OnboardingProgressDots(currentStep: state.currentStep) - .padding(.top, VSpacing.xs) + default: + EmptyView() + } } - .padding(.horizontal, VSpacing.xxl) - .padding(.top, VSpacing.xl) - .padding(.bottom, VSpacing.xxl) - .frame(maxWidth: .infinity) - .background( - Rectangle() - .fill(.ultraThinMaterial) - .overlay( - Rectangle() - .fill(Meadow.panelBackground) - ) + .transition( + .asymmetric( + insertion: .opacity.combined(with: .offset(y: 12)), + removal: .opacity.combined(with: .offset(y: -8)) + ) ) + .id(state.currentStep) + + OnboardingProgressDots(currentStep: state.currentStep) + .padding(.top, VSpacing.xs) } - .frame(maxWidth: 640) - .clipShape(RoundedRectangle(cornerRadius: VRadius.xl)) - .overlay( - RoundedRectangle(cornerRadius: VRadius.xl) - .stroke(Meadow.panelBorder, lineWidth: 1) - ) - .shadow(color: .black.opacity(0.4), radius: 24, y: 12) - .position(x: geometry.size.width / 2, y: geometry.size.height / 2) - } else { - // Step 8: Interview — manages its own layout - InterviewStepView( - state: state, - daemonClient: daemonClient, - onComplete: onComplete - ) - .transition( - .opacity.combined(with: .scale(scale: 0.97)) - ) - .id(state.currentStep) + .padding(.horizontal, VSpacing.xxl) + .padding(.top, VSpacing.xl) + .padding(.bottom, VSpacing.xxl) + .frame(maxWidth: .infinity) + .background(VColor.background) } + .ignoresSafeArea(edges: .top) + } else { + // Step 8: Interview — manages its own layout + InterviewStepView( + state: state, + daemonClient: daemonClient, + onComplete: onComplete + ) + .transition( + .opacity.combined(with: .scale(scale: 0.97)) + ) + .id(state.currentStep) } - .frame(width: geometry.size.width, height: geometry.size.height) + } } .ignoresSafeArea() + .onChange(of: state.currentStep) { _, newStep in + if newStep > 2 { + onComplete() + } + } } // MARK: - Mock Chrome diff --git a/clients/macos/vellum-assistant/Features/Onboarding/OnboardingState.swift b/clients/macos/vellum-assistant/Features/Onboarding/OnboardingState.swift index ee2ddde4912..4d72dbd5e99 100644 --- a/clients/macos/vellum-assistant/Features/Onboarding/OnboardingState.swift +++ b/clients/macos/vellum-assistant/Features/Onboarding/OnboardingState.swift @@ -27,7 +27,7 @@ final class OnboardingState { private static let currentFlowVersion = 2 var currentStep: Int = 0 - var assistantName: String = "" + var assistantName: String = "Velly" var chosenKey: ActivationKey = .fn var speechGranted: Bool = false var accessibilityGranted: Bool = false @@ -85,7 +85,7 @@ final class OnboardingState { } else { currentStep = saved } - assistantName = UserDefaults.standard.string(forKey: "onboarding.name") ?? "" + assistantName = UserDefaults.standard.string(forKey: "onboarding.name") ?? "Velly" if let raw = UserDefaults.standard.string(forKey: "onboarding.key"), let key = ActivationKey(rawValue: raw) { chosenKey = key @@ -106,11 +106,20 @@ final class OnboardingState { if currentStep > maxStep { currentStep = maxStep } + // Skip naming step (step 1) if restored to it + if onboardingVariant == .default && currentStep == 1 { + currentStep = 2 + } } func advance() { withAnimation(.spring(duration: 0.6, bounce: 0.15)) { currentStep += 1 + // Skip naming step (step 1) — name defaults to "Velly" + // Skip everything after API key (steps 3+) — go straight to chat + if currentStep == 1 { + currentStep = 2 + } } persist() } diff --git a/clients/macos/vellum-assistant/Features/Onboarding/OnboardingWindow.swift b/clients/macos/vellum-assistant/Features/Onboarding/OnboardingWindow.swift index 1c316a59e5b..b6ae3d509c7 100644 --- a/clients/macos/vellum-assistant/Features/Onboarding/OnboardingWindow.swift +++ b/clients/macos/vellum-assistant/Features/Onboarding/OnboardingWindow.swift @@ -40,7 +40,7 @@ final class OnboardingWindow { let hostingController = NSHostingController(rootView: flowView) let window = NSWindow( - contentRect: NSRect(x: 0, y: 0, width: 1366, height: 849), + contentRect: NSRect(x: 0, y: 0, width: 460, height: 520), styleMask: [.titled, .miniaturizable, .resizable, .fullSizeContentView], backing: .buffered, defer: false @@ -53,10 +53,10 @@ final class OnboardingWindow { window.backgroundColor = NSColor(VColor.background) window.isReleasedWhenClosed = false - window.contentMinSize = NSSize(width: 800, height: 600) + window.contentMinSize = NSSize(width: 420, height: 480) - let startWidth: CGFloat = 800 - let startHeight: CGFloat = 680 + let startWidth: CGFloat = 460 + let startHeight: CGFloat = 520 if let visibleFrame = Self.visibleScreenFrame() { let x = visibleFrame.midX - startWidth / 2 let y = visibleFrame.midY - startHeight / 2 diff --git a/clients/macos/vellum-assistant/Features/Onboarding/WakeUpStepView.swift b/clients/macos/vellum-assistant/Features/Onboarding/WakeUpStepView.swift index d747b3d2b26..f167dc366d8 100644 --- a/clients/macos/vellum-assistant/Features/Onboarding/WakeUpStepView.swift +++ b/clients/macos/vellum-assistant/Features/Onboarding/WakeUpStepView.swift @@ -5,58 +5,156 @@ import SwiftUI struct WakeUpStepView: View { @Bindable var state: OnboardingState + @State private var showIcon = false + @State private var showTitle = false @State private var showSubtext = false - @State private var showButton = false - @State private var isHatching = false + @State private var showButtons = false + @State private var isAdvancing = false var body: some View { - VStack(spacing: VSpacing.xxl) { - TypewriterText( - fullText: "Something is ready to hatch.", - speed: 0.06, - font: VFont.onboardingTitle - ) { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.6) { - withAnimation(.easeOut(duration: 0.5)) { - showSubtext = true - } + VStack(spacing: 0) { + Spacer() + + // Icon + Group { + if let url = ResourceBundle.bundle.url(forResource: "stage-3", withExtension: "png"), + let nsImage = NSImage(contentsOf: url) { + Image(nsImage: nsImage) + .resizable() + .interpolation(.none) + .aspectRatio(contentMode: .fit) + } else { + Image("VellyLogo") + .resizable() + .interpolation(.none) + .aspectRatio(contentMode: .fit) } } + .frame(width: 128, height: 128) + .opacity(showIcon ? 1 : 0) + .scaleEffect(showIcon ? 1 : 0.8) + .padding(.bottom, VSpacing.xxl) + + // Title + Text("Create your Velly") + .font(.system(size: 32, weight: .regular, design: .serif)) + .foregroundColor(VColor.textPrimary) + .opacity(showTitle ? 1 : 0) + .offset(y: showTitle ? 0 : 8) + .padding(.bottom, VSpacing.md) - Text("All it needs is you.") - .font(VFont.onboardingSubtitle) + // Subtitle + Text("The safest way to create your personal assistant.") + .font(.system(size: 16)) .foregroundColor(VColor.textSecondary) .opacity(showSubtext ? 1 : 0) .offset(y: showSubtext ? 0 : 8) - .onChange(of: showSubtext) { _, visible in - if visible { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.6) { - withAnimation(.easeOut(duration: 0.5)) { - showButton = true - } - } - } + + Spacer() + + // Buttons + VStack(spacing: VSpacing.md) { + Button(action: { advanceStep() }) { + Text("Start with an API key") + .font(.system(size: 15, weight: .medium)) + .foregroundColor(.white) + .frame(maxWidth: .infinity) + .padding(.vertical, VSpacing.lg) + .background( + RoundedRectangle(cornerRadius: VRadius.lg) + .fill(adaptiveColor( + light: Color(nsColor: NSColor(red: 0.12, green: 0.12, blue: 0.12, alpha: 1)), + dark: Violet._600 + )) + ) + } + .buttonStyle(.plain) + .onHover { hovering in + if hovering { NSCursor.pointingHand.push() } else { NSCursor.pop() } } - OnboardingButton(title: "Hatch it!", style: .primary) { - guard !isHatching else { return } - isHatching = true - state.hasHatched = true - DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { - state.advance() + Button(action: {}) { + Text("Continue with Google") + .font(.system(size: 15, weight: .medium)) + .foregroundColor(VColor.textPrimary) + .frame(maxWidth: .infinity) + .padding(.vertical, VSpacing.lg) + .background( + RoundedRectangle(cornerRadius: VRadius.lg) + .fill(adaptiveColor(light: .white, dark: VColor.surface)) + ) + } + .buttonStyle(.plain) + .onHover { hovering in + if hovering { NSCursor.pointingHand.push() } else { NSCursor.pop() } } } - .opacity(showButton ? 1 : 0) - .offset(y: showButton ? 0 : 8) - .disabled(isHatching) + .padding(.horizontal, VSpacing.xxl) + .padding(.bottom, VSpacing.xxl) + .opacity(showButtons ? 1 : 0) + .offset(y: showButtons ? 0 : 12) + .disabled(isAdvancing) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background( + ZStack { + VColor.background + + // Purple glow at bottom + RadialGradient( + colors: [ + Violet._600.opacity(0.15), + Violet._700.opacity(0.05), + Color.clear + ], + center: .bottom, + startRadius: 20, + endRadius: 350 + ) + + // Subtle secondary glow offset to bottom-right + RadialGradient( + colors: [ + Violet._400.opacity(0.08), + Color.clear + ], + center: UnitPoint(x: 0.7, y: 1.0), + startRadius: 10, + endRadius: 250 + ) + } + .ignoresSafeArea() + ) + .onAppear { + withAnimation(.easeOut(duration: 0.5).delay(0.2)) { + showIcon = true + } + withAnimation(.easeOut(duration: 0.5).delay(0.5)) { + showTitle = true + } + withAnimation(.easeOut(duration: 0.5).delay(0.8)) { + showSubtext = true + } + withAnimation(.easeOut(duration: 0.5).delay(1.1)) { + showButtons = true + } + } + } + + private func advanceStep() { + guard !isAdvancing else { return } + isAdvancing = true + state.hasHatched = true + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { + state.advance() } } } #Preview { ZStack { - MeadowBackground() + VColor.background.ignoresSafeArea() WakeUpStepView(state: OnboardingState()) } - .frame(width: 1366, height: 849) + .frame(width: 460, height: 520) } diff --git a/clients/macos/vellum-assistant/Resources/Assets.xcassets/OwlIcon.imageset/Contents.json b/clients/macos/vellum-assistant/Resources/Assets.xcassets/OwlIcon.imageset/Contents.json new file mode 100644 index 00000000000..c9cc6b2a568 --- /dev/null +++ b/clients/macos/vellum-assistant/Resources/Assets.xcassets/OwlIcon.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "filename" : "owl-solid.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true, + "template-rendering-intent" : "template" + } +} diff --git a/clients/macos/vellum-assistant/Resources/Assets.xcassets/OwlIcon.imageset/owl-solid.svg b/clients/macos/vellum-assistant/Resources/Assets.xcassets/OwlIcon.imageset/owl-solid.svg new file mode 100644 index 00000000000..c76cb48bf57 --- /dev/null +++ b/clients/macos/vellum-assistant/Resources/Assets.xcassets/OwlIcon.imageset/owl-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/clients/macos/vellum-assistant/Resources/Assets.xcassets/VellyLogo.imageset/Contents.json b/clients/macos/vellum-assistant/Resources/Assets.xcassets/VellyLogo.imageset/Contents.json new file mode 100644 index 00000000000..3f7bb84c8c2 --- /dev/null +++ b/clients/macos/vellum-assistant/Resources/Assets.xcassets/VellyLogo.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "velly-logo.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/clients/macos/vellum-assistant/Resources/Assets.xcassets/VellyLogo.imageset/velly-logo.png b/clients/macos/vellum-assistant/Resources/Assets.xcassets/VellyLogo.imageset/velly-logo.png new file mode 100644 index 00000000000..1e223d73da2 Binary files /dev/null and b/clients/macos/vellum-assistant/Resources/Assets.xcassets/VellyLogo.imageset/velly-logo.png differ diff --git a/clients/shared/DesignSystem/Core/Buttons/VButton.swift b/clients/shared/DesignSystem/Core/Buttons/VButton.swift index 430bd02dcee..3553aa1da05 100644 --- a/clients/shared/DesignSystem/Core/Buttons/VButton.swift +++ b/clients/shared/DesignSystem/Core/Buttons/VButton.swift @@ -76,7 +76,7 @@ private struct VButtonStyle: ButtonStyle { case .danger: return isHovered ? Rose._700 : Rose._800 case .ghost: - return isHovered ? Slate._600 : Slate._700 + return isHovered ? VColor.ghostPressed : VColor.ghostHover } } @@ -91,8 +91,8 @@ private struct VButtonStyle: ButtonStyle { if isHovered { return Rose._500 } return Rose._600 case .ghost: - if isPressed { return Slate._600 } - if isHovered { return Slate._700 } + if isPressed { return VColor.ghostPressed } + if isHovered { return VColor.ghostHover } return .clear } } @@ -108,8 +108,8 @@ private struct VButtonStyle: ButtonStyle { private func borderColor(isPressed: Bool) -> Color { switch style { case .ghost: - if isPressed { return Slate._600 } - return Slate._700 + if isPressed { return VColor.ghostPressed } + return VColor.surfaceBorder default: return .clear } diff --git a/clients/shared/DesignSystem/Core/Buttons/VIconButton.swift b/clients/shared/DesignSystem/Core/Buttons/VIconButton.swift index 15fa542e13e..79645454aeb 100644 --- a/clients/shared/DesignSystem/Core/Buttons/VIconButton.swift +++ b/clients/shared/DesignSystem/Core/Buttons/VIconButton.swift @@ -75,12 +75,12 @@ private struct VIconButtonStyle: ButtonStyle { private func backgroundColor(isPressed: Bool) -> Color { if isActive { - if isPressed { return Slate._500 } - if isHovered { return Slate._600 } + if isPressed { return VColor.ghostPressed } + if isHovered { return VColor.ghostHover } return VColor.surfaceBorder } else { - if isPressed { return Slate._600 } - if isHovered { return Slate._700 } + if isPressed { return VColor.ghostPressed } + if isHovered { return VColor.ghostHover } return .clear } } diff --git a/clients/shared/DesignSystem/Core/Inputs/VSlider.swift b/clients/shared/DesignSystem/Core/Inputs/VSlider.swift index 654f90d6e7f..7f3e167a1a6 100644 --- a/clients/shared/DesignSystem/Core/Inputs/VSlider.swift +++ b/clients/shared/DesignSystem/Core/Inputs/VSlider.swift @@ -74,7 +74,7 @@ public struct VSlider: View { ZStack(alignment: .leading) { // Unfilled track (edge-to-edge) Rectangle() - .fill(Slate._700) + .fill(VColor.ghostHover) .frame(height: trackHeight) // Filled track (from left edge to thumb center) @@ -131,7 +131,7 @@ public struct VSlider: View { let tickX = trackWidth * tickFraction + thumbWidth / 2 RoundedRectangle(cornerRadius: 0.5) - .fill(Slate._600) + .fill(VColor.ghostPressed) .frame(width: tickMarkWidth, height: trackHeight) .offset(x: tickX - tickMarkWidth / 2) } diff --git a/clients/shared/DesignSystem/Core/Inputs/VToggle.swift b/clients/shared/DesignSystem/Core/Inputs/VToggle.swift index d43647b4dfd..41355a9369b 100644 --- a/clients/shared/DesignSystem/Core/Inputs/VToggle.swift +++ b/clients/shared/DesignSystem/Core/Inputs/VToggle.swift @@ -44,11 +44,11 @@ public struct VToggle: View { ZStack(alignment: isOn ? .trailing : .leading) { // Track background RoundedRectangle(cornerRadius: VRadius.sm + 2) - .fill(isOn ? Emerald._500 : Slate._700) + .fill(isOn ? Emerald._500 : VColor.toggleOff) .frame(width: trackWidth, height: trackHeight) .overlay( RoundedRectangle(cornerRadius: VRadius.sm + 2) - .stroke(Slate._600, lineWidth: 1) + .stroke(VColor.toggleBorder, lineWidth: 1) ) // Knob diff --git a/clients/shared/DesignSystem/Core/Navigation/VTab.swift b/clients/shared/DesignSystem/Core/Navigation/VTab.swift index 94ad4dbc42e..aabcac9e415 100644 --- a/clients/shared/DesignSystem/Core/Navigation/VTab.swift +++ b/clients/shared/DesignSystem/Core/Navigation/VTab.swift @@ -30,9 +30,9 @@ public struct VTab: View { private var background: Color { switch style { case .pill, .rectangular: - return isSelected ? Slate._200 : (isHovered ? VColor.surfaceBorder.opacity(0.5) : .clear) + return isSelected ? VColor.surfaceBorder : (isHovered ? VColor.surfaceBorder.opacity(0.5) : .clear) case .flat: - return isHovered ? Slate._800 : .clear + return isHovered ? VColor.ghostHover : .clear } } @@ -57,7 +57,7 @@ public struct VTab: View { Spacer().frame(width: 16) } } - .foregroundColor(isSelected && (style == .pill || style == .rectangular) ? Slate._900 : (isSelected ? VColor.textPrimary : VColor.textSecondary)) + .foregroundColor(isSelected && (style == .pill || style == .rectangular) ? VColor.textPrimary : (isSelected ? VColor.textPrimary : VColor.textSecondary)) .padding(.horizontal, VSpacing.lg) .padding(.vertical, VSpacing.sm) .contentShape(RoundedRectangle(cornerRadius: cornerRadius)) @@ -79,7 +79,7 @@ public struct VTab: View { .clipShape(RoundedRectangle(cornerRadius: cornerRadius)) .overlay( RoundedRectangle(cornerRadius: cornerRadius) - .stroke(Slate._300, lineWidth: 1) + .stroke(VColor.surfaceBorder, lineWidth: 1) .opacity((style == .pill || style == .rectangular) && isSelected ? 1 : 0) ) .onHover { hovering in isHovered = hovering } diff --git a/clients/shared/DesignSystem/Tokens/ColorTokens.swift b/clients/shared/DesignSystem/Tokens/ColorTokens.swift index d6e2af7b4f9..8b4bcc4c883 100644 --- a/clients/shared/DesignSystem/Tokens/ColorTokens.swift +++ b/clients/shared/DesignSystem/Tokens/ColorTokens.swift @@ -1,4 +1,9 @@ import SwiftUI +#if os(macOS) +import AppKit +#else +import UIKit +#endif // MARK: - Color Extension @@ -14,6 +19,23 @@ public extension Color { } } +// MARK: - Adaptive Color Helper + +/// Creates a `Color` that automatically resolves to `light` or `dark` based on +/// the current system / window appearance. +public func adaptiveColor(light: Color, dark: Color) -> Color { + #if os(macOS) + Color(nsColor: NSColor(name: nil, dynamicProvider: { appearance in + let isDark = appearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua + return isDark ? NSColor(dark) : NSColor(light) + })) + #else + Color(uiColor: UIColor { traits in + traits.userInterfaceStyle == .dark ? UIColor(dark) : UIColor(light) + }) + #endif +} + // MARK: - Color Scales public enum Slate { @@ -99,28 +121,37 @@ public enum Amber { public enum VColor { // Backgrounds - public static let background = Slate._950 - public static let backgroundSubtle = Slate._800 - public static let chatBackground = Slate._900 - public static let surface = Slate._800 - public static let surfaceBorder = Slate._700 + public static let background = adaptiveColor(light: .white, dark: Slate._950) + public static let backgroundSubtle = adaptiveColor(light: Slate._100, dark: Slate._800) + public static let chatBackground = adaptiveColor(light: Slate._50, dark: Slate._900) + public static let surface = adaptiveColor(light: .white, dark: Slate._800) + public static let surfaceBorder = adaptiveColor(light: Slate._200, dark: Slate._700) + public static let surfaceSubtle = adaptiveColor(light: Slate._50, dark: Slate._900) // Text - public static let textPrimary = Slate._50 - public static let textSecondary = Slate._400 - public static let textMuted = Slate._500 + public static let textPrimary = adaptiveColor(light: Slate._900, dark: Slate._50) + public static let textSecondary = adaptiveColor(light: Slate._600, dark: Slate._400) + public static let textMuted = adaptiveColor(light: Slate._500, dark: Slate._500) // Accent (violet = primary) - public static let accent = Violet._600 + public static let accent = adaptiveColor(light: Violet._700, dark: Violet._600) public static let accentSubtle = Violet._100 - // Onboarding accent (amber) + // Onboarding accent (amber) — always dark theme public static let onboardingAccent = Amber._500 public static let onboardingAccentDark = Amber._600 public static let onboardingAccentDarker = Amber._800 // Status - public static let success = Emerald._600 - public static let error = Rose._600 - public static let warning = Amber._600 + public static let success = adaptiveColor(light: Emerald._700, dark: Emerald._600) + public static let error = adaptiveColor(light: Rose._700, dark: Rose._600) + public static let warning = adaptiveColor(light: Amber._700, dark: Amber._600) + + // Interactive states + public static let ghostHover = adaptiveColor(light: Slate._100, dark: Slate._700) + public static let ghostPressed = adaptiveColor(light: Slate._200, dark: Slate._600) + public static let divider = adaptiveColor(light: Slate._200, dark: Slate._700) + public static let hoverOverlay = adaptiveColor(light: Color(hex: 0x000000), dark: .white) + public static let toggleOff = adaptiveColor(light: Slate._300, dark: Slate._700) + public static let toggleBorder = adaptiveColor(light: Slate._400, dark: Slate._600) } diff --git a/clients/shared/DesignSystem/Tokens/MeadowTokens.swift b/clients/shared/DesignSystem/Tokens/MeadowTokens.swift index 573ae47c8ca..0f65dbc3667 100644 --- a/clients/shared/DesignSystem/Tokens/MeadowTokens.swift +++ b/clients/shared/DesignSystem/Tokens/MeadowTokens.swift @@ -3,8 +3,14 @@ import SwiftUI /// Onboarding-specific design tokens for the Pixel Meadow theme. public enum Meadow { // Panel - public static let panelBackground = Slate._900.opacity(0.75) - public static let panelBorder = Slate._700.opacity(0.4) + public static let panelBackground = adaptiveColor( + light: Color.white.opacity(0.85), + dark: Slate._900.opacity(0.75) + ) + public static let panelBorder = adaptiveColor( + light: Slate._200.opacity(0.6), + dark: Slate._700.opacity(0.4) + ) // Egg glow public static let eggGlow = Amber._500 @@ -12,7 +18,10 @@ public enum Meadow { public static let crackLight = Amber._200 // Bottom caption - public static let captionText = Color.white.opacity(0.5) + public static let captionText = adaptiveColor( + light: Color.black.opacity(0.4), + dark: Color.white.opacity(0.5) + ) // Pixel scaling factor public static let pixelScale: CGFloat = 2.0