feat(web): implement complete workspace with themes, tabs, sidebar, and mobile
Transform CalcText from a single-document calculator into a full workspace application with multi-document support, theming, and responsive mobile experience. - Theme system: 5 presets (Light, Dark, Matrix, Midnight, Warm) + accent colors - Document model with localStorage persistence and auto-save - Tab bar with keyboard shortcuts (Ctrl+N/W/Tab/1-9), rename, close - Sidebar with search, recent, favorites, folders, templates, drag-and-drop - 5 templates: Budget, Invoice, Unit Converter, Trip Planner, Loan Calculator - Status bar with cursor position, engine status, dedication to Igor Cassel - Results panel: type-specific colors, click-to-copy, error hints - Format toolbar: H, B, I, //, color labels with live preview toggle - Syntax highlighting using theme CSS variables - Error hover tooltips - Mobile: bottom results tray, sidebar drawer, touch targets, safe areas - Docker multi-stage build (Rust WASM + Vite + Nginx) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,13 +1,17 @@
|
||||
import AppKit
|
||||
import Combine
|
||||
import SwiftUI
|
||||
|
||||
/// The main two-column editor layout: text editor on the left, results on the right.
|
||||
/// Scrolling is synchronized between both columns.
|
||||
/// Includes a toolbar for justification toggles and alternating row striping.
|
||||
struct TwoColumnEditorView: View {
|
||||
@State private var text: String = ""
|
||||
@State private var text: String = "# CalcPad\n\n// Basic arithmetic\n2 + 3\n10 * 4.5\n100 / 7\n\n// Variables\nprice = 49.99\nquantity = 3\nsubtotal = price * quantity\n\n// Percentages\ntax = subtotal * 8%\ntotal = subtotal + tax\n\n// Functions\nsqrt(144)\n2 ^ 10\n"
|
||||
@State private var scrollOffset: CGFloat = 0
|
||||
@State private var results: [LineResult] = []
|
||||
@State private var evaluationTask: Task<Void, Never>?
|
||||
@State private var editorAlignment: NSTextAlignment = .left
|
||||
@State private var resultsAlignment: NSTextAlignment = .right
|
||||
|
||||
/// Uses the Rust FFI engine. Falls back to StubCalculationEngine if the Rust
|
||||
/// library is not linked (e.g., during UI-only development).
|
||||
@@ -18,35 +22,91 @@ struct TwoColumnEditorView: View {
|
||||
|
||||
/// Font that respects the user's accessibility / Dynamic Type settings.
|
||||
private var editorFont: NSFont {
|
||||
// Use the system's preferred monospaced font size, which scales with
|
||||
// Accessibility > Display > Text Size in System Settings (macOS 14+).
|
||||
let baseSize = NSFont.systemFontSize
|
||||
// Scale with accessibility settings via the body text style size
|
||||
let preferredSize = NSFont.preferredFont(forTextStyle: .body, options: [:]).pointSize
|
||||
// Use the larger of system default or accessibility-preferred size
|
||||
let size = max(baseSize, preferredSize)
|
||||
return NSFont.monospacedSystemFont(ofSize: size, weight: .regular)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HSplitView {
|
||||
// Left pane: Editor
|
||||
EditorTextView(
|
||||
text: $text,
|
||||
scrollOffset: $scrollOffset,
|
||||
font: editorFont
|
||||
)
|
||||
.frame(minWidth: 200)
|
||||
VStack(spacing: 0) {
|
||||
// Justification toolbar
|
||||
HStack(spacing: 0) {
|
||||
// Editor alignment buttons
|
||||
HStack(spacing: 4) {
|
||||
Button(action: { editorAlignment = .left }) {
|
||||
Image(systemName: "text.alignleft")
|
||||
}
|
||||
.buttonStyle(.borderless)
|
||||
.foregroundColor(editorAlignment == .left ? .accentColor : .secondary)
|
||||
.help("Align editor text left")
|
||||
|
||||
// Divider is automatic with HSplitView
|
||||
Button(action: { editorAlignment = .center }) {
|
||||
Image(systemName: "text.aligncenter")
|
||||
}
|
||||
.buttonStyle(.borderless)
|
||||
.foregroundColor(editorAlignment == .center ? .accentColor : .secondary)
|
||||
.help("Align editor text center")
|
||||
|
||||
// Right pane: Answer column
|
||||
AnswerColumnView(
|
||||
results: results,
|
||||
scrollOffset: $scrollOffset,
|
||||
font: editorFont
|
||||
)
|
||||
.frame(minWidth: 120, idealWidth: 200)
|
||||
Button(action: { editorAlignment = .right }) {
|
||||
Image(systemName: "text.alignright")
|
||||
}
|
||||
.buttonStyle(.borderless)
|
||||
.foregroundColor(editorAlignment == .right ? .accentColor : .secondary)
|
||||
.help("Align editor text right")
|
||||
}
|
||||
.padding(.horizontal, 8)
|
||||
|
||||
Spacer()
|
||||
|
||||
// Results alignment buttons
|
||||
HStack(spacing: 4) {
|
||||
Button(action: { resultsAlignment = .left }) {
|
||||
Image(systemName: "text.alignleft")
|
||||
}
|
||||
.buttonStyle(.borderless)
|
||||
.foregroundColor(resultsAlignment == .left ? .accentColor : .secondary)
|
||||
.help("Align results left")
|
||||
|
||||
Button(action: { resultsAlignment = .center }) {
|
||||
Image(systemName: "text.aligncenter")
|
||||
}
|
||||
.buttonStyle(.borderless)
|
||||
.foregroundColor(resultsAlignment == .center ? .accentColor : .secondary)
|
||||
.help("Align results center")
|
||||
|
||||
Button(action: { resultsAlignment = .right }) {
|
||||
Image(systemName: "text.alignright")
|
||||
}
|
||||
.buttonStyle(.borderless)
|
||||
.foregroundColor(resultsAlignment == .right ? .accentColor : .secondary)
|
||||
.help("Align results right")
|
||||
}
|
||||
.padding(.horizontal, 8)
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
|
||||
Divider()
|
||||
|
||||
HSplitView {
|
||||
// Left pane: Editor
|
||||
EditorTextView(
|
||||
text: $text,
|
||||
scrollOffset: $scrollOffset,
|
||||
font: editorFont,
|
||||
alignment: editorAlignment
|
||||
)
|
||||
.frame(minWidth: 200)
|
||||
|
||||
// Right pane: Answer column
|
||||
AnswerColumnView(
|
||||
results: results,
|
||||
scrollOffset: $scrollOffset,
|
||||
font: editorFont,
|
||||
alignment: resultsAlignment
|
||||
)
|
||||
.frame(minWidth: 120, idealWidth: 200)
|
||||
}
|
||||
}
|
||||
.onChange(of: text) { _, newValue in
|
||||
scheduleEvaluation(newValue)
|
||||
@@ -56,7 +116,6 @@ struct TwoColumnEditorView: View {
|
||||
}
|
||||
}
|
||||
|
||||
/// Debounce evaluation so rapid typing doesn't cause excessive recalculation.
|
||||
private func scheduleEvaluation(_ newText: String) {
|
||||
evaluationTask?.cancel()
|
||||
evaluationTask = Task { @MainActor in
|
||||
|
||||
Reference in New Issue
Block a user