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:
2026-03-18 09:12:05 -04:00
parent 806e2f1ec6
commit 0d38bd3108
78 changed files with 8175 additions and 421 deletions

View File

@@ -3,12 +3,13 @@ import SwiftUI
/// Displays calculation results in a vertical column, one result per line,
/// aligned to match the corresponding editor lines.
/// Uses NSViewRepresentable wrapping NSScrollView + NSTextView for pixel-perfect
/// line height alignment with the editor.
/// Uses NSViewRepresentable wrapping NSScrollView + StripedTextView for
/// pixel-perfect line height alignment with the editor, including zebra striping.
struct AnswerColumnView: NSViewRepresentable {
let results: [LineResult]
@Binding var scrollOffset: CGFloat
var font: NSFont
var alignment: NSTextAlignment = .right
func makeCoordinator() -> Coordinator {
Coordinator()
@@ -24,23 +25,23 @@ struct AnswerColumnView: NSViewRepresentable {
scrollView.verticalScrollElasticity = .none
scrollView.horizontalScrollElasticity = .none
let textView = NSTextView()
let textView = StripedTextView()
textView.isEditable = false
textView.isSelectable = true
textView.isRichText = true
textView.usesFontPanel = false
textView.drawsBackground = false
textView.drawsBackground = true
textView.backgroundColor = .clear
// Match editor text container settings for alignment
textView.textContainer?.lineFragmentPadding = 4
textView.textContainerInset = NSSize(width: 8, height: 8)
// Disable line wrapping to match editor behavior
textView.isHorizontallyResizable = true
textView.textContainer?.widthTracksTextView = false
// Let the text container track the scroll view width
textView.isHorizontallyResizable = false
textView.textContainer?.widthTracksTextView = true
textView.textContainer?.containerSize = NSSize(
width: CGFloat.greatestFiniteMagnitude,
width: 0,
height: CGFloat.greatestFiniteMagnitude
)
textView.maxSize = NSSize(
@@ -73,14 +74,15 @@ struct AnswerColumnView: NSViewRepresentable {
let attributedString = NSMutableAttributedString()
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = .right
paragraphStyle.alignment = alignment
let resultColor = NSColor.secondaryLabelColor
let errorColor = NSColor.systemRed
for (index, lineResult) in results.enumerated() {
let displayText = lineResult.result ?? ""
let color = lineResult.isError ? errorColor : resultColor
// Only show successful results errors stay as underlines in the editor
let displayText = lineResult.isError ? "" : (lineResult.result ?? "")
let color = resultColor
let attributes: [NSAttributedString.Key: Any] = [
.font: font,