Prepare v0.5.7 bugfix release

This commit is contained in:
h3p 2026-03-26 19:19:45 +01:00
parent a8a0455ceb
commit f92bcc0e42
18 changed files with 967 additions and 663 deletions

View file

@ -4,6 +4,36 @@ All notable changes to **Neon Vision Editor** are documented in this file.
The format follows *Keep a Changelog*. Versions use semantic versioning with prerelease tags.
## [v0.5.7] - 2026-03-26
### Why Upgrade
- Markdown preview on iPhone now uses a cleaner stacked layout and presents PDF export from the active preview flow.
- Markdown preview controls on macOS and iPad now use a more centered, balanced layout with direct export/share/copy actions.
- Appearance handling is more consistent when macOS follows the system light/dark setting, including Settings and editor window surfaces.
- iPad editor surfaces now avoid stray white seams and mismatched panel backgrounds around sidebars, split panes, and markdown preview.
- App Store support-purchase messaging is safer for review and restricted environments where in-app purchases are unavailable.
- Project indexing and iPad Vim-mode wiring are more complete for the `Quick Open`, `Find in Files`, and keyboard-first editing flows introduced around the `0.5.6` line.
### Highlights
- Completed the project-file index snapshot flow so project refreshes can reuse unchanged entries while continuing to feed `Quick Open` and `Find in Files`.
- Completed iPad Vim-mode integration with a dedicated Settings toggle, shared persistence, and visible mode-state reporting on iPad.
- Expanded the Code Snapshot composer with a `Custom` layout mode, better cross-platform sizing behavior, and cleaner control grouping.
### Fixes
- Fixed iPhone Markdown preview layout so title, controls, and export action read cleanly in a centered vertical flow.
- Fixed iPhone Markdown PDF export so the file exporter is presented from the active preview sheet instead of silently failing behind it.
- Fixed macOS and iPad Markdown preview control layout so template, PDF mode, and actions sit in a centered, platform-appropriate grouping.
- Fixed Code Snapshot spacing, preview sizing defaults, iPhone overflow, and iPad/macOS width behavior across `Fit`, `Wrap`, `Readable`, and `Custom`.
- Fixed macOS appearance switching so editor, sidebar, header, and Settings surfaces stay synchronized when the app follows the system mode.
- Fixed iPad editor chrome so split-pane dividers, project sidebar containers, and related surfaces no longer flash unintended white backgrounds.
- Fixed support-purchase messaging so unavailable StoreKit environments no longer blame App Store login or Screen Time during App Review-style sessions.
### Breaking changes
- None.
### Migration
- None.
## [v0.5.6] - 2026-03-17
### Hero Screenshot

View file

@ -361,7 +361,7 @@
CODE_SIGNING_ALLOWED = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 558;
CURRENT_PROJECT_VERSION = 559;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = CS727NF72U;
ENABLE_APP_SANDBOX = YES;
@ -407,7 +407,7 @@
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 0.5.6;
MARKETING_VERSION = 0.5.7;
PRODUCT_BUNDLE_IDENTIFIER = "h3p.Neon-Vision-Editor";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -444,7 +444,7 @@
CODE_SIGNING_ALLOWED = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 558;
CURRENT_PROJECT_VERSION = 559;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = CS727NF72U;
ENABLE_APP_SANDBOX = YES;
@ -490,7 +490,7 @@
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 0.5.6;
MARKETING_VERSION = 0.5.7;
PRODUCT_BUNDLE_IDENTIFIER = "h3p.Neon-Vision-Editor";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";

View file

