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

@@ -11,6 +11,8 @@ import {
GutterMarker,
gutter,
EditorView,
hoverTooltip,
type Tooltip,
} from '@codemirror/view'
import { StateField, StateEffect, type Extension, RangeSet } from '@codemirror/state'
@@ -98,6 +100,48 @@ export const errorLinesField = StateField.define<Set<number>>({
},
})
// --- Error Messages (for tooltips) ---
export const errorMessagesField = StateField.define<Map<number, string>>({
create() {
return new Map()
},
update(msgs, tr) {
for (const effect of tr.effects) {
if (effect.is(setErrorsEffect)) {
const newMsgs = new Map<number, string>()
for (const error of effect.value) {
const lineNumber = tr.state.doc.lineAt(error.from).number
newMsgs.set(lineNumber, error.message)
}
return newMsgs
}
}
return msgs
},
})
// --- Error Tooltip (hover) ---
const errorTooltip = hoverTooltip((view, pos) => {
const line = view.state.doc.lineAt(pos)
const errorMessages = view.state.field(errorMessagesField)
const msg = errorMessages.get(line.number)
if (!msg) return null
return {
pos: line.from,
end: line.to,
above: false,
create() {
const dom = document.createElement('div')
dom.className = 'cm-error-tooltip'
dom.textContent = msg
return { dom }
},
} satisfies Tooltip
})
// --- Error Gutter ---
export const errorGutter = gutter({
@@ -118,15 +162,32 @@ export const errorGutter = gutter({
export const errorBaseTheme = EditorView.baseTheme({
'.cm-error-underline': {
textDecoration: 'underline wavy red',
textDecoration: 'underline wavy var(--error, red)',
textDecorationThickness: '1.5px',
},
'.cm-error-marker': {
color: '#e53e3e',
color: 'var(--error, #e53e3e)',
fontSize: '14px',
cursor: 'pointer',
},
'.cm-error-gutter': {
width: '20px',
},
'.cm-error-tooltip': {
backgroundColor: 'var(--bg-secondary, #f8f9fa)',
color: 'var(--error, #e53e3e)',
border: '1px solid var(--border, #e5e4e7)',
borderRadius: '4px',
padding: '4px 8px',
fontSize: '12px',
fontFamily: 'var(--sans, system-ui)',
maxWidth: '300px',
boxShadow: '0 2px 8px rgba(0,0,0,0.15)',
},
'.cm-tooltip': {
border: 'none',
backgroundColor: 'transparent',
},
})
/**
@@ -136,7 +197,9 @@ export function errorDisplayExtension(): Extension {
return [
errorDecorationsField,
errorLinesField,
errorMessagesField,
errorGutter,
errorTooltip,
errorBaseTheme,
]
}