mirror of
https://github.com/h3pdesign/Neon-Vision-Editor
synced 2026-04-21 13:27:16 +00:00
Stabilize translucency surfaces and finalize v0.5.2 release notes
This commit is contained in:
parent
d4914e6d97
commit
6a9758a187
32 changed files with 431 additions and 96 deletions
|
|
@ -18,6 +18,10 @@ The format follows *Keep a Changelog*. Versions use semantic versioning with pre
|
|||
|
||||
### Fixed
|
||||
- Fixed missing diagnostics reset workflow by adding a dedicated `Clear Diagnostics` action that also clears file-open timing snapshots.
|
||||
- Fixed macOS editor-window top-bar jumping when toggling the toolbar translucency control by keeping chrome flags stable.
|
||||
- Fixed CSV/TSV mode header transparency so the mode bar now uses a solid standard window background.
|
||||
- Fixed settings-window translucency consistency on macOS so title/tab and content regions render as one unified surface.
|
||||
- Fixed cross-platform updater diagnostics compilation by adding a non-macOS bundle-version reader fallback.
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
|
|
|
|||
|
|
@ -361,7 +361,7 @@
|
|||
CODE_SIGNING_ALLOWED = YES;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 451;
|
||||
CURRENT_PROJECT_VERSION = 452;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_TEAM = CS727NF72U;
|
||||
ENABLE_APP_SANDBOX = YES;
|
||||
|
|
@ -444,7 +444,7 @@
|
|||
CODE_SIGNING_ALLOWED = YES;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 451;
|
||||
CURRENT_PROJECT_VERSION = 452;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_TEAM = CS727NF72U;
|
||||
ENABLE_APP_SANDBOX = YES;
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import FoundationModels
|
|||
|
||||
#if false
|
||||
// This enum is defined in ContentView.swift; this is here to avoid redefinition errors.
|
||||
|
||||
public enum AIModel {
|
||||
case appleIntelligence
|
||||
case grok
|
||||
|
|
@ -15,6 +16,8 @@ public enum AIModel {
|
|||
}
|
||||
#endif
|
||||
|
||||
/// MARK: - Types
|
||||
|
||||
public protocol AIClient {
|
||||
func streamSuggestions(prompt: String) -> AsyncStream<String>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,10 @@ import SwiftUI
|
|||
import FoundationModels
|
||||
#endif
|
||||
|
||||
|
||||
|
||||
/// MARK: - Types
|
||||
|
||||
struct NeonVisionMacAppCommands: Commands {
|
||||
let activeEditorViewModel: () -> EditorViewModel
|
||||
let hasActiveEditorWindow: () -> Bool
|
||||
|
|
|
|||
|
|
@ -10,6 +10,10 @@ import UIKit
|
|||
#endif
|
||||
|
||||
#if os(macOS)
|
||||
|
||||
|
||||
/// MARK: - Types
|
||||
|
||||
final class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
weak var viewModel: EditorViewModel? {
|
||||
didSet {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,10 @@ import Observation
|
|||
|
||||
@MainActor
|
||||
@Observable
|
||||
|
||||
|
||||
/// MARK: - Types
|
||||
|
||||
final class AIActivityLog {
|
||||
enum Level: String, CaseIterable {
|
||||
case info = "INFO"
|
||||
|
|
|
|||
|
|
@ -13,6 +13,10 @@ import AppKit
|
|||
import UIKit
|
||||
#endif
|
||||
|
||||
|
||||
|
||||
/// MARK: - Types
|
||||
|
||||
enum AppUpdateCheckInterval: String, CaseIterable, Identifiable {
|
||||
case hourly = "hourly"
|
||||
case daily = "daily"
|
||||
|
|
@ -1094,6 +1098,21 @@ final class AppUpdateManager: ObservableObject {
|
|||
}
|
||||
#endif
|
||||
|
||||
#if !os(macOS)
|
||||
private nonisolated static func readBundleShortVersionString(of appBundleURL: URL) -> String? {
|
||||
let infoPlistURL = appBundleURL.appendingPathComponent("Info.plist")
|
||||
guard
|
||||
let data = try? Data(contentsOf: infoPlistURL),
|
||||
let plist = try? PropertyListSerialization.propertyList(from: data, options: [], format: nil) as? [String: Any],
|
||||
let version = plist["CFBundleShortVersionString"] as? String
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
let trimmed = version.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
return trimmed.isEmpty ? nil : trimmed
|
||||
}
|
||||
#endif
|
||||
|
||||
private func openURL(_ url: URL) {
|
||||
#if canImport(AppKit)
|
||||
NSWorkspace.shared.open(url)
|
||||
|
|
|
|||
|
|
@ -3,6 +3,10 @@ import Foundation
|
|||
import FoundationModels
|
||||
|
||||
@Generable(description: "Plain generated text")
|
||||
|
||||
|
||||
/// MARK: - Types
|
||||
|
||||
public struct GeneratedText { public var text: String }
|
||||
|
||||
public enum AppleFM {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,10 @@ import Foundation
|
|||
import OSLog
|
||||
|
||||
@MainActor
|
||||
|
||||
|
||||
/// MARK: - Types
|
||||
|
||||
final class EditorPerformanceMonitor {
|
||||
struct FileOpenEvent: Codable, Identifiable {
|
||||
let id: UUID
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
import Foundation
|
||||
|
||||
|
||||
|
||||
/// MARK: - Types
|
||||
|
||||
public struct LanguageDetector {
|
||||
public static let shared = LanguageDetector()
|
||||
private init() {}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
|
||||
|
||||
/// MARK: - Types
|
||||
|
||||
enum ReleaseRuntimePolicy {
|
||||
static var isUpdaterEnabledForCurrentDistribution: Bool {
|
||||
#if os(macOS)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,10 @@ import Foundation
|
|||
import OSLog
|
||||
|
||||
@MainActor
|
||||
|
||||
|
||||
/// MARK: - Types
|
||||
|
||||
final class RuntimeReliabilityMonitor {
|
||||
static let shared = RuntimeReliabilityMonitor()
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
import SwiftUI
|
||||
import Foundation
|
||||
|
||||
|
||||
|
||||
/// MARK: - Types
|
||||
|
||||
private enum SyntaxRegexCache {
|
||||
static var storage: [String: NSRegularExpression] = [:]
|
||||
static let lock = NSLock()
|
||||
|
|
|
|||
|
|
@ -5,6 +5,10 @@
|
|||
|
||||
import Foundation
|
||||
// Supported AI providers for suggestions. Extend as needed.
|
||||
|
||||
|
||||
/// MARK: - Types
|
||||
|
||||
public enum AIModel: String, CaseIterable, Identifiable {
|
||||
case appleIntelligence
|
||||
case grok
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
import SwiftUI
|
||||
|
||||
|
||||
|
||||
/// MARK: - Types
|
||||
|
||||
struct AIActivityLogView: View {
|
||||
@State private var log = AIActivityLog.shared
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
import SwiftUI
|
||||
|
||||
|
||||
|
||||
/// MARK: - Types
|
||||
|
||||
struct AppUpdaterDialog: View {
|
||||
@EnvironmentObject private var appUpdateManager: AppUpdateManager
|
||||
@Environment(\.accessibilityReduceTransparency) private var reduceTransparency
|
||||
|
|
|
|||
|
|
@ -6,6 +6,10 @@ import AppKit
|
|||
import UIKit
|
||||
#endif
|
||||
|
||||
|
||||
|
||||
/// MARK: - Types
|
||||
|
||||
extension ContentView {
|
||||
private struct ProjectEditorOverrides: Decodable {
|
||||
let indentWidth: Int?
|
||||
|
|
@ -629,9 +633,13 @@ extension ContentView {
|
|||
func applyWindowTranslucency(_ enabled: Bool) {
|
||||
#if os(macOS)
|
||||
for window in NSApp.windows {
|
||||
// Apply only to editor windows registered by ContentView instances.
|
||||
guard WindowViewModelRegistry.shared.viewModel(for: window.windowNumber) != nil else {
|
||||
continue
|
||||
}
|
||||
window.isOpaque = !enabled
|
||||
window.backgroundColor = enabled ? .clear : NSColor.windowBackgroundColor
|
||||
// Keep window chrome layout stable across both modes to avoid frame/titlebar jumps.
|
||||
// Keep chrome flags constant; toggling these causes visible top-bar jumps.
|
||||
window.titlebarAppearsTransparent = true
|
||||
window.toolbarStyle = .unified
|
||||
window.styleMask.insert(.fullSizeContentView)
|
||||
|
|
@ -760,7 +768,16 @@ extension ContentView {
|
|||
return []
|
||||
}
|
||||
|
||||
let sorted = urls.sorted { $0.lastPathComponent.localizedCaseInsensitiveCompare($1.lastPathComponent) == .orderedAscending }
|
||||
let sorted = urls.sorted { lhs, rhs in
|
||||
let lhsValues = try? lhs.resourceValues(forKeys: [.isDirectoryKey])
|
||||
let rhsValues = try? rhs.resourceValues(forKeys: [.isDirectoryKey])
|
||||
let lhsIsDirectory = lhsValues?.isDirectory == true
|
||||
let rhsIsDirectory = rhsValues?.isDirectory == true
|
||||
if lhsIsDirectory != rhsIsDirectory {
|
||||
return lhsIsDirectory && !rhsIsDirectory
|
||||
}
|
||||
return lhs.lastPathComponent.localizedCaseInsensitiveCompare(rhs.lastPathComponent) == .orderedAscending
|
||||
}
|
||||
var nodes: [ProjectTreeNode] = []
|
||||
for url in sorted {
|
||||
if Task.isCancelled { break }
|
||||
|
|
|
|||
|
|
@ -5,6 +5,10 @@ import AppKit
|
|||
import UIKit
|
||||
#endif
|
||||
|
||||
|
||||
|
||||
/// MARK: - Types
|
||||
|
||||
extension ContentView {
|
||||
private var compactActiveProviderName: String {
|
||||
activeProviderName.components(separatedBy: " (").first ?? activeProviderName
|
||||
|
|
|
|||
|
|
@ -347,6 +347,9 @@ struct ContentView: View {
|
|||
@State private var recoverySnapshotIdentifier: String = UUID().uuidString
|
||||
@State private var lastCaretLocation: Int = 0
|
||||
@State private var sessionCaretByFileURL: [String: Int] = [:]
|
||||
#if os(macOS)
|
||||
@State private var isProjectSidebarResizeHandleHovered: Bool = false
|
||||
#endif
|
||||
private let quickSwitcherRecentsDefaultsKey = "QuickSwitcherRecentItemsV1"
|
||||
|
||||
#if USE_FOUNDATION_MODELS && canImport(FoundationModels)
|
||||
|
|
@ -3463,19 +3466,51 @@ struct ContentView: View {
|
|||
}
|
||||
|
||||
return ZStack {
|
||||
Color.clear
|
||||
// Match the same surface as the editor area so the splitter doesn't look like a foreign strip.
|
||||
Rectangle()
|
||||
.fill(Color.secondary.opacity(0.32))
|
||||
.fill(projectSidebarHandleSurfaceStyle)
|
||||
Rectangle()
|
||||
.fill(Color.secondary.opacity(0.22))
|
||||
.frame(width: 1)
|
||||
}
|
||||
.frame(width: 10)
|
||||
.contentShape(Rectangle())
|
||||
.gesture(drag)
|
||||
#if os(macOS)
|
||||
.onHover { hovering in
|
||||
guard hovering != isProjectSidebarResizeHandleHovered else { return }
|
||||
isProjectSidebarResizeHandleHovered = hovering
|
||||
if hovering {
|
||||
NSCursor.resizeLeftRight.push()
|
||||
} else {
|
||||
NSCursor.pop()
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
if isProjectSidebarResizeHandleHovered {
|
||||
isProjectSidebarResizeHandleHovered = false
|
||||
NSCursor.pop()
|
||||
}
|
||||
}
|
||||
#endif
|
||||
.accessibilityElement()
|
||||
.accessibilityLabel("Resize Project Sidebar")
|
||||
.accessibilityHint("Drag left or right to adjust project sidebar width")
|
||||
}
|
||||
|
||||
private var projectSidebarHandleSurfaceStyle: AnyShapeStyle {
|
||||
if enableTranslucentWindow {
|
||||
return editorSurfaceBackgroundStyle
|
||||
}
|
||||
#if os(iOS)
|
||||
return useIOSUnifiedSolidSurfaces
|
||||
? AnyShapeStyle(iOSNonTranslucentSurfaceColor)
|
||||
: AnyShapeStyle(Color.clear)
|
||||
#else
|
||||
return AnyShapeStyle(Color.clear)
|
||||
#endif
|
||||
}
|
||||
|
||||
private var projectStructureSidebarBody: some View {
|
||||
ProjectStructureSidebarView(
|
||||
rootFolderURL: projectRootFolderURL,
|
||||
|
|
@ -3524,6 +3559,15 @@ struct ContentView: View {
|
|||
}
|
||||
.padding(.horizontal, 10)
|
||||
.padding(.vertical, 8)
|
||||
.background(delimitedHeaderBackgroundColor)
|
||||
}
|
||||
|
||||
private var delimitedHeaderBackgroundColor: Color {
|
||||
#if os(macOS)
|
||||
Color(nsColor: .windowBackgroundColor)
|
||||
#else
|
||||
Color(.systemBackground)
|
||||
#endif
|
||||
}
|
||||
|
||||
private var delimitedTableView: some View {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
import SwiftUI
|
||||
|
||||
|
||||
|
||||
/// MARK: - Types
|
||||
|
||||
enum GlassShapeKind {
|
||||
case capsule
|
||||
case circle
|
||||
|
|
|
|||
|
|
@ -2,6 +2,10 @@ import SwiftUI
|
|||
#if canImport(UIKit)
|
||||
import UIKit
|
||||
|
||||
|
||||
|
||||
/// MARK: - Types
|
||||
|
||||
struct IPadKeyboardShortcutBridge: UIViewRepresentable {
|
||||
let onNewTab: () -> Void
|
||||
let onOpenFile: () -> Void
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
#if os(macOS)
|
||||
import AppKit
|
||||
|
||||
|
||||
|
||||
/// MARK: - Types
|
||||
|
||||
private struct RulerObserverToken: @unchecked Sendable {
|
||||
let raw: NSObjectProtocol
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,10 @@ import SwiftUI
|
|||
import WebKit
|
||||
|
||||
#if os(macOS)
|
||||
|
||||
|
||||
/// MARK: - Types
|
||||
|
||||
struct MarkdownPreviewWebView: NSViewRepresentable {
|
||||
let html: String
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,10 @@ import CoreText
|
|||
import UIKit
|
||||
#endif
|
||||
|
||||
|
||||
|
||||
/// MARK: - Types
|
||||
|
||||
struct NeonSettingsView: View {
|
||||
private static var cachedEditorFonts: [String] = []
|
||||
let supportsOpenInTabs: Bool
|
||||
|
|
@ -254,6 +258,7 @@ struct NeonSettingsView: View {
|
|||
var body: some View {
|
||||
settingsTabs
|
||||
#if os(macOS)
|
||||
.background(settingsWindowBackground)
|
||||
.frame(
|
||||
minWidth: macSettingsWindowSize.min.width,
|
||||
idealWidth: macSettingsWindowSize.ideal.width,
|
||||
|
|
@ -264,14 +269,19 @@ struct NeonSettingsView: View {
|
|||
SettingsWindowConfigurator(
|
||||
minSize: macSettingsWindowSize.min,
|
||||
idealSize: macSettingsWindowSize.ideal,
|
||||
translucentEnabled: supportsTranslucency && translucentWindow
|
||||
translucentEnabled: supportsTranslucency && translucentWindow,
|
||||
translucencyModeRaw: macTranslucencyModeRaw
|
||||
)
|
||||
)
|
||||
#endif
|
||||
.preferredColorScheme(preferredColorSchemeOverride)
|
||||
.onAppear {
|
||||
settingsActiveTab = "general"
|
||||
moreSectionTab = "support"
|
||||
if settingsActiveTab.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
settingsActiveTab = "general"
|
||||
}
|
||||
if moreSectionTab.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
moreSectionTab = "support"
|
||||
}
|
||||
selectedTheme = canonicalThemeName(selectedTheme)
|
||||
migrateLegacyPinkSettingsIfNeeded()
|
||||
loadAvailableEditorFontsIfNeeded()
|
||||
|
|
@ -2039,16 +2049,23 @@ struct NeonSettingsView: View {
|
|||
@ViewBuilder
|
||||
private var settingsContainerBackground: some View {
|
||||
#if os(macOS)
|
||||
if supportsTranslucency && translucentWindow {
|
||||
Color.clear.background(.ultraThinMaterial)
|
||||
} else {
|
||||
Color(nsColor: .windowBackgroundColor)
|
||||
}
|
||||
Color.clear
|
||||
#else
|
||||
Color.clear.background(.ultraThinMaterial)
|
||||
#endif
|
||||
}
|
||||
|
||||
#if os(macOS)
|
||||
@ViewBuilder
|
||||
private var settingsWindowBackground: some View {
|
||||
if supportsTranslucency && translucentWindow {
|
||||
Color.clear
|
||||
} else {
|
||||
Color(nsColor: .windowBackgroundColor)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
private func settingsEffectiveMaxWidth(base: CGFloat) -> CGFloat {
|
||||
#if os(iOS)
|
||||
if useTwoColumnSettingsLayout { return max(base, 780) }
|
||||
|
|
@ -2186,24 +2203,8 @@ struct NeonSettingsView: View {
|
|||
}
|
||||
|
||||
private var macSettingsWindowSize: (min: NSSize, ideal: NSSize) {
|
||||
switch settingsActiveTab {
|
||||
case "themes":
|
||||
return (NSSize(width: 740, height: 900), NSSize(width: 840, height: 980))
|
||||
case "editor":
|
||||
return (NSSize(width: 640, height: 820), NSSize(width: 720, height: 900))
|
||||
case "templates":
|
||||
return (NSSize(width: 600, height: 760), NSSize(width: 680, height: 840))
|
||||
case "general":
|
||||
return (NSSize(width: 600, height: 760), NSSize(width: 680, height: 840))
|
||||
case "ai":
|
||||
return (NSSize(width: 640, height: 780), NSSize(width: 720, height: 860))
|
||||
case "updates":
|
||||
return (NSSize(width: 580, height: 720), NSSize(width: 660, height: 780))
|
||||
case "support":
|
||||
return (NSSize(width: 580, height: 720), NSSize(width: 660, height: 780))
|
||||
default:
|
||||
return (NSSize(width: 600, height: 760), NSSize(width: 680, height: 840))
|
||||
}
|
||||
// Keep a stable window envelope across tabs to avoid toolbar-tab jump/overflow relayout.
|
||||
(NSSize(width: 740, height: 900), NSSize(width: 840, height: 980))
|
||||
}
|
||||
#endif
|
||||
|
||||
|
|
@ -2323,13 +2324,13 @@ struct SettingsWindowConfigurator: NSViewRepresentable {
|
|||
let minSize: NSSize
|
||||
let idealSize: NSSize
|
||||
let translucentEnabled: Bool
|
||||
let translucencyModeRaw: String
|
||||
|
||||
final class Coordinator {
|
||||
var didInitialApply = false
|
||||
var pendingApply: DispatchWorkItem?
|
||||
var lastMinSize: NSSize?
|
||||
var lastIdealSize: NSSize?
|
||||
var lastTranslucentEnabled: Bool?
|
||||
var lastTranslucencyModeRaw: String?
|
||||
var didConfigureWindowChrome = false
|
||||
}
|
||||
|
||||
|
|
@ -2350,13 +2351,11 @@ struct SettingsWindowConfigurator: NSViewRepresentable {
|
|||
}
|
||||
|
||||
private func scheduleApply(to window: NSWindow?, coordinator: Coordinator) {
|
||||
if coordinator.didInitialApply,
|
||||
coordinator.lastMinSize == minSize,
|
||||
coordinator.lastIdealSize == idealSize,
|
||||
coordinator.lastTranslucentEnabled == translucentEnabled {
|
||||
coordinator.pendingApply?.cancel()
|
||||
if !coordinator.didInitialApply, let window {
|
||||
apply(to: window, coordinator: coordinator)
|
||||
return
|
||||
}
|
||||
coordinator.pendingApply?.cancel()
|
||||
let work = DispatchWorkItem {
|
||||
apply(to: window, coordinator: coordinator)
|
||||
}
|
||||
|
|
@ -2367,59 +2366,90 @@ struct SettingsWindowConfigurator: NSViewRepresentable {
|
|||
private func apply(to window: NSWindow?, coordinator: Coordinator) {
|
||||
guard let window else { return }
|
||||
let isFirstApply = !coordinator.didInitialApply
|
||||
let translucencyChanged = coordinator.lastTranslucentEnabled != translucentEnabled
|
||||
coordinator.lastMinSize = minSize
|
||||
coordinator.lastIdealSize = idealSize
|
||||
coordinator.lastTranslucentEnabled = translucentEnabled
|
||||
coordinator.lastTranslucencyModeRaw = translucencyModeRaw
|
||||
window.minSize = minSize
|
||||
|
||||
// Always enforce native macOS Settings toolbar chrome; other window updaters may have changed it.
|
||||
window.toolbarStyle = .preference
|
||||
window.titleVisibility = .hidden
|
||||
window.title = ""
|
||||
if isFirstApply {
|
||||
let targetWidth = max(minSize.width, idealSize.width)
|
||||
let targetHeight = max(minSize.height, idealSize.height)
|
||||
if abs(targetWidth - window.frame.size.width) > 1 || abs(targetHeight - window.frame.size.height) > 1 {
|
||||
// Apply initial geometry once; avoid frame churn during tab/content updates.
|
||||
var frame = window.frame
|
||||
let oldHeight = frame.size.height
|
||||
frame.size = NSSize(width: targetWidth, height: targetHeight)
|
||||
frame.origin.y += oldHeight - targetHeight
|
||||
window.setFrame(frame, display: true, animate: false)
|
||||
}
|
||||
centerSettingsWindow(window)
|
||||
}
|
||||
|
||||
if !coordinator.didConfigureWindowChrome {
|
||||
// Match native macOS Settings layout: centered preference tabs and hidden title text.
|
||||
window.toolbarStyle = .preference
|
||||
window.titleVisibility = .hidden
|
||||
window.title = ""
|
||||
// Keep settings chrome stable for the lifetime of this window.
|
||||
window.isOpaque = false
|
||||
window.titlebarAppearsTransparent = true
|
||||
window.styleMask.insert(.fullSizeContentView)
|
||||
if #available(macOS 13.0, *) {
|
||||
window.titlebarSeparatorStyle = .none
|
||||
}
|
||||
coordinator.didConfigureWindowChrome = true
|
||||
}
|
||||
|
||||
let targetWidth: CGFloat
|
||||
let targetHeight: CGFloat
|
||||
if coordinator.didInitialApply {
|
||||
// Respect manual window size changes while enforcing per-tab minimums.
|
||||
targetWidth = max(minSize.width, window.frame.size.width)
|
||||
targetHeight = max(minSize.height, window.frame.size.height)
|
||||
} else {
|
||||
targetWidth = max(minSize.width, idealSize.width)
|
||||
targetHeight = max(minSize.height, idealSize.height)
|
||||
}
|
||||
if abs(targetWidth - window.frame.size.width) > 1 || abs(targetHeight - window.frame.size.height) > 1 {
|
||||
// Keep the top edge visually stable while adapting size per tab.
|
||||
var frame = window.frame
|
||||
let oldHeight = frame.size.height
|
||||
frame.size = NSSize(width: targetWidth, height: targetHeight)
|
||||
frame.origin.y += oldHeight - targetHeight
|
||||
window.setFrame(frame, display: true, animate: false)
|
||||
}
|
||||
|
||||
// Keep settings-window translucency in sync without relying on editor view events.
|
||||
if translucencyChanged || isFirstApply {
|
||||
window.isOpaque = !translucentEnabled
|
||||
window.backgroundColor = translucentEnabled ? .clear : NSColor.windowBackgroundColor
|
||||
window.titlebarAppearsTransparent = translucentEnabled
|
||||
if translucentEnabled {
|
||||
window.styleMask.insert(.fullSizeContentView)
|
||||
} else {
|
||||
window.styleMask.remove(.fullSizeContentView)
|
||||
}
|
||||
if #available(macOS 13.0, *) {
|
||||
window.titlebarSeparatorStyle = translucentEnabled ? .none : .automatic
|
||||
}
|
||||
}
|
||||
// Keep a non-clear background to avoid fully transparent titlebar artifacts.
|
||||
window.backgroundColor = translucencyEnabledColor(enabled: translucentEnabled, window: window)
|
||||
// Some macOS states restore the title from the selected settings tab.
|
||||
// Force an empty, hidden title for native Settings appearance.
|
||||
window.title = ""
|
||||
window.titleVisibility = .hidden
|
||||
window.representedURL = nil
|
||||
coordinator.didInitialApply = true
|
||||
}
|
||||
|
||||
private func translucencyEnabledColor(enabled: Bool, window: NSWindow) -> NSColor {
|
||||
guard enabled else { return NSColor.windowBackgroundColor }
|
||||
let isDark = window.effectiveAppearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua
|
||||
let whiteLevel: CGFloat
|
||||
switch translucencyModeRaw {
|
||||
case "subtle":
|
||||
whiteLevel = isDark ? 0.18 : 0.90
|
||||
case "vibrant":
|
||||
whiteLevel = isDark ? 0.12 : 0.82
|
||||
default:
|
||||
whiteLevel = isDark ? 0.15 : 0.86
|
||||
}
|
||||
// Keep settings tint almost opaque to avoid "more transparent" appearance.
|
||||
return NSColor(calibratedWhite: whiteLevel, alpha: 0.98)
|
||||
}
|
||||
|
||||
private func centerSettingsWindow(_ settingsWindow: NSWindow) {
|
||||
let referenceWindow = preferredReferenceWindow(excluding: settingsWindow)
|
||||
let size = settingsWindow.frame.size
|
||||
let referenceFrame = referenceWindow?.frame ?? settingsWindow.frame
|
||||
var origin = NSPoint(
|
||||
x: round(referenceFrame.midX - size.width / 2),
|
||||
y: round(referenceFrame.midY - size.height / 2)
|
||||
)
|
||||
if let visibleFrame = referenceWindow?.screen?.visibleFrame ?? settingsWindow.screen?.visibleFrame {
|
||||
origin.x = min(max(origin.x, visibleFrame.minX), visibleFrame.maxX - size.width)
|
||||
origin.y = min(max(origin.y, visibleFrame.minY), visibleFrame.maxY - size.height)
|
||||
}
|
||||
settingsWindow.setFrameOrigin(origin)
|
||||
}
|
||||
|
||||
private func preferredReferenceWindow(excluding settingsWindow: NSWindow) -> NSWindow? {
|
||||
if let key = NSApp.keyWindow, key !== settingsWindow, key.isVisible {
|
||||
return key
|
||||
}
|
||||
if let main = NSApp.mainWindow, main !== settingsWindow, main.isVisible {
|
||||
return main
|
||||
}
|
||||
return NSApp.windows.first(where: { window in
|
||||
window !== settingsWindow && window.isVisible && window.level == .normal
|
||||
})
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,10 @@ import UniformTypeIdentifiers
|
|||
import AppKit
|
||||
#endif
|
||||
|
||||
|
||||
|
||||
/// MARK: - Types
|
||||
|
||||
enum NeonUIStyle {
|
||||
static let accentBlue = Color(red: 0.17, green: 0.49, blue: 0.98)
|
||||
static let accentBlueSoft = Color(red: 0.44, green: 0.72, blue: 0.99)
|
||||
|
|
|
|||
|
|
@ -3,6 +3,10 @@ import SwiftUI
|
|||
import UniformTypeIdentifiers
|
||||
import UIKit
|
||||
|
||||
|
||||
|
||||
/// MARK: - Types
|
||||
|
||||
struct ProjectFolderPicker: UIViewControllerRepresentable {
|
||||
let onPick: (URL) -> Void
|
||||
let onCancel: () -> Void
|
||||
|
|
|
|||
|
|
@ -2,6 +2,10 @@ import SwiftUI
|
|||
import Foundation
|
||||
|
||||
#if os(macOS)
|
||||
|
||||
|
||||
/// MARK: - Types
|
||||
|
||||
private enum MacTranslucencyMode: String {
|
||||
case subtle
|
||||
case balanced
|
||||
|
|
@ -355,6 +359,18 @@ struct SidebarView: View {
|
|||
}
|
||||
}
|
||||
struct ProjectStructureSidebarView: View {
|
||||
private enum SidebarDensity: String, CaseIterable, Identifiable {
|
||||
case compact
|
||||
case comfortable
|
||||
|
||||
var id: String { rawValue }
|
||||
}
|
||||
|
||||
private struct FileIconStyle {
|
||||
let symbol: String
|
||||
let color: Color
|
||||
}
|
||||
|
||||
let rootFolderURL: URL?
|
||||
let nodes: [ProjectTreeNode]
|
||||
let selectedFileURL: URL?
|
||||
|
|
@ -370,6 +386,8 @@ struct ProjectStructureSidebarView: View {
|
|||
#if os(macOS)
|
||||
@AppStorage("SettingsMacTranslucencyMode") private var macTranslucencyModeRaw: String = "balanced"
|
||||
#endif
|
||||
@AppStorage("SettingsProjectSidebarDensity") private var sidebarDensityRaw: String = SidebarDensity.compact.rawValue
|
||||
@AppStorage("SettingsProjectSidebarAutoCollapseDeep") private var autoCollapseDeepFolders: Bool = true
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
|
|
@ -405,6 +423,12 @@ struct ProjectStructureSidebarView: View {
|
|||
)
|
||||
}
|
||||
Divider()
|
||||
Picker("Density", selection: $sidebarDensityRaw) {
|
||||
Text("Compact").tag(SidebarDensity.compact.rawValue)
|
||||
Text("Comfortable").tag(SidebarDensity.comfortable.rawValue)
|
||||
}
|
||||
Toggle("Auto-collapse Deep Folders", isOn: $autoCollapseDeepFolders)
|
||||
Divider()
|
||||
Button("Expand All") {
|
||||
expandAllDirectories()
|
||||
}
|
||||
|
|
@ -419,20 +443,20 @@ struct ProjectStructureSidebarView: View {
|
|||
.accessibilityLabel("Expand or collapse all folders")
|
||||
.accessibilityHint("Expands or collapses all folders in the project tree")
|
||||
}
|
||||
.padding(.horizontal, 10)
|
||||
.padding(.top, 10)
|
||||
.padding(.bottom, 8)
|
||||
.padding(.horizontal, headerHorizontalPadding)
|
||||
.padding(.top, headerTopPadding)
|
||||
.padding(.bottom, headerBottomPadding)
|
||||
#if os(macOS)
|
||||
.background(sidebarHeaderFill)
|
||||
#endif
|
||||
|
||||
if let rootFolderURL {
|
||||
Text(rootFolderURL.path)
|
||||
.font(.caption2)
|
||||
.font(.system(size: isCompactDensity ? 11 : 12))
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(2)
|
||||
.lineLimit(isCompactDensity ? 1 : 2)
|
||||
.textSelection(.enabled)
|
||||
.padding(.horizontal, 10)
|
||||
.padding(.horizontal, headerHorizontalPadding)
|
||||
}
|
||||
|
||||
List {
|
||||
|
|
@ -560,18 +584,21 @@ struct ProjectStructureSidebarView: View {
|
|||
}
|
||||
|
||||
private func expandAllDirectories() {
|
||||
expandedDirectories = allDirectoryNodeIDs(in: nodes)
|
||||
expandedDirectories = allDirectoryNodeIDs(in: nodes, level: 0)
|
||||
}
|
||||
|
||||
private func collapseAllDirectories() {
|
||||
expandedDirectories.removeAll()
|
||||
}
|
||||
|
||||
private func allDirectoryNodeIDs(in treeNodes: [ProjectTreeNode]) -> Set<String> {
|
||||
private func allDirectoryNodeIDs(in treeNodes: [ProjectTreeNode], level: Int) -> Set<String> {
|
||||
var result: Set<String> = []
|
||||
for node in treeNodes where node.isDirectory {
|
||||
result.insert(node.id)
|
||||
result.formUnion(allDirectoryNodeIDs(in: node.children))
|
||||
let shouldInclude = !autoCollapseDeepFolders || level < 2
|
||||
if shouldInclude {
|
||||
result.insert(node.id)
|
||||
}
|
||||
result.formUnion(allDirectoryNodeIDs(in: node.children, level: level + 1))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
|
@ -593,21 +620,30 @@ struct ProjectStructureSidebarView: View {
|
|||
projectNodeView(child, level: level + 1)
|
||||
}
|
||||
} label: {
|
||||
Label(node.url.lastPathComponent, systemImage: "folder")
|
||||
.lineLimit(1)
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: "folder")
|
||||
.foregroundStyle(.blue)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
Text(node.url.lastPathComponent)
|
||||
.lineLimit(1)
|
||||
}
|
||||
.padding(.vertical, rowVerticalPadding)
|
||||
}
|
||||
.padding(.leading, CGFloat(level) * 10)
|
||||
.padding(.leading, CGFloat(level) * levelIndent)
|
||||
.listRowInsets(rowInsets)
|
||||
.listRowBackground(Color.clear)
|
||||
.listRowSeparator(.hidden)
|
||||
)
|
||||
} else {
|
||||
let style = fileIconStyle(for: node.url)
|
||||
return AnyView(
|
||||
Button {
|
||||
onOpenProjectFile(node.url)
|
||||
} label: {
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: "doc.text")
|
||||
.foregroundColor(.secondary)
|
||||
Image(systemName: style.symbol)
|
||||
.foregroundStyle(style.color)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
Text(node.url.lastPathComponent)
|
||||
.lineLimit(1)
|
||||
Spacer()
|
||||
|
|
@ -616,14 +652,100 @@ struct ProjectStructureSidebarView: View {
|
|||
.foregroundColor(.accentColor)
|
||||
}
|
||||
}
|
||||
.padding(.vertical, rowVerticalPadding)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.padding(.leading, CGFloat(level) * 10)
|
||||
.padding(.leading, CGFloat(level) * levelIndent)
|
||||
.listRowInsets(rowInsets)
|
||||
.listRowBackground(Color.clear)
|
||||
.listRowSeparator(.hidden)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private var sidebarDensity: SidebarDensity {
|
||||
SidebarDensity(rawValue: sidebarDensityRaw) ?? .compact
|
||||
}
|
||||
|
||||
private var isCompactDensity: Bool { sidebarDensity == .compact }
|
||||
|
||||
private var levelIndent: CGFloat {
|
||||
isCompactDensity ? 8 : 12
|
||||
}
|
||||
|
||||
private var rowVerticalPadding: CGFloat {
|
||||
isCompactDensity ? 1 : 4
|
||||
}
|
||||
|
||||
private var headerHorizontalPadding: CGFloat {
|
||||
isCompactDensity ? 8 : 10
|
||||
}
|
||||
|
||||
private var headerTopPadding: CGFloat {
|
||||
isCompactDensity ? 8 : 10
|
||||
}
|
||||
|
||||
private var headerBottomPadding: CGFloat {
|
||||
isCompactDensity ? 6 : 8
|
||||
}
|
||||
|
||||
private var rowInsets: EdgeInsets {
|
||||
EdgeInsets(top: 0, leading: isCompactDensity ? 4 : 6, bottom: 0, trailing: 4)
|
||||
}
|
||||
|
||||
private func fileIconStyle(for url: URL) -> FileIconStyle {
|
||||
let ext = url.pathExtension.lowercased()
|
||||
let name = url.lastPathComponent.lowercased()
|
||||
|
||||
switch ext {
|
||||
case "swift":
|
||||
return .init(symbol: "swift", color: .orange)
|
||||
case "js", "mjs", "cjs":
|
||||
return .init(symbol: "curlybraces.square", color: .yellow)
|
||||
case "ts", "tsx":
|
||||
return .init(symbol: "chevron.left.forwardslash.chevron.right", color: .blue)
|
||||
case "json", "jsonc", "json5":
|
||||
return .init(symbol: "curlybraces", color: .green)
|
||||
case "md", "markdown":
|
||||
return .init(symbol: "text.alignleft", color: .teal)
|
||||
case "yml", "yaml", "toml", "ini", "env":
|
||||
return .init(symbol: "slider.horizontal.3", color: .mint)
|
||||
case "html", "htm":
|
||||
return .init(symbol: "chevron.left.slash.chevron.right", color: .orange)
|
||||
case "css":
|
||||
return .init(symbol: "paintbrush.pointed", color: .cyan)
|
||||
case "xml", "svg":
|
||||
return .init(symbol: "diamond", color: .pink)
|
||||
case "sh", "bash", "zsh", "ps1":
|
||||
return .init(symbol: "terminal", color: .indigo)
|
||||
case "py":
|
||||
return .init(symbol: "chevron.left.forwardslash.chevron.right", color: .yellow)
|
||||
case "rb":
|
||||
return .init(symbol: "diamond.fill", color: .red)
|
||||
case "go":
|
||||
return .init(symbol: "g.circle", color: .cyan)
|
||||
case "rs":
|
||||
return .init(symbol: "gearshape.2", color: .orange)
|
||||
case "sql":
|
||||
return .init(symbol: "cylinder", color: .purple)
|
||||
case "csv", "tsv":
|
||||
return .init(symbol: "tablecells", color: .green)
|
||||
case "txt", "log":
|
||||
return .init(symbol: "doc.plaintext", color: .secondary)
|
||||
case "png", "jpg", "jpeg", "gif", "webp", "heic":
|
||||
return .init(symbol: "photo", color: .purple)
|
||||
case "pdf":
|
||||
return .init(symbol: "doc.richtext", color: .red)
|
||||
default:
|
||||
if name.hasPrefix(".git") {
|
||||
return .init(symbol: "arrow.triangle.branch", color: .orange)
|
||||
}
|
||||
if name.hasPrefix(".env") {
|
||||
return .init(symbol: "lock.doc", color: .mint)
|
||||
}
|
||||
return .init(symbol: "doc.text", color: .secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ProjectTreeNode: Identifiable {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
import XCTest
|
||||
@testable import Neon_Vision_Editor
|
||||
|
||||
|
||||
|
||||
/// MARK: - Tests
|
||||
|
||||
final class AppUpdateManagerTests: XCTestCase {
|
||||
func testHostAllowlistBehavior() {
|
||||
XCTAssertTrue(AppUpdateManager.isTrustedGitHubHost("github.com"))
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
import XCTest
|
||||
|
||||
|
||||
|
||||
/// MARK: - Tests
|
||||
|
||||
final class LanguageDetectorTests: XCTestCase {
|
||||
func testPreferredLanguageForExtensions() {
|
||||
let cases: [(String, String)] = [
|
||||
|
|
|
|||
|
|
@ -2,6 +2,10 @@ import XCTest
|
|||
import SwiftUI
|
||||
@testable import Neon_Vision_Editor
|
||||
|
||||
|
||||
|
||||
/// MARK: - Tests
|
||||
|
||||
final class MarkdownSyntaxHighlightingTests: XCTestCase {
|
||||
private func markdownPatterns() -> [String: Color] {
|
||||
getSyntaxPatterns(
|
||||
|
|
|
|||
|
|
@ -2,6 +2,10 @@ import XCTest
|
|||
import SwiftUI
|
||||
@testable import Neon_Vision_Editor
|
||||
|
||||
|
||||
|
||||
/// MARK: - Tests
|
||||
|
||||
final class ReleaseRuntimePolicyTests: XCTestCase {
|
||||
func testSettingsTabFallsBackToGeneral() {
|
||||
XCTAssertEqual(ReleaseRuntimePolicy.settingsTab(from: nil), "general")
|
||||
|
|
|
|||
|
|
@ -5,6 +5,10 @@ import XCTest
|
|||
import AppKit
|
||||
|
||||
@MainActor
|
||||
|
||||
|
||||
/// MARK: - Tests
|
||||
|
||||
final class WindowTranslucencyTests: XCTestCase {
|
||||
// Verifies that the translucency toggle updates AppKit window flags used by the toolbar/titlebar.
|
||||
func testApplyWindowTranslucencyUpdatesMacWindowFlags() {
|
||||
|
|
|
|||
Loading…
Reference in a new issue