mirror of
https://github.com/h3pdesign/Neon-Vision-Editor
synced 2026-04-21 13:27:16 +00:00
Prepare v0.5.7 bugfix release
This commit is contained in:
parent
a8a0455ceb
commit
f92bcc0e42
18 changed files with 967 additions and 663 deletions
30
CHANGELOG.md
30
CHANGELOG.md
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 = "";
|
||||
|
|
|
|||
|
|
@ -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() }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
18
Neon Vision Editor/UI/ConfiguredSettingsView.swift
Normal file
18
Neon Vision Editor/UI/ConfiguredSettingsView.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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…"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -377,12 +377,12 @@ struct WelcomeTourView: View {
|
|||
private let pages: [TourPage] = [
|
||||
TourPage(
|
||||
title: "What’s New in This Release",
|
||||
subtitle: "Major changes since v0.5.5:",
|
||||
subtitle: "Major changes since v0.5.6:",
|
||||
bullets: [
|
||||
"",
|
||||
"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)],
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 @@
|
|||
"Can’t Open File" = "Datei kann nicht geöffnet werden";
|
||||
"The file \"%@\" is not supported and can’t 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.";
|
||||
|
|
|
|||
|
|
@ -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 @@
|
|||
"Can’t Open File" = "Can’t Open File";
|
||||
"The file \"%@\" is not supported and can’t be opened." = "The file \"%@\" is not supported and can’t 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.";
|
||||
|
|
|
|||
19
README.md
19
README.md
|
|
@ -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)
|
||||
|
||||
-  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)
|
||||
-  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)
|
||||
-  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)
|
||||
|
||||
-  Safe Mode startup.
|
||||
Tracking: [#27](https://github.com/h3pdesign/Neon-Vision-Editor/issues/27)
|
||||
-  incremental loading for huge files.
|
||||
-  incremental loading for huge files.
|
||||
Tracking: [#28](https://github.com/h3pdesign/Neon-Vision-Editor/issues/28)
|
||||
-  follow-up platform polish and release hardening.
|
||||
-  follow-up platform polish and release hardening.
|
||||
|
||||
### Later (v0.6.0)
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
Loading…
Reference in a new issue