@ -349,12 +349,12 @@ struct NeonVisionEditorApp: App {
.handlesExternalEvents(matching: [])
Settings {
NeonSettingsView(
ConfiguredSettingsView(
supportsOpenInTabs: false,
supportsTranslucency: true
supportsTranslucency: true,
supportPurchaseManager: supportPurchaseManager,
appUpdateManager: appUpdateManager
)
.environmentObject(supportPurchaseManager)
.environmentObject(appUpdateManager)
.onAppear { applyGlobalAppearanceOverride() }
.onAppear { applyMacWindowTabbingPolicy() }
.onChange(of: appearance) { _, _ in applyGlobalAppearanceOverride() }

View file

@ -1,56 +1,153 @@
import Foundation
struct ProjectFileIndex {
static func buildFileURLs(
struct Entry: Sendable, Hashable {
let url: URL
let standardizedPath: String
let relativePath: String
let displayName: String
let contentModificationDate: Date?
let fileSize: Int64?
}
struct Snapshot: Sendable {
let entries: [Entry]
nonisolated static let empty = Snapshot(entries: [])
var fileURLs: [URL] {
entries.map(\.url)
}
}
nonisolated static func buildSnapshot(
at root: URL,
supportedOnly: Bool,
isSupportedFile: @escaping @Sendable (URL) -> Bool
) async -> [URL] {
) async -> Snapshot {
await Task.detached(priority: .utility) {
let resourceKeys: [URLResourceKey] = [
.isRegularFileKey,
.isDirectoryKey,
.isHiddenKey,
.nameKey
]
let options: FileManager.DirectoryEnumerationOptions = [
.skipsHiddenFiles,
.skipsPackageDescendants
]
guard let enumerator = FileManager.default.enumerator(
buildSnapshotSync(
at: root,
includingPropertiesForKeys: resourceKeys,
options: options
) else {
return []
}
var results: [URL] = []
results.reserveCapacity(512)
while let fileURL = enumerator.nextObject() as? URL {
if Task.isCancelled {
return []
}
guard let values = try? fileURL.resourceValues(forKeys: Set(resourceKeys)) else {
continue
}
if values.isHidden == true {
if values.isDirectory == true {
enumerator.skipDescendants()
}
continue
}
guard values.isRegularFile == true else { continue }
if supportedOnly && !isSupportedFile(fileURL) {
continue
}
results.append(fileURL)
}
return results.sorted {
$0.path.localizedCaseInsensitiveCompare($1.path) == .orderedAscending
}
supportedOnly: supportedOnly,
isSupportedFile: isSupportedFile
)
}.value
}
nonisolated static func refreshSnapshot(
_ previous: Snapshot,
at root: URL,
supportedOnly: Bool,
isSupportedFile: @escaping @Sendable (URL) -> Bool
) async -> Snapshot {
await Task.detached(priority: .utility) {
refreshSnapshotSync(
previous,
at: root,
supportedOnly: supportedOnly,
isSupportedFile: isSupportedFile
)
}.value
}
private nonisolated static func buildSnapshotSync(
at root: URL,
supportedOnly: Bool,
isSupportedFile: @escaping @Sendable (URL) -> Bool
) -> Snapshot {
let previous = Snapshot.empty
return refreshSnapshotSync(
previous,
at: root,
supportedOnly: supportedOnly,
isSupportedFile: isSupportedFile
)
}
private nonisolated static func refreshSnapshotSync(
_ previous: Snapshot,
at root: URL,
supportedOnly: Bool,
isSupportedFile: @escaping @Sendable (URL) -> Bool
) -> Snapshot {
let resourceKeys: Set<URLResourceKey> = [
.isRegularFileKey,
.isDirectoryKey,
.isHiddenKey,
.nameKey,
.contentModificationDateKey,
.fileSizeKey
]
let options: FileManager.DirectoryEnumerationOptions = [
.skipsHiddenFiles,
.skipsPackageDescendants
]
guard let enumerator = FileManager.default.enumerator(
at: root,
includingPropertiesForKeys: Array(resourceKeys),
options: options
) else {
return .empty
}
let previousByPath = Dictionary(uniqueKeysWithValues: previous.entries.map { ($0.standardizedPath, $0) })
var refreshedEntries: [Entry] = []
refreshedEntries.reserveCapacity(max(previous.entries.count, 512))
while let fileURL = enumerator.nextObject() as? URL {
if Task.isCancelled {
return previous
}
guard let values = try? fileURL.resourceValues(forKeys: resourceKeys) else {
continue
}
if values.isHidden == true {
if values.isDirectory == true {
enumerator.skipDescendants()
}
continue
}
guard values.isRegularFile == true else { continue }
if supportedOnly && !isSupportedFile(fileURL) {
continue
}
let standardizedURL = fileURL.standardizedFileURL
let standardizedPath = standardizedURL.path
let modificationDate = values.contentModificationDate
let fileSize = values.fileSize.map(Int64.init)
if let previousEntry = previousByPath[standardizedPath],
previousEntry.contentModificationDate == modificationDate,
previousEntry.fileSize == fileSize {
refreshedEntries.append(previousEntry)
continue
}
let relativePath = relativePathForFile(standardizedURL, root: root)
refreshedEntries.append(
Entry(
url: standardizedURL,
standardizedPath: standardizedPath,
relativePath: relativePath,
displayName: values.name ?? standardizedURL.lastPathComponent,
contentModificationDate: modificationDate,
fileSize: fileSize
)
)
}
refreshedEntries.sort {
$0.standardizedPath.localizedCaseInsensitiveCompare($1.standardizedPath) == .orderedAscending
}
return Snapshot(entries: refreshedEntries)
}
private nonisolated static func relativePathForFile(_ fileURL: URL, root: URL) -> String {
let rootPath = root.standardizedFileURL.path
let filePath = fileURL.standardizedFileURL.path
guard filePath.hasPrefix(rootPath) else { return fileURL.lastPathComponent }
let trimmed = String(filePath.dropFirst(rootPath.count)).trimmingCharacters(in: CharacterSet(charactersIn: "/"))
return trimmed.isEmpty ? fileURL.lastPathComponent : trimmed
}
}

View file

@ -96,6 +96,7 @@ enum CodeSnapshotLayoutMode: String, CaseIterable, Identifiable {
case fit
case readable
case wrap
case custom
var id: String { rawValue }
@ -104,6 +105,7 @@ enum CodeSnapshotLayoutMode: String, CaseIterable, Identifiable {
case .fit: return "Fit"
case .readable: return "Readable"
case .wrap: return "Wrap"
case .custom: return "Custom"
}
}
}
@ -112,9 +114,11 @@ struct CodeSnapshotStyle: Equatable {
var appearance: CodeSnapshotAppearance = .dark
var backgroundPreset: CodeSnapshotBackgroundPreset = .sunrise
var frameStyle: CodeSnapshotFrameStyle = .macWindow
var layoutMode: CodeSnapshotLayoutMode = .fit
var layoutMode: CodeSnapshotLayoutMode = .readable
var showLineNumbers: Bool = true
var padding: CGFloat = 26
var padding: CGFloat = 5
var customCardWidth: CGFloat = 1100
var customCardHeight: CGFloat = 880
}
struct PNGSnapshotDocument: FileDocument {
@ -203,8 +207,14 @@ private enum CodeSnapshotRenderer {
style: CodeSnapshotStyle
) -> Data? {
let renderWidth = snapshotRenderWidth(payload: payload, style: style)
let card = CodeSnapshotCardView(payload: payload, style: style, cardWidth: renderWidth)
.frame(width: renderWidth)
let renderHeight = snapshotRenderHeight(style: style)
let card = CodeSnapshotCardView(
payload: payload,
style: style,
cardWidth: renderWidth,
cardHeight: renderHeight
)
.frame(width: renderWidth, height: renderHeight)
let renderer = ImageRenderer(content: card)
renderer.scale = 2
#if os(macOS)
@ -234,6 +244,9 @@ private enum CodeSnapshotRenderer {
}
private static func snapshotRenderWidth(payload: CodeSnapshotPayload, style: CodeSnapshotStyle) -> CGFloat {
if style.layoutMode == .custom {
return style.customCardWidth
}
if style.layoutMode != .readable {
return 940
}
@ -245,6 +258,11 @@ private enum CodeSnapshotRenderer {
let estimated = CGFloat(longestLine) * 9.0 + baseInsets
return min(max(940, estimated), 2200)
}
private static func snapshotRenderHeight(style: CodeSnapshotStyle) -> CGFloat? {
guard style.layoutMode == .custom else { return nil }
return style.customCardHeight
}
}
#if os(macOS)
@ -257,6 +275,20 @@ private struct CodeSnapshotCardView: View {
let payload: CodeSnapshotPayload
let style: CodeSnapshotStyle
let cardWidth: CGFloat
let cardHeight: CGFloat?
private var outerCanvasHorizontalInset: CGFloat {
style.layoutMode == .readable ? 50 : 42
}
private var outerCanvasVerticalInset: CGFloat {
switch style.layoutMode {
case .fit, .readable, .wrap:
return outerCanvasHorizontalInset
case .custom:
return outerCanvasHorizontalInset
}
}
private var lines: [AttributedString] {
CodeSnapshotRenderer.attributedLines(
@ -311,8 +343,9 @@ private struct CodeSnapshotCardView: View {
return max(5.0, min(15.0, fitted))
}
@ViewBuilder
var body: some View {
ZStack {
let card = ZStack {
style.backgroundPreset.gradient
VStack(alignment: .leading, spacing: 0) {
if style.frameStyle == .macWindow {
@ -343,9 +376,12 @@ private struct CodeSnapshotCardView: View {
Text(line)
.font(.system(size: 15, weight: .regular, design: .monospaced))
.font(.system(size: codeFontSize, weight: .regular, design: .monospaced))
.lineLimit(style.layoutMode == .wrap ? nil : 1)
.lineLimit(style.layoutMode == .fit || style.layoutMode == .readable ? 1 : nil)
.minimumScaleFactor(style.layoutMode == .fit ? 0.2 : 1.0)
.fixedSize(horizontal: style.layoutMode == .readable, vertical: style.layoutMode == .wrap)
.fixedSize(
horizontal: style.layoutMode == .readable,
vertical: style.layoutMode == .wrap || style.layoutMode == .custom
)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
@ -359,9 +395,16 @@ private struct CodeSnapshotCardView: View {
.stroke(surfaceBorder, lineWidth: 1)
)
.shadow(color: style.frameStyle == .glow ? Color.white.opacity(0.24) : Color.black.opacity(0.18), radius: style.frameStyle == .glow ? 26 : 16, y: 10)
.padding(42)
.padding(.horizontal, outerCanvasHorizontalInset)
.padding(.vertical, outerCanvasVerticalInset)
}
if style.layoutMode == .custom, let cardHeight {
card
.frame(width: cardWidth, height: cardHeight)
} else {
card
}
.aspectRatio(1.25, contentMode: .fit)
}
}
@ -384,26 +427,69 @@ struct CodeSnapshotComposerView: View {
let estimatedControlsHeight: CGFloat = usesCompactScrollingLayout ? 210 : 152
let availablePreviewHeight = max(220, proxy.size.height - estimatedControlsHeight - 44)
let fittedPreviewWidth = min(980, availableWidth, availablePreviewHeight * 1.25)
let compactFitWrapPreviewWidth = fittedPreviewWidth
let regularFitWrapBaseWidth = min(1320, availableWidth, availablePreviewHeight * 1.25)
let regularFitWrapWidth: CGFloat = {
#if os(iOS)
if usesRegularIPadLayout {
let widenedTarget = regularFitWrapBaseWidth + 400
return min(1720, availableWidth, widenedTarget)
}
#endif
return regularFitWrapBaseWidth
}()
let regularWrapWidth: CGFloat = {
#if os(iOS)
if usesRegularIPadLayout {
return min(1840, availableWidth, regularFitWrapWidth + 180)
}
#endif
return regularFitWrapWidth
}()
let previewCardWidth = max(fittedPreviewWidth, min(2200, estimatedCardWidth))
VStack(spacing: 16) {
snapshotControls
if style.layoutMode == .fit {
CodeSnapshotCardView(payload: payload, style: style, cardWidth: fittedPreviewWidth)
.frame(width: fittedPreviewWidth)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
.padding(.bottom, 92)
let fitPreviewWidth = usesCompactScrollingLayout ? compactFitWrapPreviewWidth : regularFitWrapWidth
Group {
if usesCompactScrollingLayout {
ScrollView([.vertical, .horizontal]) {
CodeSnapshotCardView(payload: payload, style: style, cardWidth: fitPreviewWidth, cardHeight: nil)
.frame(width: fitPreviewWidth)
.frame(maxWidth: .infinity, alignment: .topLeading)
.padding(.bottom, 92)
}
} else {
CodeSnapshotCardView(payload: payload, style: style, cardWidth: fitPreviewWidth, cardHeight: nil)
.frame(width: fitPreviewWidth)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
.padding(.bottom, 92)
}
}
} else if style.layoutMode == .wrap {
ScrollView(.vertical) {
CodeSnapshotCardView(payload: payload, style: style, cardWidth: fittedPreviewWidth)
.frame(width: fittedPreviewWidth)
.frame(maxWidth: .infinity, alignment: .topLeading)
let wrapPreviewWidth = usesCompactScrollingLayout ? compactFitWrapPreviewWidth : regularWrapWidth
CodeSnapshotCardView(payload: payload, style: style, cardWidth: wrapPreviewWidth, cardHeight: nil)
.frame(width: wrapPreviewWidth)
.frame(maxWidth: .infinity, alignment: usesCompactScrollingLayout ? .topLeading : .top)
.padding(.bottom, 92)
}
} else if style.layoutMode == .custom {
ScrollView([.vertical, .horizontal]) {
CodeSnapshotCardView(
payload: payload,
style: style,
cardWidth: style.customCardWidth,
cardHeight: style.customCardHeight
)
.frame(width: style.customCardWidth, height: style.customCardHeight)
.frame(maxWidth: .infinity, alignment: .topLeading)
.padding(.bottom, 92)
}
} else {
ScrollView([.vertical, .horizontal]) {
CodeSnapshotCardView(payload: payload, style: style, cardWidth: previewCardWidth)
.frame(width: usesCompactScrollingLayout ? min(980, availableWidth) : previewCardWidth)
CodeSnapshotCardView(payload: payload, style: style, cardWidth: previewCardWidth, cardHeight: nil)
.frame(width: previewCardWidth)
.frame(maxWidth: .infinity, alignment: .topLeading)
.padding(.bottom, 92)
}
@ -462,7 +548,18 @@ struct CodeSnapshotComposerView: View {
#endif
}
private var usesRegularIPadLayout: Bool {
#if os(iOS)
return horizontalSizeClass == .regular
#else
return false
#endif
}
private var estimatedCardWidth: CGFloat {
if style.layoutMode == .custom {
return style.customCardWidth
}
let baseInsets = (style.padding * 2) + (style.showLineNumbers ? 70 : 26) + 84
let estimated = CGFloat(max(1, payload.text.components(separatedBy: "\n").map(\.count).max() ?? 0)) * 9.0 + baseInsets
return min(max(940, estimated), 2200)
@ -470,123 +567,110 @@ struct CodeSnapshotComposerView: View {
private var snapshotControls: some View {
VStack(alignment: .leading, spacing: 14) {
#if os(iOS)
if horizontalSizeClass == .compact {
VStack(spacing: 12) {
HStack(spacing: 10) {
Picker("Appearance", selection: $style.appearance) {
ForEach(CodeSnapshotAppearance.allCases) { appearance in
Text(appearance.title).tag(appearance)
}
}
.pickerStyle(.menu)
.labelsHidden()
.frame(maxWidth: .infinity, alignment: .leading)
Picker("Background", selection: $style.backgroundPreset) {
ForEach(CodeSnapshotBackgroundPreset.allCases) { preset in
Text(preset.title).tag(preset)
}
}
.pickerStyle(.menu)
.labelsHidden()
.frame(maxWidth: .infinity, alignment: .leading)
}
HStack(spacing: 10) {
Picker("Frame", selection: $style.frameStyle) {
ForEach(CodeSnapshotFrameStyle.allCases) { frame in
Text(frame.title).tag(frame)
}
}
.pickerStyle(.menu)
.labelsHidden()
.frame(maxWidth: .infinity, alignment: .leading)
Picker("Layout", selection: $style.layoutMode) {
ForEach(CodeSnapshotLayoutMode.allCases) { mode in
Text(mode.title).tag(mode)
}
}
.pickerStyle(.menu)
.labelsHidden()
.frame(maxWidth: .infinity, alignment: .leading)
}
HStack(spacing: 10) {
Toggle("Line Numbers", isOn: $style.showLineNumbers)
.toggleStyle(.switch)
.lineLimit(1)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
} else {
HStack(spacing: 16) {
VStack(spacing: 10) {
snapshotMenuRow("Appearance") {
Picker("Appearance", selection: $style.appearance) {
ForEach(CodeSnapshotAppearance.allCases) { appearance in
Text(appearance.title).tag(appearance)
}
}
}
snapshotMenuRow("Background") {
Picker("Background", selection: $style.backgroundPreset) {
ForEach(CodeSnapshotBackgroundPreset.allCases) { preset in
Text(preset.title).tag(preset)
}
}
}
snapshotMenuRow("Frame") {
Picker("Frame", selection: $style.frameStyle) {
ForEach(CodeSnapshotFrameStyle.allCases) { frame in
Text(frame.title).tag(frame)
}
}
}
snapshotMenuRow("Layout") {
Picker("Layout", selection: $style.layoutMode) {
ForEach(CodeSnapshotLayoutMode.allCases) { mode in
Text(mode.title).tag(mode)
}
}
Toggle("Line Numbers", isOn: $style.showLineNumbers)
.lineLimit(1)
}
snapshotToggleRow("Line Numbers", isOn: $style.showLineNumbers)
}
#else
HStack(spacing: 16) {
Picker("Appearance", selection: $style.appearance) {
ForEach(CodeSnapshotAppearance.allCases) { appearance in
Text(appearance.title).tag(appearance)
}
}
Picker("Background", selection: $style.backgroundPreset) {
ForEach(CodeSnapshotBackgroundPreset.allCases) { preset in
Text(preset.title).tag(preset)
}
}
Picker("Frame", selection: $style.frameStyle) {
ForEach(CodeSnapshotFrameStyle.allCases) { frame in
Text(frame.title).tag(frame)
}
}
Picker("Layout", selection: $style.layoutMode) {
ForEach(CodeSnapshotLayoutMode.allCases) { mode in
Text(mode.title).tag(mode)
}
}
Toggle("Line Numbers", isOn: $style.showLineNumbers)
}
#endif
HStack(spacing: 12) {
Text("Padding")
.font(.system(size: 12, weight: .semibold))
.foregroundStyle(.secondary)
Slider(value: $style.padding, in: 18...40, step: 2)
Slider(value: $style.padding, in: 5...40, step: 1)
Text("\(Int(style.padding))")
.font(.system(size: 12, weight: .medium, design: .monospaced))
.foregroundStyle(.secondary)
.frame(width: 36, alignment: .trailing)
}
if style.layoutMode == .custom {
HStack(spacing: 12) {
Text("Width")
.font(.system(size: 12, weight: .semibold))
.foregroundStyle(.secondary)
Slider(value: $style.customCardWidth, in: 700...2200, step: 20)
Text("\(Int(style.customCardWidth))")
.font(.system(size: 12, weight: .medium, design: .monospaced))
.foregroundStyle(.secondary)
.frame(width: 48, alignment: .trailing)
}
HStack(spacing: 12) {
Text("Height")
.font(.system(size: 12, weight: .semibold))
.foregroundStyle(.secondary)
Slider(value: $style.customCardHeight, in: 560...1800, step: 20)
Text("\(Int(style.customCardHeight))")
.font(.system(size: 12, weight: .medium, design: .monospaced))
.foregroundStyle(.secondary)
.frame(width: 48, alignment: .trailing)
}
}
}
.padding(16)
.background(.thinMaterial, in: RoundedRectangle(cornerRadius: 18, style: .continuous))
}
@ViewBuilder
private func snapshotMenuRow<Content: View>(_ title: String, @ViewBuilder content: () -> Content) -> some View {
HStack(alignment: .center, spacing: 14) {
Text(title)
.font(.system(size: 13, weight: .semibold))
.foregroundStyle(.secondary)
.frame(maxWidth: .infinity, alignment: .leading)
content()
.pickerStyle(.menu)
.labelsHidden()
.controlSize(.regular)
.frame(maxWidth: .infinity, alignment: .trailing)
}
}
@ViewBuilder
private func snapshotToggleRow(_ title: String, isOn: Binding<Bool>) -> some View {
HStack(alignment: .center, spacing: 14) {
Text(title)
.font(.system(size: 13, weight: .semibold))
.foregroundStyle(.secondary)
.frame(maxWidth: .infinity, alignment: .leading)
Toggle(title, isOn: isOn)
.labelsHidden()
}
}
@MainActor
private func refreshRenderedSnapshot() async {
let data = CodeSnapshotRenderer.pngData(payload: payload, style: style)

View file

@ -0,0 +1,18 @@
import SwiftUI
struct ConfiguredSettingsView: View {
let supportsOpenInTabs: Bool
let supportsTranslucency: Bool
@ObservedObject var supportPurchaseManager: SupportPurchaseManager
@ObservedObject var appUpdateManager: AppUpdateManager
var body: some View {
NeonSettingsView(
supportsOpenInTabs: supportsOpenInTabs,
supportsTranslucency: supportsTranslucency
)
.environmentObject(supportPurchaseManager)
.environmentObject(appUpdateManager)
}
}

View file

@ -268,6 +268,7 @@ extension ContentView {
showProjectStructureSidebar = false
showCompactProjectSidebarSheet = false
} else if UIDevice.current.userInterfaceIdiom == .phone && nextValue {
markdownPreviewSheetDetent = .large
dismissKeyboard()
}
#endif
@ -636,13 +637,17 @@ extension ContentView {
func applyWindowTranslucency(_ enabled: Bool) {
#if os(macOS)
let isDarkMode = colorScheme == .dark
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 = editorTranslucentBackgroundColor(enabled: enabled, window: window)
window.backgroundColor = editorTranslucentBackgroundColor(
enabled: enabled,
isDarkMode: isDarkMode
)
// Keep chrome flags constant; toggling these causes visible top-bar jumps.
window.titlebarAppearsTransparent = true
window.toolbarStyle = .unified
@ -655,21 +660,20 @@ extension ContentView {
}
#if os(macOS)
private func editorTranslucentBackgroundColor(enabled: Bool, window: NSWindow) -> NSColor {
private func editorTranslucentBackgroundColor(enabled: Bool, isDarkMode: Bool) -> NSColor {
guard enabled else { return NSColor.windowBackgroundColor }
let isDark = window.effectiveAppearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua
let modeRaw = UserDefaults.standard.string(forKey: "SettingsMacTranslucencyMode") ?? "balanced"
let whiteLevel: CGFloat
let alpha: CGFloat
switch modeRaw {
case "subtle":
whiteLevel = isDark ? 0.18 : 0.90
whiteLevel = isDarkMode ? 0.18 : 0.90
alpha = 0.86
case "vibrant":
whiteLevel = isDark ? 0.12 : 0.82
whiteLevel = isDarkMode ? 0.12 : 0.82
alpha = 0.72
default:
whiteLevel = isDark ? 0.15 : 0.86
whiteLevel = isDarkMode ? 0.15 : 0.86
alpha = 0.79
}
return NSColor(calibratedWhite: whiteLevel, alpha: alpha)
@ -721,7 +725,7 @@ extension ContentView {
projectFileIndexTask?.cancel()
projectFileIndexTask = nil
projectFileIndexRefreshGeneration &+= 1
indexedProjectFileURLs = []
projectFileIndexSnapshot = .empty
isProjectFileIndexing = false
return
}
@ -730,10 +734,12 @@ extension ContentView {
projectFileIndexRefreshGeneration &+= 1
let generation = projectFileIndexRefreshGeneration
let supportedOnly = showSupportedProjectFilesOnly
let previousSnapshot = projectFileIndexSnapshot
isProjectFileIndexing = true
projectFileIndexTask = Task(priority: .utility) {
let urls = await ProjectFileIndex.buildFileURLs(
let snapshot = await ProjectFileIndex.refreshSnapshot(
previousSnapshot,
at: root,
supportedOnly: supportedOnly,
isSupportedFile: { url in
@ -744,7 +750,7 @@ extension ContentView {
await MainActor.run {
guard generation == projectFileIndexRefreshGeneration else { return }
guard projectRootFolderURL?.standardizedFileURL == root.standardizedFileURL else { return }
indexedProjectFileURLs = urls
projectFileIndexSnapshot = snapshot
isProjectFileIndexing = false
projectFileIndexTask = nil
}
@ -835,7 +841,7 @@ extension ContentView {
projectRootFolderURL = folderURL
projectTreeNodes = []
quickSwitcherProjectFileURLs = []
indexedProjectFileURLs = []
projectFileIndexSnapshot = .empty
isProjectFileIndexing = false
safeModeRecoveryPreparedForNextLaunch = false
applyProjectEditorOverrides(from: folderURL)

View file

@ -1,8 +1,8 @@
import Foundation
import SwiftUI
import UniformTypeIdentifiers
#if os(macOS)
import AppKit
import UniformTypeIdentifiers
#endif
extension ContentView {
@ -139,6 +139,37 @@ extension ContentView {
return "\(safeBase)-Preview.pdf"
}
func suggestedMarkdownPreviewBaseName() -> String {
let tabName = viewModel.selectedTab?.name ?? "Markdown-Preview"
let rawName = URL(fileURLWithPath: tabName).deletingPathExtension().lastPathComponent
return rawName.isEmpty ? "Markdown-Preview" : rawName
}
var markdownPreviewShareHTML: String {
markdownPreviewExportHTML(from: currentContent, mode: markdownPDFExportMode)
}
@MainActor
func copyMarkdownPreviewHTML() {
#if os(macOS)
NSPasteboard.general.clearContents()
NSPasteboard.general.setString(markdownPreviewShareHTML, forType: .string)
#elseif os(iOS)
UIPasteboard.general.setValue(markdownPreviewShareHTML, forPasteboardType: UTType.html.identifier)
UIPasteboard.general.string = markdownPreviewShareHTML
#endif
}
@MainActor
func copyMarkdownPreviewMarkdown() {
#if os(macOS)
NSPasteboard.general.clearContents()
NSPasteboard.general.setString(currentContent, forType: .string)
#elseif os(iOS)
UIPasteboard.general.string = currentContent
#endif
}
#if os(macOS)
@MainActor
func saveMarkdownPreviewPDFOnMac(_ data: Data, suggestedFilename: String) throws {

View file

@ -260,22 +260,24 @@ extension ContentView {
@ViewBuilder
private var languagePickerControl: some View {
Menu {
ForEach(languageOptions, id: \.self) { lang in
Button {
currentLanguagePickerBinding.wrappedValue = lang
} label: {
if lang == currentLanguagePickerBinding.wrappedValue {
Label(languageLabel(for: lang), systemImage: "checkmark")
} else {
Text(languageLabel(for: lang))
}
}
let selectedLanguage = currentLanguagePickerBinding.wrappedValue
Button {
currentLanguagePickerBinding.wrappedValue = selectedLanguage
} label: {
Label(languageLabel(for: selectedLanguage), systemImage: "checkmark")
}
Divider()
Button(action: { presentLanguageSearchSheet() }) {
Label("Language…", systemImage: "magnifyingglass")
}
.keyboardShortcut("l", modifiers: [.command, .shift])
Divider()
ForEach(languageOptions.filter { $0 != selectedLanguage }, id: \.self) { lang in
Button {
currentLanguagePickerBinding.wrappedValue = lang
} label: {
Text(languageLabel(for: lang))
}
}
} label: {
Text(toolbarCompactLanguageLabel(currentLanguagePickerBinding.wrappedValue))
.lineLimit(1)
@ -296,8 +298,8 @@ extension ContentView {
.accessibilityLabel("Language picker")
.accessibilityHint("Choose syntax language for the current tab")
.layoutPriority(2)
.tint(iOSToolbarTintColor)
#if os(iOS)
.tint(iOSToolbarTintColor)
.menuStyle(.button)
#endif
}
@ -837,8 +839,7 @@ extension ContentView {
if iPhonePromotedActionsCount >= 3 { saveFileControl }
if iPhonePromotedActionsCount >= 4 { findReplaceControl }
keyboardAccessoryControl
Divider()
.frame(height: 18)
iOSVerticalSurfaceDivider
moreActionsControl
}
@ -885,8 +886,7 @@ extension ContentView {
.contentShape(Rectangle())
}
if !iPadOverflowActions.isEmpty {
Divider()
.frame(height: 18)
iOSVerticalSurfaceDivider
.padding(.horizontal, 2)
iPadOverflowMenuControl
}
@ -1045,21 +1045,23 @@ extension ContentView {
ToolbarItemGroup(placement: .automatic) {
Menu {
ForEach(languageOptions, id: \.self) { lang in
let selectedLanguage = currentLanguagePickerBinding.wrappedValue
Button {
currentLanguagePickerBinding.wrappedValue = selectedLanguage
} label: {
Label(languageLabel(for: selectedLanguage), systemImage: "checkmark")
}
Button(action: { presentLanguageSearchSheet() }) {
Label("Language…", systemImage: "magnifyingglass")
}
Divider()
ForEach(languageOptions.filter { $0 != selectedLanguage }, id: \.self) { lang in
Button {
currentLanguagePickerBinding.wrappedValue = lang
} label: {
if lang == currentLanguagePickerBinding.wrappedValue {
Label(languageLabel(for: lang), systemImage: "checkmark")
} else {
Text(languageLabel(for: lang))
}
Text(languageLabel(for: lang))
}
}
Divider()
Button(action: { presentLanguageSearchSheet() }) {
Label("Language…", systemImage: "magnifyingglass")
}
} label: {
Text(toolbarCompactLanguageLabel(currentLanguagePickerBinding.wrappedValue))
.lineLimit(1)

View file

@ -254,7 +254,7 @@ struct ContentView: View {
#endif
#if os(iOS)
@State private var previousKeyboardAccessoryVisibility: Bool? = nil
@State private var markdownPreviewSheetDetent: PresentationDetent = .medium
@State var markdownPreviewSheetDetent: PresentationDetent = .medium
#endif
#if os(macOS)
@AppStorage("SettingsMacTranslucencyMode") private var macTranslucencyModeRaw: String = "balanced"
@ -302,7 +302,7 @@ struct ContentView: View {
@State var showQuickSwitcher: Bool = false
@State var quickSwitcherQuery: String = ""
@State var quickSwitcherProjectFileURLs: [URL] = []
@State var indexedProjectFileURLs: [URL] = []
@State var projectFileIndexSnapshot: ProjectFileIndex.Snapshot = .empty
@State var isProjectFileIndexing: Bool = false
@State var projectFileIndexRefreshGeneration: Int = 0
@State var projectFileIndexTask: Task<Void, Never>? = nil
@ -321,7 +321,7 @@ struct ContentView: View {
@State private var statusWordCount: Int = 0
@State private var statusLineCount: Int = 1
@State private var wordCountTask: Task<Void, Never>?
@State var vimModeEnabled: Bool = UserDefaults.standard.bool(forKey: "EditorVimModeEnabled")
@AppStorage("EditorVimModeEnabled") var vimModeEnabled: Bool = false
@State var vimInsertMode: Bool = true
@State var safeModeRecoveryPreparedForNextLaunch: Bool = false
@State var droppedFileLoadInProgress: Bool = false
@ -463,18 +463,21 @@ struct ContentView: View {
private var macUnifiedTranslucentMaterialStyle: AnyShapeStyle {
AnyShapeStyle(macTranslucencyMode.material.opacity(macTranslucencyMode.opacity))
}
private var macSolidSurfaceColor: Color {
currentEditorTheme(colorScheme: colorScheme).background
}
private var macChromeBackgroundStyle: AnyShapeStyle {
if enableTranslucentWindow {
return macUnifiedTranslucentMaterialStyle
}
return AnyShapeStyle(Color(nsColor: .textBackgroundColor))
return AnyShapeStyle(macSolidSurfaceColor)
}
private var macToolbarBackgroundStyle: AnyShapeStyle {
if enableTranslucentWindow {
return AnyShapeStyle(macTranslucencyMode.material.opacity(macTranslucencyMode.toolbarOpacity))
}
return AnyShapeStyle(Color(nsColor: .textBackgroundColor))
return AnyShapeStyle(macSolidSurfaceColor)
}
#elseif os(iOS)
var primaryGlassMaterial: Material { colorScheme == .dark ? .regularMaterial : .ultraThinMaterial }
@ -500,7 +503,7 @@ struct ContentView: View {
if enableTranslucentWindow {
return macUnifiedTranslucentMaterialStyle
}
return AnyShapeStyle(Color(nsColor: .textBackgroundColor))
return AnyShapeStyle(macSolidSurfaceColor)
#else
if useIOSUnifiedSolidSurfaces {
return AnyShapeStyle(iOSNonTranslucentSurfaceColor)
@ -2004,12 +2007,8 @@ struct ContentView: View {
} detail: {
editorView
}
.navigationSplitViewColumnWidth(min: 200, ideal: 250, max: 600)
.background(
enableTranslucentWindow
? AnyShapeStyle(.ultraThinMaterial)
: (useIOSUnifiedSolidSurfaces ? AnyShapeStyle(iOSNonTranslucentSurfaceColor) : AnyShapeStyle(Color.clear))
)
.navigationSplitViewColumnWidth(min: 200, ideal: 250, max: 600)
.background(editorSurfaceBackgroundStyle)
} else {
editorView
}
@ -2324,11 +2323,12 @@ struct ContentView: View {
}
#if canImport(UIKit)
.sheet(isPresented: contentView.$showSettingsSheet) {
NeonSettingsView(
ConfiguredSettingsView(
supportsOpenInTabs: false,
supportsTranslucency: false
supportsTranslucency: false,
supportPurchaseManager: contentView.supportPurchaseManager,
appUpdateManager: contentView.appUpdateManager
)
.environmentObject(contentView.supportPurchaseManager)
#if os(iOS)
.presentationDetents(contentView.settingsSheetDetents)
.presentationDragIndicator(.visible)
@ -2604,16 +2604,6 @@ struct ContentView: View {
) { result in
contentView.handleIOSExportResult(result)
}
.fileExporter(
isPresented: contentView.$showMarkdownPDFExporter,
document: contentView.markdownPDFExportDocument,
contentType: .pdf,
defaultFilename: contentView.markdownPDFExportFilename
) { result in
if case .failure(let error) = result {
contentView.markdownPDFExportErrorMessage = error.localizedDescription
}
}
#endif
}
}
@ -2655,7 +2645,7 @@ struct ContentView: View {
projectTreeNodes = []
quickSwitcherProjectFileURLs = []
stopProjectFolderObservation()
indexedProjectFileURLs = []
projectFileIndexSnapshot = .empty
isProjectFileIndexing = false
projectFileIndexTask?.cancel()
projectFileIndexTask = nil
@ -2682,7 +2672,7 @@ struct ContentView: View {
projectTreeNodes = []
quickSwitcherProjectFileURLs = []
stopProjectFolderObservation()
indexedProjectFileURLs = []
projectFileIndexSnapshot = .empty
isProjectFileIndexing = false
projectFileIndexTask?.cancel()
projectFileIndexTask = nil
@ -3162,7 +3152,11 @@ struct ContentView: View {
)
.frame(minWidth: 200, idealWidth: 250, maxWidth: 600)
.safeAreaInset(edge: .bottom) {
#if os(iOS)
iOSHorizontalSurfaceDivider
#else
Divider()
#endif
}
.background(editorSurfaceBackgroundStyle)
} else {
@ -3727,6 +3721,7 @@ struct ContentView: View {
idealWidth: clampedProjectSidebarWidth,
maxWidth: clampedProjectSidebarWidth
)
.background(editorSurfaceBackgroundStyle)
#endif
}
@ -3798,6 +3793,49 @@ struct ContentView: View {
#endif
}
#if os(iOS)
var iOSSurfaceSeparatorFill: Color {
iOSNonTranslucentSurfaceColor
}
var iOSSurfaceSeparatorLine: Color {
colorScheme == .dark ? Color.white.opacity(0.14) : Color.black.opacity(0.10)
}
var iOSPaneDivider: some View {
ZStack {
Rectangle()
.fill(iOSSurfaceSeparatorFill)
Rectangle()
.fill(iOSSurfaceSeparatorLine)
.frame(width: 1)
}
.frame(width: 10)
}
var iOSHorizontalSurfaceDivider: some View {
ZStack {
Rectangle()
.fill(iOSSurfaceSeparatorFill)
Rectangle()
.fill(iOSSurfaceSeparatorLine)
.frame(height: 1)
}
.frame(height: 10)
}
var iOSVerticalSurfaceDivider: some View {
ZStack {
Rectangle()
.fill(iOSSurfaceSeparatorFill)
Rectangle()
.fill(iOSSurfaceSeparatorLine)
.frame(width: 1)
}
.frame(width: 10, height: 18)
}
#endif
private var projectStructureSidebarBody: some View {
ProjectStructureSidebarView(
rootFolderURL: projectRootFolderURL,
@ -3865,7 +3903,7 @@ struct ContentView: View {
private var delimitedHeaderBackgroundColor: Color {
#if os(macOS)
Color(nsColor: .windowBackgroundColor)
currentEditorTheme(colorScheme: colorScheme).background
#else
Color(.systemBackground)
#endif
@ -4192,7 +4230,11 @@ struct ContentView: View {
)
if canShowMarkdownPreviewSplitPane && showMarkdownPreviewPane && currentLanguage == "markdown" && !brainDumpLayoutEnabled {
#if os(iOS)
iOSPaneDivider
#else
Divider()
#endif
markdownPreviewPane
.frame(minWidth: 280, idealWidth: 420, maxWidth: 680, maxHeight: .infinity)
}
@ -4208,7 +4250,7 @@ struct ContentView: View {
Color.clear.background(editorSurfaceBackgroundStyle)
} else {
#if os(iOS)
useIOSUnifiedSolidSurfaces ? iOSNonTranslucentSurfaceColor : Color.clear
Color.clear.background(editorSurfaceBackgroundStyle)
#else
Color.clear
#endif
@ -4312,6 +4354,10 @@ struct ContentView: View {
applyWindowTranslucency(enableTranslucentWindow)
highlightRefreshToken &+= 1
}
.onChange(of: colorScheme) { _, _ in
applyWindowTranslucency(enableTranslucentWindow)
highlightRefreshToken &+= 1
}
#endif
.toolbar {
editorToolbarContent
@ -4391,49 +4437,7 @@ struct ContentView: View {
@ViewBuilder
private var markdownPreviewPane: some View {
VStack(alignment: .leading, spacing: 0) {
HStack {
Text("Markdown Preview")
.font(.headline)
Spacer()
Picker("Template", selection: $markdownPreviewTemplateRaw) {
Text("Default").tag("default")
Text("Docs").tag("docs")
Text("Article").tag("article")
Text("Compact").tag("compact")
Text("GitHub Docs").tag("github-docs")
Text("Academic Paper").tag("academic-paper")
Text("Terminal Notes").tag("terminal-notes")
Text("Magazine").tag("magazine")
Text("Minimal Reader").tag("minimal-reader")
Text("Presentation").tag("presentation")
Text("Night Contrast").tag("night-contrast")
Text("Warm Sepia").tag("warm-sepia")
Text("Dense Compact").tag("dense-compact")
Text("Developer Spec").tag("developer-spec")
}
.labelsHidden()
.pickerStyle(.menu)
.frame(minWidth: 120, idealWidth: 190, maxWidth: 220)
Picker("PDF Mode", selection: $markdownPDFExportModeRaw) {
Text("Paginated Fit").tag(MarkdownPDFExportMode.paginatedFit.rawValue)
Text("One Page Fit").tag(MarkdownPDFExportMode.onePageFit.rawValue)
}
.labelsHidden()
.pickerStyle(.menu)
.frame(minWidth: 128, idealWidth: 160, maxWidth: 180)
Button {
exportMarkdownPreviewPDF()
} label: {
Label("Export PDF", systemImage: "doc.badge.arrow.down")
.lineLimit(1)
.fixedSize(horizontal: true, vertical: false)
}
.buttonStyle(.borderedProminent)
.tint(NeonUIStyle.accentBlue)
.controlSize(.regular)
.layoutPriority(1)
.accessibilityLabel("Export Markdown preview as PDF")
}
markdownPreviewHeader
.padding(.horizontal, 12)
.padding(.vertical, 10)
.background(editorSurfaceBackgroundStyle)
@ -4447,9 +4451,270 @@ struct ContentView: View {
.accessibilityLabel("Markdown Preview Content")
}
.background(editorSurfaceBackgroundStyle)
#if canImport(UIKit)
.fileExporter(
isPresented: $showMarkdownPDFExporter,
document: markdownPDFExportDocument,
contentType: .pdf,
defaultFilename: markdownPDFExportFilename
) { result in
if case .failure(let error) = result {
markdownPDFExportErrorMessage = error.localizedDescription
}
}
#endif
}
#endif
@ViewBuilder
private var markdownPreviewHeader: some View {
#if os(iOS)
if UIDevice.current.userInterfaceIdiom == .phone {
VStack(spacing: 16) {
VStack(spacing: 10) {
markdownPreviewPickerRow("Template") {
markdownPreviewTemplatePicker
}
markdownPreviewPickerRow("PDF Mode") {
markdownPreviewPDFModePicker
}
HStack {
Spacer()
markdownPreviewExportButton
Spacer()
}
.padding(.top, 4)
}
.padding(16)
.background(.thinMaterial, in: RoundedRectangle(cornerRadius: 18, style: .continuous))
}
.frame(maxWidth: .infinity)
} else if UIDevice.current.userInterfaceIdiom == .pad {
markdownPreviewIPadHeader
} else {
markdownPreviewRegularHeader
}
#else
markdownPreviewRegularHeader
#endif
}
private var markdownPreviewRegularHeader: some View {
VStack(spacing: 16) {
Text("Markdown Preview")
.font(.headline)
VStack(spacing: 10) {
HStack(alignment: .top, spacing: 24) {
markdownPreviewCenteredPickerGroup("Template") {
markdownPreviewTemplatePicker
}
markdownPreviewCenteredPickerGroup("PDF Mode") {
markdownPreviewPDFModePicker
}
}
.frame(maxWidth: .infinity, alignment: .center)
markdownPreviewActionRow {
markdownPreviewActionButtons
}
.padding(.top, 2)
}
#if os(iOS)
.frame(minWidth: 320, maxWidth: 420)
#else
.frame(minWidth: 520, idealWidth: 640, maxWidth: 760)
#endif
.padding(16)
.background(.thinMaterial, in: RoundedRectangle(cornerRadius: 18, style: .continuous))
}
.frame(maxWidth: .infinity, alignment: .center)
}
private var markdownPreviewIPadHeader: some View {
VStack(spacing: 16) {
Text("Markdown Preview")
.font(.headline)
.frame(maxWidth: .infinity, alignment: .center)
VStack(spacing: 10) {
HStack(alignment: .top, spacing: 28) {
markdownPreviewCenteredPickerGroup("Template") {
markdownPreviewTemplatePicker
}
markdownPreviewCenteredPickerGroup("PDF Mode") {
markdownPreviewPDFModePicker
}
}
.frame(maxWidth: .infinity, alignment: .center)
markdownPreviewActionRow {
markdownPreviewActionButtons
}
.padding(.top, 2)
}
.frame(minWidth: 560, idealWidth: 700, maxWidth: 820)
.padding(16)
.background(.thinMaterial, in: RoundedRectangle(cornerRadius: 18, style: .continuous))
}
.frame(maxWidth: .infinity, alignment: .center)
}
private var markdownPreviewTemplatePicker: some View {
Picker("Template", selection: $markdownPreviewTemplateRaw) {
Text("Default").tag("default")
Text("Docs").tag("docs")
Text("Article").tag("article")
Text("Compact").tag("compact")
Text("GitHub Docs").tag("github-docs")
Text("Academic Paper").tag("academic-paper")
Text("Terminal Notes").tag("terminal-notes")
Text("Magazine").tag("magazine")
Text("Minimal Reader").tag("minimal-reader")
Text("Presentation").tag("presentation")
Text("Night Contrast").tag("night-contrast")
Text("Warm Sepia").tag("warm-sepia")
Text("Dense Compact").tag("dense-compact")
Text("Developer Spec").tag("developer-spec")
}
.labelsHidden()
.pickerStyle(.menu)
#if os(iOS)
.frame(maxWidth: .infinity, alignment: .leading)
#else
.frame(minWidth: 120, idealWidth: 190, maxWidth: 220)
#endif
}
private var markdownPreviewPDFModePicker: some View {
Picker("PDF Mode", selection: $markdownPDFExportModeRaw) {
Text("Paginated Fit").tag(MarkdownPDFExportMode.paginatedFit.rawValue)
Text("One Page Fit").tag(MarkdownPDFExportMode.onePageFit.rawValue)
}
.labelsHidden()
.pickerStyle(.menu)
#if os(iOS)
.frame(maxWidth: .infinity, alignment: .leading)
#else
.frame(minWidth: 128, idealWidth: 160, maxWidth: 180)
#endif
}
private var markdownPreviewExportButton: some View {
Button {
exportMarkdownPreviewPDF()
} label: {
Label("Export PDF", systemImage: "doc.badge.arrow.down")
.lineLimit(1)
.fixedSize(horizontal: true, vertical: false)
}
.buttonStyle(.borderedProminent)
.tint(NeonUIStyle.accentBlue)
.controlSize(.regular)
.layoutPriority(1)
.accessibilityLabel("Export Markdown preview as PDF")
}
private var markdownPreviewShareButton: some View {
ShareLink(
item: markdownPreviewShareHTML,
preview: SharePreview("\(suggestedMarkdownPreviewBaseName()).html")
) {
Label("Share", systemImage: "square.and.arrow.up")
.lineLimit(1)
.fixedSize(horizontal: true, vertical: false)
}
.buttonStyle(.bordered)
.controlSize(.regular)
.layoutPriority(1)
.accessibilityLabel("Share Markdown preview HTML")
}
private var markdownPreviewCopyHTMLButton: some View {
Button {
copyMarkdownPreviewHTML()
} label: {
Label("Copy HTML", systemImage: "doc.on.doc")
.lineLimit(1)
.fixedSize(horizontal: true, vertical: false)
}
.buttonStyle(.bordered)
.controlSize(.regular)
.layoutPriority(1)
.accessibilityLabel("Copy Markdown preview HTML")
}
private var markdownPreviewCopyMarkdownButton: some View {
Button {
copyMarkdownPreviewMarkdown()
} label: {
Label("Copy Markdown", systemImage: "doc.on.clipboard")
.lineLimit(1)
.fixedSize(horizontal: true, vertical: false)
}
.buttonStyle(.bordered)
.controlSize(.regular)
.layoutPriority(1)
.accessibilityLabel("Copy Markdown source")
}
@ViewBuilder
private var markdownPreviewActionButtons: some View {
HStack(spacing: 10) {
markdownPreviewExportButton
markdownPreviewShareButton
#if os(macOS)
markdownPreviewCopyHTMLButton
markdownPreviewCopyMarkdownButton
#else
#if os(iOS)
if UIDevice.current.userInterfaceIdiom == .pad {
markdownPreviewCopyHTMLButton
}
#endif
#endif
}
}
@ViewBuilder
private func markdownPreviewCenteredPickerGroup<Content: View>(_ title: String, @ViewBuilder content: () -> Content) -> some View {
VStack(spacing: 8) {
Text(title)
.font(.system(size: 13, weight: .semibold))
.foregroundStyle(.secondary)
.frame(maxWidth: .infinity, alignment: .center)
content()
.frame(maxWidth: .infinity, alignment: .center)
}
.frame(minWidth: 220, maxWidth: 260, alignment: .center)
}
@ViewBuilder
private func markdownPreviewPickerRow<Content: View>(_ title: String, @ViewBuilder content: () -> Content) -> some View {
HStack(alignment: .center, spacing: 14) {
Text(title)
.font(.system(size: 13, weight: .semibold))
.foregroundStyle(.secondary)
.frame(maxWidth: .infinity, alignment: .leading)
content()
.frame(maxWidth: .infinity, alignment: .trailing)
}
}
@ViewBuilder
private func markdownPreviewActionRow<Content: View>(@ViewBuilder content: () -> Content) -> some View {
HStack(spacing: 14) {
content()
}
.frame(maxWidth: .infinity, alignment: .center)
}
#if os(iOS)
@ViewBuilder
private var iPhoneUnifiedTopChromeHost: some View {
@ -4689,7 +4954,11 @@ struct ContentView: View {
.padding(.trailing, 10)
.padding(.vertical, 6)
}
#if os(iOS)
iOSHorizontalSurfaceDivider.opacity(0.7)
#else
Divider().opacity(0.45)
#endif
}
.frame(minHeight: 42, maxHeight: 42, alignment: .center)
#if os(macOS)
@ -4710,7 +4979,9 @@ struct ContentView: View {
guard vimModeEnabled else { return " • Vim: OFF" }
return vimInsertMode ? " • Vim: INSERT" : " • Vim: NORMAL"
#else
return ""
guard UIDevice.current.userInterfaceIdiom == .pad else { return "" }
guard vimModeEnabled else { return " • Vim: OFF" }
return vimInsertMode ? " • Vim: INSERT" : " • Vim: NORMAL"
#endif
}
@ -4762,23 +5033,37 @@ struct ContentView: View {
)
}
let projectQuickSwitcherFileURLs = indexedProjectFileURLs.isEmpty
? quickSwitcherProjectFileURLs
: indexedProjectFileURLs
for url in projectQuickSwitcherFileURLs {
let standardized = url.standardizedFileURL.path
if fileURLSet.contains(standardized) { continue }
if items.contains(where: { $0.id == "file:\(standardized)" }) { continue }
items.append(
QuickFileSwitcherPanel.Item(
id: "file:\(standardized)",
title: url.lastPathComponent,
subtitle: standardized,
isPinned: false,
canTogglePin: true
if projectFileIndexSnapshot.entries.isEmpty {
for url in quickSwitcherProjectFileURLs {
let standardized = url.standardizedFileURL.path
if fileURLSet.contains(standardized) { continue }
if items.contains(where: { $0.id == "file:\(standardized)" }) { continue }
items.append(
QuickFileSwitcherPanel.Item(
id: "file:\(standardized)",
title: url.lastPathComponent,
subtitle: standardized,
isPinned: false,
canTogglePin: true
)
)
)
}
} else {
for entry in projectFileIndexSnapshot.entries {
let standardized = entry.standardizedPath
let subtitle = entry.relativePath == entry.displayName ? standardized : entry.relativePath
if fileURLSet.contains(standardized) { continue }
if items.contains(where: { $0.id == "file:\(standardized)" }) { continue }
items.append(
QuickFileSwitcherPanel.Item(
id: "file:\(standardized)",
title: entry.displayName,
subtitle: subtitle,
isPinned: false,
canTogglePin: true
)
)
}
}
let query = quickSwitcherQuery.trimmingCharacters(in: .whitespacesAndNewlines)
@ -4946,6 +5231,7 @@ struct ContentView: View {
}
findInFilesTask?.cancel()
let indexedProjectFileURLs = projectFileIndexSnapshot.fileURLs
let candidateFiles = indexedProjectFileURLs.isEmpty ? nil : indexedProjectFileURLs
if candidateFiles == nil, isProjectFileIndexing {
findInFilesStatusMessage = "Searching while project index updates…"

View file

@ -1962,9 +1962,6 @@ struct CustomTextEditor: NSViewRepresentable {
}
private func effectiveBaseTextColor() -> NSColor {
if colorScheme == .light && !translucentBackgroundEnabled {
return NSColor.textColor
}
let theme = currentEditorTheme(colorScheme: colorScheme)
return NSColor(theme.text)
}
@ -2058,8 +2055,7 @@ struct CustomTextEditor: NSViewRepresentable {
textView.backgroundColor = .clear
textView.drawsBackground = false
} else {
let bg = (colorScheme == .light) ? NSColor.textBackgroundColor : NSColor(theme.background)
textView.backgroundColor = bg
textView.backgroundColor = NSColor(theme.background)
textView.drawsBackground = true
}
@ -2070,7 +2066,7 @@ struct CustomTextEditor: NSViewRepresentable {
textView.allowsUndo = !isLargeFileMode
let baseTextColor = effectiveBaseTextColor()
textView.textColor = baseTextColor
textView.insertionPointColor = (colorScheme == .light && !translucentBackgroundEnabled) ? NSColor.labelColor : NSColor(theme.cursor)
textView.insertionPointColor = NSColor(theme.cursor)
textView.selectedTextAttributes = [
.backgroundColor: NSColor(theme.selection)
]
@ -2306,12 +2302,11 @@ struct CustomTextEditor: NSViewRepresentable {
textView.drawsBackground = false
} else {
nsView.drawsBackground = false
let bg = (colorScheme == .light) ? NSColor.textBackgroundColor : NSColor(theme.background)
textView.backgroundColor = bg
textView.backgroundColor = NSColor(theme.background)
textView.drawsBackground = true
}
let baseTextColor = effectiveBaseTextColor()
let caretColor = (colorScheme == .light && !translucentBackgroundEnabled) ? NSColor.labelColor : NSColor(theme.cursor)
let caretColor = NSColor(theme.cursor)
if textView.insertionPointColor != caretColor {
textView.insertionPointColor = caretColor
}

View file

@ -14,6 +14,19 @@ import UIKit
/// MARK: - Types
struct NeonSettingsView: View {
private struct SettingsTabPage: View {
let title: String
let systemImage: String
let tag: String
let content: AnyView
var body: some View {
content
.tabItem { Label(title, systemImage: systemImage) }
.tag(tag)
}
}
fileprivate static let defaultSettingsTab = "general"
private static var cachedEditorFonts: [String] = []
let supportsOpenInTabs: Bool
@ -57,6 +70,8 @@ struct NeonSettingsView: View {
@AppStorage("SettingsAutoCloseBrackets") private var autoCloseBrackets: Bool = false
@AppStorage("SettingsTrimTrailingWhitespace") private var trimTrailingWhitespace: Bool = false
@AppStorage("SettingsTrimWhitespaceForSyntaxDetection") private var trimWhitespaceForSyntaxDetection: Bool = false
@AppStorage("EditorVimModeEnabled") private var vimModeEnabled: Bool = false
@AppStorage("EditorVimInterceptionEnabled") private var vimInterceptionEnabled: Bool = false
@AppStorage("SettingsProjectNavigatorPlacement") private var projectNavigatorPlacementRaw: String = ContentView.ProjectNavigatorPlacement.trailing.rawValue
@AppStorage("SettingsPerformancePreset") private var performancePresetRaw: String = ContentView.PerformancePreset.balanced.rawValue
@AppStorage("SettingsLargeFileSyntaxHighlighting") private var largeFileSyntaxHighlightingRaw: String = "minimal"
@ -141,6 +156,29 @@ struct NeonSettingsView: View {
#endif
}
private var isIPadDevice: Bool {
#if os(iOS)
UIDevice.current.userInterfaceIdiom == .pad
#else
false
#endif
}
private var vimModeBinding: Binding<Bool> {
Binding(
get: { vimModeEnabled && vimInterceptionEnabled },
set: { enabled in
vimModeEnabled = enabled
vimInterceptionEnabled = enabled
NotificationCenter.default.post(
name: .vimModeStateDidChange,
object: nil,
userInfo: ["insertMode": !enabled]
)
}
)
}
private var standardLabelWidth: CGFloat {
useTwoColumnSettingsLayout ? 180 : 140
}
@ -216,35 +254,59 @@ struct NeonSettingsView: View {
private var settingsTabs: some View {
TabView(selection: $settingsActiveTab) {
generalTab
.tabItem { Label("General", systemImage: "gearshape") }
.tag("general")
editorTab
.tabItem { Label("Editor", systemImage: "slider.horizontal.3") }
.tag("editor")
templateTab
.tabItem { Label("Templates", systemImage: "doc.badge.plus") }
.tag("templates")
themeTab
.tabItem { Label("Themes", systemImage: "paintpalette") }
.tag("themes")
SettingsTabPage(
title: "General",
systemImage: "gearshape",
tag: "general",
content: AnyView(generalTab)
)
SettingsTabPage(
title: "Editor",
systemImage: "slider.horizontal.3",
tag: "editor",
content: AnyView(editorTab)
)
SettingsTabPage(
title: "Templates",
systemImage: "doc.badge.plus",
tag: "templates",
content: AnyView(templateTab)
)
SettingsTabPage(
title: "Themes",
systemImage: "paintpalette",
tag: "themes",
content: AnyView(themeTab)
)
#if os(iOS)
moreTab
.tabItem { Label("More", systemImage: "ellipsis.circle") }
.tag("more")
SettingsTabPage(
title: "More",
systemImage: "ellipsis.circle",
tag: "more",
content: AnyView(moreTab)
)
#else
supportTab
.tabItem { Label("Support", systemImage: "heart") }
.tag("support")
aiTab
.tabItem { Label("AI", systemImage: "brain.head.profile") }
.tag("ai")
SettingsTabPage(
title: "Support",
systemImage: "heart",
tag: "support",
content: AnyView(supportTab)
)
SettingsTabPage(
title: "AI",
systemImage: "brain.head.profile",
tag: "ai",
content: AnyView(aiTab)
)
#endif
#if os(macOS)
if ReleaseRuntimePolicy.isUpdaterEnabledForCurrentDistribution {
updatesTab
.tabItem { Label("Updates", systemImage: "arrow.triangle.2.circlepath.circle") }
.tag("updates")
SettingsTabPage(
title: "Updates",
systemImage: "arrow.triangle.2.circlepath.circle",
tag: "updates",
content: AnyView(updatesTab)
)
}
#endif
}
@ -289,7 +351,8 @@ struct NeonSettingsView: View {
idealSize: macSettingsWindowSize.ideal,
translucentEnabled: supportsTranslucency && translucentWindow,
translucencyModeRaw: macTranslucencyModeRaw,
appearanceRaw: appearance
appearanceRaw: appearance,
effectiveColorScheme: effectiveSettingsColorScheme
)
)
#endif
@ -1184,6 +1247,13 @@ struct NeonSettingsView: View {
Toggle("Auto Close Brackets", isOn: $autoCloseBrackets)
Toggle("Trim Trailing Whitespace", isOn: $trimTrailingWhitespace)
Toggle("Trim Edges for Syntax Detection", isOn: $trimWhitespaceForSyntaxDetection)
if isIPadDevice {
Divider()
Toggle("Enable Vim Mode", isOn: vimModeBinding)
Text("Requires a hardware keyboard on iPad. Escape returns to Normal mode, and the status line shows INSERT or NORMAL while Vim mode is active.")
.font(Typography.footnote)
.foregroundStyle(.secondary)
}
}
settingsCardSection(
@ -2622,6 +2692,7 @@ struct SettingsWindowConfigurator: NSViewRepresentable {
let translucentEnabled: Bool
let translucencyModeRaw: String
let appearanceRaw: String
let effectiveColorScheme: ColorScheme
final class Coordinator {
var didInitialApply = false
@ -2727,19 +2798,23 @@ struct SettingsWindowConfigurator: NSViewRepresentable {
case "dark":
isDark = true
default:
isDark = window.effectiveAppearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua
isDark = effectiveColorScheme == .dark
}
let whiteLevel: CGFloat
let alpha: CGFloat
switch translucencyModeRaw {
case "subtle":
whiteLevel = isDark ? 0.18 : 0.90
alpha = 0.98
case "vibrant":
whiteLevel = isDark ? 0.12 : 0.82
alpha = 0.98
default:
whiteLevel = isDark ? 0.15 : 0.86
alpha = 0.98
}
// Keep settings tint almost opaque to avoid "more transparent" appearance.
return NSColor(calibratedWhite: whiteLevel, alpha: 0.98)
return NSColor(calibratedWhite: whiteLevel, alpha: alpha)
}
private func centerSettingsWindow(_ settingsWindow: NSWindow) {

View file

@ -377,12 +377,12 @@ struct WelcomeTourView: View {
private let pages: [TourPage] = [
TourPage(
title: "Whats New in This Release",
subtitle: "Major changes since v0.5.5:",
subtitle: "Major changes since v0.5.6:",
bullets: [
"![v0.5.6 hero screenshot](docs/images/iphone-themes-light.png)",
"Safe Mode now recovers from repeated failed launches without getting stuck on every normal restart.",
"Large project folders now get a background file index that feeds `Quick Open` and `Find in Files` instead of relying only on live folder scans.",
"Markdown documents can now be exported directly from preview as PDF in both paginated and one-page formats."
"Markdown preview on iPhone now uses a cleaner centered layout, and PDF export is presented from the active preview flow.",
"System light/dark switching now keeps editor, sidebar, header, and Settings surfaces aligned on macOS.",
"Support-purchase messaging is more neutral in App Review or restricted StoreKit environments.",
"Project indexing and iPad Vim-mode wiring are now completed for the current release line."
],
iconName: "sparkles.rectangle.stack",
colors: [Color(red: 0.40, green: 0.28, blue: 0.90), Color(red: 0.96, green: 0.46, blue: 0.55)],

View file

@ -110,30 +110,9 @@ struct SidebarView: View {
#endif
}
#if os(macOS)
return AnyShapeStyle(Color(nsColor: .textBackgroundColor))
return AnyShapeStyle(currentEditorTheme(colorScheme: colorScheme).background)
#else
if colorScheme == .dark {
return AnyShapeStyle(
LinearGradient(
colors: [
Color(red: 0.11, green: 0.13, blue: 0.17),
Color(red: 0.15, green: 0.18, blue: 0.23)
],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
}
return AnyShapeStyle(
LinearGradient(
colors: [
Color(red: 0.92, green: 0.96, blue: 1.0),
Color(red: 0.88, green: 0.93, blue: 1.0)
],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
return AnyShapeStyle(currentEditorTheme(colorScheme: colorScheme).background)
#endif
}
@ -514,30 +493,9 @@ struct ProjectStructureSidebarView: View {
#endif
}
#if os(macOS)
return AnyShapeStyle(Color(nsColor: .textBackgroundColor))
return AnyShapeStyle(currentEditorTheme(colorScheme: colorScheme).background)
#else
if colorScheme == .dark {
return AnyShapeStyle(
LinearGradient(
colors: [
Color(red: 0.11, green: 0.13, blue: 0.17),
Color(red: 0.15, green: 0.18, blue: 0.23)
],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
}
return AnyShapeStyle(
LinearGradient(
colors: [
Color(red: 0.92, green: 0.96, blue: 1.0),
Color(red: 0.88, green: 0.93, blue: 1.0)
],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
return AnyShapeStyle(currentEditorTheme(colorScheme: colorScheme).background)
#endif
}

View file

@ -31,7 +31,7 @@
"Failed to load App Store products: %@" = "App-Store-Produkte konnten nicht geladen werden: %@";
"In-app purchase is only available in App Store/TestFlight builds." = "In-App-Kauf ist nur in App-Store-/TestFlight-Builds verfügbar.";
"In-app purchase is only available in App Store/TestFlight builds. Use external support in direct distribution." = "In-App-Kauf ist nur in App-Store-/TestFlight-Builds verfügbar. In externer Distribution nutze den externen Support.";
"In-App Purchases are disabled on this device. Check App Store login and Screen Time restrictions." = "In-App-Käufe sind auf diesem Gerät deaktiviert. Prüfe App-Store-Login und Bildschirmzeit-Beschränkungen.";
"In-App Purchases are disabled on this device. Check App Store login and Screen Time restrictions." = "In-App-Käufe sind in dieser Umgebung derzeit nicht verfügbar. Bitte versuche es später erneut.";
"Support purchase is currently unavailable." = "Support-Kauf ist derzeit nicht verfügbar.";
"Thank you for supporting Neon Vision Editor." = "Danke für deine Unterstützung von Neon Vision Editor.";
"Purchase is pending approval." = "Der Kauf wartet auf Genehmigung.";
@ -312,6 +312,8 @@
"Cant Open File" = "Datei kann nicht geöffnet werden";
"The file \"%@\" is not supported and cant be opened." = "Die Datei \"%@\" wird nicht unterstützt und kann nicht geöffnet werden.";
"Support via Patreon" = "Unterstützen via Patreon";
"App Store pricing is currently unavailable." = "App-Store-Preise sind derzeit nicht verfügbar.";
"In-App Purchases are currently unavailable on this device. Check App Store login and Screen Time restrictions." = "In-App-Käufe sind auf diesem Gerät derzeit nicht verfügbar. Prüfe App-Store-Login und Bildschirmzeit-Beschränkungen.";
"App Store pricing is currently unavailable." = "App-Store-Preise sind in dieser Umgebung derzeit nicht verfügbar. Bitte versuche es später erneut.";
"In-App Purchases are currently unavailable on this device. Check App Store login and Screen Time restrictions." = "In-App-Käufe sind in dieser Umgebung derzeit nicht verfügbar. Bitte versuche es später erneut.";
"Show Supported Files Only" = "Nur unterstützte Dateien anzeigen";
"Enable Vim Mode" = "Vim-Modus aktivieren";
"Requires a hardware keyboard on iPad. Escape returns to Normal mode, and the status line shows INSERT or NORMAL while Vim mode is active." = "Erfordert eine Hardware-Tastatur auf dem iPad. Escape wechselt in den Normal-Modus, und die Statusleiste zeigt bei aktivem Vim-Modus INSERT oder NORMAL an.";

View file

@ -31,7 +31,7 @@
"Failed to load App Store products: %@" = "Failed to load App Store products: %@";
"In-app purchase is only available in App Store/TestFlight builds." = "In-app purchase is only available in App Store/TestFlight builds.";
"In-app purchase is only available in App Store/TestFlight builds. Use external support in direct distribution." = "In-app purchase is only available in App Store/TestFlight builds. Use external support in direct distribution.";
"In-App Purchases are disabled on this device. Check App Store login and Screen Time restrictions." = "In-App Purchases are disabled on this device. Check App Store login and Screen Time restrictions.";
"In-App Purchases are disabled on this device. Check App Store login and Screen Time restrictions." = "In-App Purchases are currently unavailable in this environment. Please try again later.";
"Support purchase is currently unavailable." = "Support purchase is currently unavailable.";
"Thank you for supporting Neon Vision Editor." = "Thank you for supporting Neon Vision Editor.";
"Purchase is pending approval." = "Purchase is pending approval.";
@ -213,6 +213,8 @@
"Cant Open File" = "Cant Open File";
"The file \"%@\" is not supported and cant be opened." = "The file \"%@\" is not supported and cant be opened.";
"Support via Patreon" = "Support via Patreon";
"App Store pricing is currently unavailable." = "App Store pricing is currently unavailable.";
"In-App Purchases are currently unavailable on this device. Check App Store login and Screen Time restrictions." = "In-App Purchases are currently unavailable on this device. Check App Store login and Screen Time restrictions.";
"App Store pricing is currently unavailable." = "App Store pricing is currently unavailable in this environment. Please try again later.";
"In-App Purchases are currently unavailable on this device. Check App Store login and Screen Time restrictions." = "In-App Purchases are currently unavailable in this environment. Please try again later.";
"Show Supported Files Only" = "Show Supported Files Only";
"Enable Vim Mode" = "Enable Vim Mode";
"Requires a hardware keyboard on iPad. Escape returns to Normal mode, and the status line shows INSERT or NORMAL while Vim mode is active." = "Requires a hardware keyboard on iPad. Escape returns to Normal mode, and the status line shows INSERT or NORMAL while Vim mode is active.";

View file

@ -64,7 +64,8 @@
> Latest release: **v0.5.6**
> Platform target: **macOS 26 (Tahoe)** compatible with **macOS Sequoia**
> Apple Silicon: tested / Intel: not tested
> Last updated (README): **2026-03-26** for release line **v0.5.6**
<<<<<<< HEAD
> Last updated (README): **2026-03-26** for active development line **v0.5.7** while latest tagged release remains **v0.5.6**
## Start Here
@ -500,12 +501,12 @@ Most editor features are shared across macOS, iOS, and iPadOS.
## Roadmap (Near Term)
<p align="center">
<img alt="Now" src="https://img.shields.io/badge/NOW-v0.5.4%20to%20v0.5.6-22C55E?style=for-the-badge">
<img alt="Next" src="https://img.shields.io/badge/NEXT-v0.5.7%20to%20v0.5.9-F59E0B?style=for-the-badge">
<img alt="Now" src="https://img.shields.io/badge/NOW-v0.5.5%20to%20v0.5.7-22C55E?style=for-the-badge">
<img alt="Next" src="https://img.shields.io/badge/NEXT-v0.5.8%20to%20v0.5.9-F59E0B?style=for-the-badge">
<img alt="Later" src="https://img.shields.io/badge/LATER-v0.6.0-0A84FF?style=for-the-badge">
</p>
### Now (v0.5.4 - v0.5.6)
### Now (v0.5.5 - v0.5.7)
- ![v0.5.3](https://img.shields.io/badge/v0.5.3-22C55E?style=flat-square) indexed project search and Open Recent favorites.
Tracking: [Milestone 0.5.3](https://github.com/h3pdesign/Neon-Vision-Editor/milestone/4) · [#29](https://github.com/h3pdesign/Neon-Vision-Editor/issues/29) · [#31](https://github.com/h3pdesign/Neon-Vision-Editor/issues/31)
@ -513,14 +514,14 @@ Most editor features are shared across macOS, iOS, and iPadOS.
Tracking: [Milestone 0.5.4](https://github.com/h3pdesign/Neon-Vision-Editor/milestone/5)
- ![v0.5.5](https://img.shields.io/badge/v0.5.5-22C55E?style=flat-square) first-open/sidebar rendering stabilization, session-restore hardening, and Code Snapshot workflow polish.
Tracking: [Milestone 0.5.5](https://github.com/h3pdesign/Neon-Vision-Editor/milestone/6) · [Release v0.5.5](https://github.com/h3pdesign/Neon-Vision-Editor/releases/tag/v0.5.5)
- ![v0.5.7](https://img.shields.io/badge/v0.5.7-22C55E?style=flat-square) bugfix-focused release line for Markdown preview/export polish, system appearance consistency, StoreKit review-safe messaging, and the final wiring for indexed project search plus iPad Vim mode.
Tracking: [Milestone 0.5.7](https://github.com/h3pdesign/Neon-Vision-Editor/milestone/8) · [#27](https://github.com/h3pdesign/Neon-Vision-Editor/issues/27) · [#29](https://github.com/h3pdesign/Neon-Vision-Editor/issues/29) · [#60](https://github.com/h3pdesign/Neon-Vision-Editor/issues/60) · [#61](https://github.com/h3pdesign/Neon-Vision-Editor/issues/61) · [#62](https://github.com/h3pdesign/Neon-Vision-Editor/issues/62)
### Next (v0.5.7 - v0.5.9)
### Next (v0.5.8 - v0.5.9)
- ![v0.5.6](https://img.shields.io/badge/v0.5.6-F59E0B?style=flat-square) Safe Mode startup.
Tracking: [#27](https://github.com/h3pdesign/Neon-Vision-Editor/issues/27)
- ![v0.5.7](https://img.shields.io/badge/v0.5.7-F59E0B?style=flat-square) incremental loading for huge files.
- ![v0.5.8](https://img.shields.io/badge/v0.5.8-F59E0B?style=flat-square) incremental loading for huge files.
Tracking: [#28](https://github.com/h3pdesign/Neon-Vision-Editor/issues/28)
- ![v0.5.8](https://img.shields.io/badge/v0.5.8-F59E0B?style=flat-square) follow-up platform polish and release hardening.
- ![v0.5.9](https://img.shields.io/badge/v0.5.9-F59E0B?style=flat-square) follow-up platform polish and release hardening.
### Later (v0.6.0)

View file

@ -1,283 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Neon Vision Editor Wordmark Preview</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Archivo:wght@800&family=Exo+2:wght@800&family=Manrope:wght@800&family=Outfit:wght@800&family=Space+Grotesk:wght@700&family=Sora:wght@800&family=Syne:wght@800&family=Urbanist:wght@800&display=swap" rel="stylesheet">
<style>
:root {
--page-bg: #f6f4fb;
--card-bg: rgba(255, 255, 255, 0.9);
--card-border: rgba(148, 163, 184, 0.22);
--text: #111827;
--muted: #5b6475;
--line: linear-gradient(90deg, #7c3aed 0%, #ba33c6 50%, #ff00bc 100%);
--light-wordmark: #ba0090;
--dark-wordmark: #ff00bc;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
padding: 32px 20px 48px;
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", sans-serif;
color: var(--text);
background:
radial-gradient(circle at top, rgba(186, 51, 198, 0.08), transparent 30%),
radial-gradient(circle at bottom right, rgba(37, 99, 235, 0.08), transparent 28%),
var(--page-bg);
}
main {
width: min(1160px, 100%);
margin: 0 auto;
}
h1 {
margin: 0 0 8px;
text-align: center;
font-size: clamp(2rem, 5vw, 3.2rem);
line-height: 1.05;
}
.lead {
margin: 0 auto 28px;
max-width: 760px;
text-align: center;
color: var(--muted);
font-size: 1rem;
line-height: 1.55;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 18px;
}
.card {
padding: 22px 20px 24px;
border: 1px solid var(--card-border);
border-radius: 24px;
background: var(--card-bg);
box-shadow: 0 16px 44px rgba(15, 23, 42, 0.08);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
}
.card h2 {
margin: 0 0 12px;
font-size: 1rem;
letter-spacing: 0.02em;
}
.meta {
margin: 10px 0 0;
color: var(--muted);
font-size: 0.92rem;
}
.demo {
border-radius: 20px;
padding: 18px 16px 20px;
background: #ffffff;
border: 1px solid rgba(148, 163, 184, 0.18);
}
.demo.dark {
background: #090e19;
border-color: rgba(148, 163, 184, 0.12);
}
.wordmark {
margin: 0;
text-align: center;
line-height: 1;
letter-spacing: 0.01em;
font-size: clamp(2rem, 4vw, 3.3rem);
font-weight: 800;
color: var(--light-wordmark);
}
.demo.dark .wordmark {
color: var(--dark-wordmark);
}
.accent {
width: 150px;
height: 8px;
margin: 14px auto 0;
border-radius: 999px;
background: var(--line);
}
.space-grotesk { font-family: "Space Grotesk", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
.sora { font-family: "Sora", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
.outfit { font-family: "Outfit", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
.manrope { font-family: "Manrope", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
.syne { font-family: "Syne", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
.exo { font-family: "Exo 2", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
.archivo { font-family: "Archivo", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
.urbanist { font-family: "Urbanist", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
.avenir { font-family: "Avenir Next", "Avenir", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
.helvetica { font-family: "Helvetica Neue", "Helvetica", Arial, sans-serif; }
@media (max-width: 640px) {
body {
padding: 20px 14px 34px;
}
.card {
padding: 18px 16px 20px;
}
}
</style>
</head>
<body>
<main>
<h1>README Wordmark Preview</h1>
<p class="lead">
Compare the same “Neon Vision Editor” wordmark across several headline fonts. Each card shows light and dark mode styling
with the current neon pink title color and accent line.
</p>
<section class="grid">
<article class="card">
<h2>Space Grotesk</h2>
<div class="demo">
<p class="wordmark space-grotesk">Neon Vision Editor</p>
<div class="accent"></div>
</div>
<div class="demo dark" style="margin-top: 12px;">
<p class="wordmark space-grotesk">Neon Vision Editor</p>
<div class="accent"></div>
</div>
<p class="meta">Tech-forward, geometric, compact. Good fit if you want a sharper product look.</p>
</article>
<article class="card">
<h2>Sora</h2>
<div class="demo">
<p class="wordmark sora">Neon Vision Editor</p>
<div class="accent"></div>
</div>
<div class="demo dark" style="margin-top: 12px;">
<p class="wordmark sora">Neon Vision Editor</p>
<div class="accent"></div>
</div>
<p class="meta">Cleaner and slightly friendlier than Space Grotesk, still modern and distinctive.</p>
</article>
<article class="card">
<h2>Outfit</h2>
<div class="demo">
<p class="wordmark outfit">Neon Vision Editor</p>
<div class="accent"></div>
</div>
<div class="demo dark" style="margin-top: 12px;">
<p class="wordmark outfit">Neon Vision Editor</p>
<div class="accent"></div>
</div>
<p class="meta">Rounder and softer. Works if you want the brand to feel calmer and less technical.</p>
</article>
<article class="card">
<h2>Syne</h2>
<div class="demo">
<p class="wordmark syne">Neon Vision Editor</p>
<div class="accent"></div>
</div>
<div class="demo dark" style="margin-top: 12px;">
<p class="wordmark syne">Neon Vision Editor</p>
<div class="accent"></div>
</div>
<p class="meta">More character and tension. Good if you want the title to feel more like a distinctive brand.</p>
</article>
<article class="card">
<h2>Manrope</h2>
<div class="demo">
<p class="wordmark manrope">Neon Vision Editor</p>
<div class="accent"></div>
</div>
<div class="demo dark" style="margin-top: 12px;">
<p class="wordmark manrope">Neon Vision Editor</p>
<div class="accent"></div>
</div>
<p class="meta">Balanced and premium, with less personality than Space Grotesk but very clean.</p>
</article>
<article class="card">
<h2>Exo 2</h2>
<div class="demo">
<p class="wordmark exo">Neon Vision Editor</p>
<div class="accent"></div>
</div>
<div class="demo dark" style="margin-top: 12px;">
<p class="wordmark exo">Neon Vision Editor</p>
<div class="accent"></div>
</div>
<p class="meta">Most futuristic of the set. Stronger sci-fi/editor-tool energy, but also less timeless.</p>
</article>
<article class="card">
<h2>Archivo</h2>
<div class="demo">
<p class="wordmark archivo">Neon Vision Editor</p>
<div class="accent"></div>
</div>
<div class="demo dark" style="margin-top: 12px;">
<p class="wordmark archivo">Neon Vision Editor</p>
<div class="accent"></div>
</div>
<p class="meta">Compressed and editorial. Feels stronger and more assertive than the softer geometric options.</p>
</article>
<article class="card">
<h2>Urbanist</h2>
<div class="demo">
<p class="wordmark urbanist">Neon Vision Editor</p>
<div class="accent"></div>
</div>
<div class="demo dark" style="margin-top: 12px;">
<p class="wordmark urbanist">Neon Vision Editor</p>
<div class="accent"></div>
</div>
<p class="meta">Cleaner and more digital-product oriented, with less personality than Syne but more polish than Helvetica.</p>
</article>
<article class="card">
<h2>Avenir Next</h2>
<div class="demo">
<p class="wordmark avenir">Neon Vision Editor</p>
<div class="accent"></div>
</div>
<div class="demo dark" style="margin-top: 12px;">
<p class="wordmark avenir">Neon Vision Editor</p>
<div class="accent"></div>
</div>
<p class="meta">Native Apple feel. More familiar and polished, but less brand-specific.</p>
</article>
<article class="card">
<h2>Helvetica Neue</h2>
<div class="demo">
<p class="wordmark helvetica">Neon Vision Editor</p>
<div class="accent"></div>
</div>
<div class="demo dark" style="margin-top: 12px;">
<p class="wordmark helvetica">Neon Vision Editor</p>
<div class="accent"></div>
</div>
<p class="meta">Most neutral option. Reliable, but less memorable as a branded header.</p>
</article>
</section>
</main>
</body>
</html>