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>
206 lines
4.8 KiB
TypeScript
206 lines
4.8 KiB
TypeScript
/**
|
|
* Inline error display for CalcPad.
|
|
* Adapted from epic/9-2-codemirror-6-editor.
|
|
*
|
|
* Shows red underline decorations and gutter markers for lines with errors.
|
|
*/
|
|
|
|
import {
|
|
Decoration,
|
|
type DecorationSet,
|
|
GutterMarker,
|
|
gutter,
|
|
EditorView,
|
|
hoverTooltip,
|
|
type Tooltip,
|
|
} from '@codemirror/view'
|
|
import { StateField, StateEffect, type Extension, RangeSet } from '@codemirror/state'
|
|
|
|
// --- State Effects ---
|
|
|
|
export interface LineError {
|
|
from: number // absolute start position in document
|
|
to: number // absolute end position in document
|
|
message: string
|
|
}
|
|
|
|
export const setErrorsEffect = StateEffect.define<LineError[]>()
|
|
|
|
// --- Error Gutter Marker ---
|
|
|
|
class ErrorGutterMarker extends GutterMarker {
|
|
override toDOM(): HTMLElement {
|
|
const span = document.createElement('span')
|
|
span.className = 'cm-error-marker'
|
|
span.textContent = '\u26A0' // Warning sign
|
|
return span
|
|
}
|
|
|
|
override eq(other: GutterMarker): boolean {
|
|
return other instanceof ErrorGutterMarker
|
|
}
|
|
}
|
|
|
|
const errorMarkerInstance = new ErrorGutterMarker()
|
|
|
|
// --- Error Underline Decorations (StateField) ---
|
|
|
|
const errorUnderlineMark = Decoration.mark({ class: 'cm-error-underline' })
|
|
|
|
export const errorDecorationsField = StateField.define<DecorationSet>({
|
|
create() {
|
|
return Decoration.none
|
|
},
|
|
|
|
update(decos, tr) {
|
|
for (const effect of tr.effects) {
|
|
if (effect.is(setErrorsEffect)) {
|
|
if (effect.value.length === 0) {
|
|
return Decoration.none
|
|
}
|
|
const ranges = effect.value
|
|
.filter((e) => e.from < e.to)
|
|
.sort((a, b) => a.from - b.from)
|
|
.map((e) => errorUnderlineMark.range(e.from, e.to))
|
|
return RangeSet.of(ranges)
|
|
}
|
|
}
|
|
|
|
if (tr.docChanged) {
|
|
return decos.map(tr.changes)
|
|
}
|
|
|
|
return decos
|
|
},
|
|
|
|
provide(field) {
|
|
return EditorView.decorations.from(field)
|
|
},
|
|
})
|
|
|
|
// --- Error Lines Set (for gutter) ---
|
|
|
|
export const errorLinesField = StateField.define<Set<number>>({
|
|
create() {
|
|
return new Set()
|
|
},
|
|
|
|
update(lines, tr) {
|
|
for (const effect of tr.effects) {
|
|
if (effect.is(setErrorsEffect)) {
|
|
const newLines = new Set<number>()
|
|
for (const error of effect.value) {
|
|
const lineNumber = tr.state.doc.lineAt(error.from).number
|
|
newLines.add(lineNumber)
|
|
}
|
|
return newLines
|
|
}
|
|
}
|
|
return lines
|
|
},
|
|
})
|
|
|
|
// --- 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({
|
|
class: 'cm-error-gutter',
|
|
lineMarker(view, line) {
|
|
const lineNumber = view.state.doc.lineAt(line.from).number
|
|
const errorLines = view.state.field(errorLinesField)
|
|
return errorLines.has(lineNumber) ? errorMarkerInstance : null
|
|
},
|
|
lineMarkerChange(update) {
|
|
return update.transactions.some((tr) =>
|
|
tr.effects.some((e) => e.is(setErrorsEffect)),
|
|
)
|
|
},
|
|
})
|
|
|
|
// --- Base Theme ---
|
|
|
|
export const errorBaseTheme = EditorView.baseTheme({
|
|
'.cm-error-underline': {
|
|
textDecoration: 'underline wavy var(--error, red)',
|
|
textDecorationThickness: '1.5px',
|
|
},
|
|
'.cm-error-marker': {
|
|
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',
|
|
},
|
|
})
|
|
|
|
/**
|
|
* Creates the error display extension bundle.
|
|
*/
|
|
export function errorDisplayExtension(): Extension {
|
|
return [
|
|
errorDecorationsField,
|
|
errorLinesField,
|
|
errorMessagesField,
|
|
errorGutter,
|
|
errorTooltip,
|
|
errorBaseTheme,
|
|
]
|
|
}
|