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:
@@ -12,6 +12,8 @@ pub enum ResultType {
|
||||
DateTime,
|
||||
TimeDelta,
|
||||
Boolean,
|
||||
Empty,
|
||||
NonCalculable,
|
||||
Error,
|
||||
}
|
||||
|
||||
@@ -24,6 +26,8 @@ impl fmt::Display for ResultType {
|
||||
ResultType::DateTime => write!(f, "DateTime"),
|
||||
ResultType::TimeDelta => write!(f, "TimeDelta"),
|
||||
ResultType::Boolean => write!(f, "Boolean"),
|
||||
ResultType::Empty => write!(f, "Empty"),
|
||||
ResultType::NonCalculable => write!(f, "NonCalculable"),
|
||||
ResultType::Error => write!(f, "Error"),
|
||||
}
|
||||
}
|
||||
@@ -168,6 +172,36 @@ impl CalcResult {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn empty(span: Span) -> Self {
|
||||
CalcResult {
|
||||
value: CalcValue::Error {
|
||||
message: "empty input".to_string(),
|
||||
span: span.into(),
|
||||
},
|
||||
metadata: ResultMetadata {
|
||||
span: span.into(),
|
||||
result_type: ResultType::Empty,
|
||||
display: String::new(),
|
||||
raw_value: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn non_calculable(span: Span) -> Self {
|
||||
CalcResult {
|
||||
value: CalcValue::Error {
|
||||
message: "no expression found".to_string(),
|
||||
span: span.into(),
|
||||
},
|
||||
metadata: ResultMetadata {
|
||||
span: span.into(),
|
||||
result_type: ResultType::NonCalculable,
|
||||
display: String::new(),
|
||||
raw_value: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn error(message: &str, span: Span) -> Self {
|
||||
CalcResult {
|
||||
value: CalcValue::Error {
|
||||
@@ -186,13 +220,34 @@ impl CalcResult {
|
||||
pub fn result_type(&self) -> ResultType {
|
||||
self.metadata.result_type
|
||||
}
|
||||
|
||||
/// Returns true if this result is a computed value (not empty, non-calculable, or error).
|
||||
pub fn is_calculable(&self) -> bool {
|
||||
!matches!(
|
||||
self.metadata.result_type,
|
||||
ResultType::Empty | ResultType::NonCalculable | ResultType::Error
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn format_number(val: f64) -> String {
|
||||
if val == val.floor() && val.abs() < 1e15 {
|
||||
format!("{}", val as i64)
|
||||
} else {
|
||||
format!("{}", val)
|
||||
// Round to 10 significant figures to avoid f64 noise
|
||||
let abs = val.abs();
|
||||
let magnitude = if abs > 0.0 { abs.log10().floor() as i32 } else { 0 };
|
||||
let precision = (10 - magnitude - 1).max(0) as usize;
|
||||
let formatted = format!("{:.prec$}", val, prec = precision);
|
||||
// Strip trailing zeros after decimal point
|
||||
if formatted.contains('.') {
|
||||
formatted
|
||||
.trim_end_matches('0')
|
||||
.trim_end_matches('.')
|
||||
.to_string()
|
||||
} else {
|
||||
formatted
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user