diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 62c0c34..ba53eac 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -331,6 +331,8 @@ dependencies = [ "env_logger", "futures-util", "log", + "serde", + "serde_json", "tokio", "tokio-tungstenite", ] @@ -512,6 +514,12 @@ version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + [[package]] name = "serde" version = "1.0.219" @@ -532,6 +540,18 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_json" +version = "1.0.143" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + [[package]] name = "sha1" version = "0.10.6" diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 260d543..718984d 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -7,5 +7,7 @@ edition = "2024" env_logger = "0.11.8" futures-util = "0.3.31" log = "0.4.27" +serde = { version = "1.0.219", features = ["derive"] } +serde_json = "1.0.143" tokio = { version = "1.47.1", features = ["rt-multi-thread", "macros", "net", "time"] } tokio-tungstenite = "0.27.0" diff --git a/backend/src/cell.rs b/backend/src/cell.rs index 399592b..8f2c9d8 100644 --- a/backend/src/cell.rs +++ b/backend/src/cell.rs @@ -1,5 +1,7 @@ use std::collections::HashSet; +use serde::{Deserialize, Serialize}; + use crate::evaluator::*; #[derive(Clone)] @@ -49,23 +51,24 @@ impl Cell { } } -#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] +#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)] pub struct CellRef { - pub row: i64, - pub col: i64, + pub row: usize, + pub col: usize, } impl CellRef { + // Zero indexed pub fn new(s: String) -> Result { let s = s.trim(); - let mut col: i64 = 0; + let mut col: usize = 0; let mut i = 0; // consume leading letters for the column for (idx, ch) in s.char_indices() { if ch.is_ascii_alphabetic() { let u = ch.to_ascii_uppercase() as u8; - let val = (u - b'A' + 1) as i64; // A->1 ... Z->26 + let val = (u - b'A' + 1) as usize; // A->1 ... Z->26 col = col * 26 + val; i = idx + ch.len_utf8(); } else { @@ -90,8 +93,11 @@ impl CellRef { )); } - if let Ok(row) = row_part.parse::() { - Ok(CellRef { row, col }) + if let Ok(row) = row_part.parse::() { + Ok(CellRef { + row: row - 1, + col: col - 1, + }) } else { Err(format!("Parse error: invalid row number.")) } diff --git a/backend/src/common.rs b/backend/src/common.rs new file mode 100644 index 0000000..e69de29 diff --git a/backend/src/evaluator.rs b/backend/src/evaluator.rs index aa95340..4afe1e3 100644 --- a/backend/src/evaluator.rs +++ b/backend/src/evaluator.rs @@ -1,7 +1,8 @@ -use crate::cell::{Cell, CellRef}; +use crate::cell::CellRef; +use crate::grid::Grid; use crate::parser::*; use crate::tokenizer::Literal; -use std::collections::{HashMap, HashSet}; +use std::collections::HashSet; use std::fmt; #[derive(Debug, PartialEq, Clone)] @@ -17,115 +18,58 @@ impl fmt::Display for Eval { } } -pub struct Evaluator { - cells: HashMap, +pub fn evaluate(str: String, grid: Option<&Grid>) -> Result<(Eval, HashSet), String> { + let (expr, deps) = parse(&str)?; + + match evaluate_expr(&expr, grid) { + Ok(it) => Ok((it, deps)), + Err(it) => Err(it), + } } -impl Evaluator { - pub fn new() -> Evaluator { - return Evaluator { - cells: HashMap::new(), - }; - } - - pub fn set_cell(&mut self, cell_ref: CellRef, raw_val: String) -> Result { - if self.cells.contains_key(&cell_ref) && self.cells[&cell_ref].raw() == raw_val { - return self.get_cell(cell_ref); - } - - let eval: Eval; - let deps: HashSet; - - if let Some(c) = raw_val.chars().nth(0) - && c == '=' - { - (eval, deps) = self.evaluate(raw_val[1..].to_owned())?; - // for dep in deps {} - } else { - match self.evaluate(raw_val.to_owned()) { - Ok(e) => { - (eval, deps) = e; - } - Err(_) => eval = Eval::Literal(Literal::String(raw_val.to_owned())), +fn evaluate_expr(expr: &Expr, grid: Option<&Grid>) -> Result { + let res = match expr { + Expr::Literal(lit) => Eval::Literal(lit.clone()), + Expr::CellRef(re) => { + if let Some(g) = grid { + g.get_cell(re.to_owned())? + } else { + return Err("Evaluation error: Found cell reference but no grid.".into()); } } + Expr::Infix { op, lhs, rhs } => { + let lval = evaluate_expr(lhs, grid)?; + let rval = evaluate_expr(rhs, grid)?; - self.cells - .insert(cell_ref, Cell::new(eval.clone(), raw_val)); - Ok(eval) - } - - // pub fn get_cell(&mut self, cell_ref: CellRef) -> Result<(String, Eval), String> { - pub fn get_cell(&mut self, cell_ref: CellRef) -> Result { - if !self.cells.contains_key(&cell_ref) { - return Err(format!("Cell at {:?} not found.", cell_ref)); - } - - let cell = &self.cells[&cell_ref]; - - // Ok((cell.raw(), cell.eval())) - Ok(cell.eval()) - } - - pub fn add_cell_dep(&mut self, cell_ref: CellRef, dep_ref: CellRef) -> Result<(), String> { - if !self.cells.contains_key(&cell_ref) { - return Err(format!("Cell at {:?} not found.", cell_ref)); - } - - if let Some(cell) = self.cells.get_mut(&cell_ref) { - cell.add_i_dep(dep_ref); - } - - Ok(()) - } - - pub fn evaluate(&mut self, str: String) -> Result<(Eval, HashSet), String> { - let (expr, deps) = parse(&str)?; - - match self.evaluate_expr(&expr) { - Ok(it) => Ok((it, deps)), - Err(it) => Err(it), - } - } - - fn evaluate_expr(&mut self, expr: &Expr) -> Result { - let res = match expr { - Expr::Literal(lit) => Eval::Literal(lit.clone()), - Expr::CellRef(re) => self.get_cell(re.to_owned())?, - Expr::Infix { op, lhs, rhs } => { - let lval = self.evaluate_expr(lhs)?; - let rval = self.evaluate_expr(rhs)?; - - match op { - InfixOp::ADD => eval_add(&lval, &rval)?, - InfixOp::SUB => eval_sub(&lval, &rval)?, - InfixOp::MUL => eval_mul(&lval, &rval)?, - InfixOp::DIV => eval_div(&lval, &rval)?, - _ => return Err(format!("Evaluation error: Unsupported operator {:?}", op)), - } + match op { + InfixOp::ADD => eval_add(&lval, &rval)?, + InfixOp::SUB => eval_sub(&lval, &rval)?, + InfixOp::MUL => eval_mul(&lval, &rval)?, + InfixOp::DIV => eval_div(&lval, &rval)?, + _ => return Err(format!("Evaluation error: Unsupported operator {:?}", op)), } - Expr::Prefix { op, expr } => { - let val = self.evaluate_expr(expr)?; + } + Expr::Prefix { op, expr } => { + let val = evaluate_expr(expr, grid)?; - match op { - PrefixOp::POS => eval_pos(&val)?, - PrefixOp::NEG => eval_neg(&val)?, - PrefixOp::NOT => eval_not(&val)?, - _ => return Err(format!("Evaluation error: Unsupported operator {:?}", op)), - } + match op { + PrefixOp::POS => eval_pos(&val)?, + PrefixOp::NEG => eval_neg(&val)?, + PrefixOp::NOT => eval_not(&val)?, + // _ => return Err(format!("Evaluation error: Unsupported operator {:?}", op)), } - Expr::Group(g) => self.evaluate_expr(g)?, - it => return Err(format!("Evaluation error: Unsupported expression {:?}", it)), - }; + } + Expr::Group(g) => evaluate_expr(g, grid)?, + it => return Err(format!("Evaluation error: Unsupported expression {:?}", it)), + }; - Ok(res) - } + Ok(res) } fn eval_add(lval: &Eval, rval: &Eval) -> Result { match (lval, rval) { (Eval::Literal(a), Eval::Literal(b)) => { - if let Some(res) = eval_numeric_infix(a, b, |x, y| x + y, |x, y| x + y) { + if let Some(res) = eval_numeric_infix(a, b, |x, y| x + y) { return Ok(Eval::Literal(res)); } @@ -144,7 +88,7 @@ fn eval_add(lval: &Eval, rval: &Eval) -> Result { fn eval_sub(lval: &Eval, rval: &Eval) -> Result { match (lval, rval) { (Eval::Literal(a), Eval::Literal(b)) => { - if let Some(res) = eval_numeric_infix(a, b, |x, y| x - y, |x, y| x - y) { + if let Some(res) = eval_numeric_infix(a, b, |x, y| x - y) { return Ok(Eval::Literal(res)); } @@ -155,7 +99,7 @@ fn eval_sub(lval: &Eval, rval: &Eval) -> Result { fn eval_mul(lval: &Eval, rval: &Eval) -> Result { match (lval, rval) { (Eval::Literal(a), Eval::Literal(b)) => { - if let Some(res) = eval_numeric_infix(a, b, |x, y| x * y, |x, y| x * y) { + if let Some(res) = eval_numeric_infix(a, b, |x, y| x * y) { return Ok(Eval::Literal(res)); } @@ -166,15 +110,15 @@ fn eval_mul(lval: &Eval, rval: &Eval) -> Result { fn eval_div(lval: &Eval, rval: &Eval) -> Result { match (lval, rval) { (Eval::Literal(a), Eval::Literal(b)) => { - if let (Literal::Integer(_), Literal::Integer(y)) = (a, b) { - if *y == 0 { + if let (Literal::Number(_), Literal::Number(y)) = (a, b) { + if *y == 0f64 { return Err( "Evaluation error: integers attempted to divide by zero.".to_string() ); } } - if let Some(res) = eval_numeric_infix(a, b, |x, y| x / y, |x, y| x / y) { + if let Some(res) = eval_numeric_infix(a, b, |x, y| x / y) { return Ok(Eval::Literal(res)); } @@ -183,41 +127,23 @@ fn eval_div(lval: &Eval, rval: &Eval) -> Result { } } -fn eval_numeric_infix( - lhs: &Literal, - rhs: &Literal, - int_op: FInt, - double_op: FDouble, -) -> Option -where - FInt: Fn(i64, i64) -> i64, - FDouble: Fn(f64, f64) -> f64, -{ +fn eval_numeric_infix(lhs: &Literal, rhs: &Literal, op: fn(f64, f64) -> f64) -> Option { match (lhs, rhs) { - (Literal::Integer(a), Literal::Integer(b)) => Some(Literal::Integer(int_op(*a, *b))), - (Literal::Double(a), Literal::Double(b)) => Some(Literal::Double(double_op(*a, *b))), - (Literal::Integer(a), Literal::Double(b)) => { - Some(Literal::Double(double_op(*a as f64, *b))) - } - (Literal::Double(a), Literal::Integer(b)) => { - Some(Literal::Double(double_op(*a, *b as f64))) - } + (Literal::Number(a), Literal::Number(b)) => Some(Literal::Number(op(*a, *b))), _ => None, } } fn eval_pos(val: &Eval) -> Result { match val { - Eval::Literal(Literal::Integer(it)) => Ok(Eval::Literal(Literal::Integer(*it))), - Eval::Literal(Literal::Double(it)) => Ok(Eval::Literal(Literal::Double(*it))), + Eval::Literal(Literal::Number(it)) => Ok(Eval::Literal(Literal::Number(*it))), _ => Err("Evaluation error: expected numeric type for POS function.".to_string()), } } fn eval_neg(val: &Eval) -> Result { match val { - Eval::Literal(Literal::Integer(it)) => Ok(Eval::Literal(Literal::Integer(-it))), - Eval::Literal(Literal::Double(it)) => Ok(Eval::Literal(Literal::Double(-it))), + Eval::Literal(Literal::Number(it)) => Ok(Eval::Literal(Literal::Number(-it))), _ => Err("Evaluation error: expected numeric type for NEG function.".to_string()), } } diff --git a/backend/src/grid.rs b/backend/src/grid.rs new file mode 100644 index 0000000..0ae5f62 --- /dev/null +++ b/backend/src/grid.rs @@ -0,0 +1,72 @@ +use std::collections::{HashMap, HashSet}; + +use crate::{ + cell::{Cell, CellRef}, + evaluator::{Eval, evaluate}, + tokenizer::Literal, +}; + +pub struct Grid { + cells: HashMap, +} + +impl Grid { + pub fn new() -> Grid { + return Grid { + cells: HashMap::new(), + }; + } +} + +impl Grid { + pub fn set_cell(&mut self, cell_ref: CellRef, raw_val: String) -> Result { + if self.cells.contains_key(&cell_ref) && self.cells[&cell_ref].raw() == raw_val { + return self.get_cell(cell_ref); + } + + let eval: Eval; + let deps: HashSet; + + if let Some(c) = raw_val.chars().nth(0) + && c == '=' + { + (eval, deps) = evaluate(raw_val[1..].to_owned(), Some(&self))?; + // for dep in deps {} + } else { + match evaluate(raw_val.to_owned(), Some(&self)) { + Ok(e) => { + (eval, deps) = e; + } + Err(_) => eval = Eval::Literal(Literal::String(raw_val.to_owned())), + } + } + + self.cells + .insert(cell_ref, Cell::new(eval.clone(), raw_val)); + Ok(eval) + } + + // pub fn get_cell(&mut self, cell_ref: CellRef) -> Result<(String, Eval), String> { + pub fn get_cell(&self, cell_ref: CellRef) -> Result { + if !self.cells.contains_key(&cell_ref) { + return Err(format!("Cell at {:?} not found.", cell_ref)); + } + + let cell = &self.cells[&cell_ref]; + + // Ok((cell.raw(), cell.eval())) + Ok(cell.eval()) + } + + pub fn add_cell_dep(&mut self, cell_ref: CellRef, dep_ref: CellRef) -> Result<(), String> { + if !self.cells.contains_key(&cell_ref) { + return Err(format!("Cell at {:?} not found.", cell_ref)); + } + + if let Some(cell) = self.cells.get_mut(&cell_ref) { + cell.add_i_dep(dep_ref); + } + + Ok(()) + } +} diff --git a/backend/src/main.rs b/backend/src/main.rs index 43323f3..db7f500 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -1,28 +1,24 @@ mod cell; mod evaluator; +mod grid; +mod messages; mod parser; mod tokenizer; -use futures_util::{SinkExt, StreamExt, TryStreamExt, future}; +use futures_util::{SinkExt, StreamExt, TryStreamExt}; use log::info; use std::{env, io::Error}; use tokio::net::{TcpListener, TcpStream}; -use crate::{cell::CellRef, evaluator::Evaluator}; +use crate::{ + evaluator::Eval, + grid::Grid, + messages::{LeadMsg, MsgType}, +}; #[tokio::main] async fn main() -> Result<(), Error> { env_logger::init(); - // let mut input = String::new(); - // io::stdin().read_line(&mut input).expect("Expected input."); - - // let mut ast = parser::parse(&input).unwrap(); - // println!("{}", ast.pretty()); - let mut evaluator = Evaluator::new(); - // // println!("{}", evaluator.evaluate(input).unwrap()); - // let a1 = CellRef { row: 1, col: 2 }; - // evaluator.set_cell(a1, input).unwrap(); - // println!("{:?}", evaluator.get_cell(a1).unwrap()); let addr = env::args() .nth(1) @@ -38,78 +34,8 @@ async fn main() -> Result<(), Error> { } Ok(()) - - // println!("CMDS : set , get "); - // loop { - // let mut input = String::new(); - // io::stdin().read_line(&mut input).expect("Expected input."); - // - // let cmds = ["set", "get"]; - // let cmd = &input[0..3]; - // if !cmds.iter().any(|c| c == &cmd) { - // println!("{} is an invalid command!", cmd); - // println!("CMDS : set , get "); - // continue; - // } - // - // let rest = &input[4..]; - // let mut parts = rest.splitn(2, char::is_whitespace); - // - // let raw_ref = parts.next().unwrap_or("").trim(); // cell reference - // let raw_str = parts.next().unwrap_or("").trim(); // rest of the string (value) - // // println!("{} {}", raw_ref, raw_str); - // - // if let Ok(cell_ref) = CellRef::new(raw_ref.to_owned()) { - // match cmd { - // "set" => match evaluator.set_cell(cell_ref, raw_str.to_owned()) { - // Ok(_) => println!("Successfully set cell {} to {}.", raw_ref, raw_str), - // Err(e) => println!("{}", e), - // }, - // "get" => match evaluator.get_cell(cell_ref) { - // Ok(res) => println!("{:?}", res), - // Err(e) => println!("{}", e), - // }, - // _ => { - // panic!("Impossible."); - // } - // } - // } else { - // println!("{} is an invalid cell reference!", raw_ref); - // continue; - // } - // } } -// async fn accept_connection(stream: TcpStream) { -// let addr = stream -// .peer_addr() -// .expect("connected streams should have a peer address"); -// info!("Peer address: {}", addr); -// -// let ws_stream = tokio_tungstenite::accept_async(stream) -// .await -// .expect("Error during the websocket handshake occurred"); -// -// info!("New WebSocket connection: {}", addr); -// -// let (mut write, mut read) = ws_stream.split(); -// -// // We should not forward messages other than text or binary. -// while let Some(msg) = read.try_next().await.unwrap_or(None) { -// if msg.is_text() || msg.is_binary() { -// if let Err(e) = write -// .send(format!("This is a message {}!", msg.to_text().unwrap_or("")).into()) -// .await -// { -// eprintln!("send error: {}", e); -// break; -// } -// } -// } -// -// info!("Disconnected from {}", addr); -// } - async fn accept_connection(stream: TcpStream) { let addr = stream .peer_addr() @@ -125,56 +51,51 @@ async fn accept_connection(stream: TcpStream) { let (mut write, mut read) = ws_stream.split(); // Each connection gets its own evaluator - let mut evaluator = Evaluator::new(); + let mut grid = Grid::new(); while let Some(msg) = read.try_next().await.unwrap_or(None) { if msg.is_text() { - let input = msg.to_text().unwrap_or("").trim().to_string(); + let input = msg.to_text().unwrap(); - let cmds = ["set", "get"]; - let cmd = &input[0..3.min(input.len())]; // avoid panic on short strings + if let Ok(req) = serde_json::from_str::(&input) { + match req.msg_type { + MsgType::Set => { + let Some(cell_ref) = req.cell else { continue }; + let Some(raw) = req.raw else { continue }; - if !cmds.iter().any(|c| c == &cmd) { - let _ = write - .send(format!("ERR invalid command: {}", input).into()) - .await; - continue; - } - - let rest = input[4..].trim(); - let mut parts = rest.splitn(2, char::is_whitespace); - - let raw_ref = parts.next().unwrap_or("").trim(); // cell reference - let raw_str = parts.next().unwrap_or("").trim(); // rest (value) - - if let Ok(cell_ref) = CellRef::new(raw_ref.to_owned()) { - match cmd { - "set" => match evaluator.set_cell(cell_ref.clone(), raw_str.to_owned()) { - Ok(eval) => { - let _ = write.send(format!("{} {}", raw_ref, eval).into()).await; + match grid.set_cell(cell_ref.clone(), raw.to_owned()) { + Ok(eval) => match eval { + Eval::Literal(lit) => { + let res = LeadMsg { + msg_type: MsgType::Set, + cell: Some(cell_ref), + raw: Some(raw.to_string()), + eval: Some(lit), + }; + let _ = write + .send(serde_json::to_string(&res).unwrap().into()) + .await; + } + }, + Err(e) => { + let res = LeadMsg { + msg_type: MsgType::Error, + cell: Some(cell_ref), + raw: Some(e.to_string()), + eval: None, + }; + let _ = write + .send(serde_json::to_string(&res).unwrap().into()) + .await; + } } - Err(e) => { - let _ = write.send(format!("ERR {}", e).into()).await; - } - }, - "get" => match evaluator.get_cell(cell_ref.clone()) { - Ok(res) => { - let _ = write - .send(format!("{} {}", raw_ref, res.to_string()).into()) - .await; - } - Err(e) => { - let _ = write.send(format!("ERR {}", e).into()).await; - } - }, + } _ => { - let _ = write.send("ERR impossible".into()).await; + continue; // handle other cases } } } else { - let _ = write - .send(format!("ERR invalid cell reference: {}", raw_ref).into()) - .await; + continue; } } } diff --git a/backend/src/messages.rs b/backend/src/messages.rs new file mode 100644 index 0000000..e9791cc --- /dev/null +++ b/backend/src/messages.rs @@ -0,0 +1,19 @@ +use serde::{Deserialize, Serialize}; + +use crate::{cell::CellRef, tokenizer::Literal}; + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum MsgType { + Set, + Get, + Error, +} + +#[derive(Serialize, Deserialize)] +pub struct LeadMsg { + pub msg_type: MsgType, + pub cell: Option, + pub raw: Option, + pub eval: Option, +} diff --git a/backend/src/parser.rs b/backend/src/parser.rs index bbb9e16..4c31cd6 100644 --- a/backend/src/parser.rs +++ b/backend/src/parser.rs @@ -169,11 +169,9 @@ pub fn _parse( ) -> Result { let mut lhs = match input.next() { Token::Literal(it) => Expr::Literal(it), - Token::Identifier(id) if id == "true" => Expr::Literal(Literal::Boolean(true)), - Token::Identifier(id) if id == "false" => Expr::Literal(Literal::Boolean(false)), - Token::Paren('(') => { + Token::OpenParen => { let lhs = _parse(input, 0, deps)?; - if input.next() != Token::Paren(')') { + if input.next() != Token::CloseParen { return Err(format!("Parse error: expected closing paren.")); } Expr::Group(Box::new(lhs)) @@ -194,14 +192,14 @@ pub fn _parse( } } Token::Identifier(id) => match input.peek() { - Token::Paren('(') => { + Token::OpenParen => { input.next(); let mut args: Vec = Vec::new(); loop { let nxt = input.peek(); - if nxt == Token::Paren(')') { + if nxt == Token::CloseParen { input.next(); break; } else if nxt != Token::Comma && args.len() != 0 { @@ -286,6 +284,5 @@ pub fn _parse( } } - Ok(lhs) } diff --git a/backend/src/tokenizer.rs b/backend/src/tokenizer.rs index 547aa2a..794b698 100644 --- a/backend/src/tokenizer.rs +++ b/backend/src/tokenizer.rs @@ -1,7 +1,9 @@ -#[derive(Debug, Clone, PartialEq)] +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(tag = "type", content = "value")] pub enum Literal { - Integer(i64), - Double(f64), + Number(f64), Boolean(bool), String(String), } @@ -11,7 +13,8 @@ pub enum Token { Identifier(String), // Could be a function Literal(Literal), Operator(char), - Paren(char), + OpenParen, + CloseParen, Comma, Eof, } @@ -39,29 +42,38 @@ impl Tokenizer { break; } } - tokens.push(Token::Identifier(ident)); + let res = match ident.as_str() { + "true" => Token::Literal(Literal::Boolean(true)), + "false" => Token::Literal(Literal::Boolean(false)), + it => Token::Identifier(it.into()), + }; + + tokens.push(res); } else if c.is_ascii_digit() { // parse number let mut number = String::new(); let mut is_decimal = false; + let mut is_exp = false; while let Some(&ch) = chars.peek() { if ch.is_ascii_digit() { number.push(ch); chars.next(); - } else if ch == '.' && !is_decimal { + } else if ch == '.' && !is_decimal && !is_exp { is_decimal = true; number.push(ch); chars.next(); + } else if ch == 'e' && !is_decimal && !is_exp { + is_exp = true; + number.push(ch); + chars.next(); } else { break; } } - if is_decimal { - tokens.push(Token::Literal(Literal::Double(number.parse().unwrap()))) - } else { - tokens.push(Token::Literal(Literal::Integer(number.parse().unwrap()))) - }; + + // TODO: REMOVE UNWRAP + tokens.push(Token::Literal(Literal::Number(number.parse().unwrap()))); } else if c == '"' || c == '\'' { // parse string literal let mut string = String::new(); @@ -89,7 +101,11 @@ impl Tokenizer { tokens.push(Token::Operator(c)); chars.next(); } else if "()".contains(c) { - tokens.push(Token::Paren(c)); + if c == '(' { + tokens.push(Token::OpenParen); + } else { + tokens.push(Token::CloseParen); + } chars.next(); } else if c == ',' { tokens.push(Token::Comma); @@ -116,20 +132,140 @@ mod tests { use super::*; #[test] - fn test_tokenizer() { - let raw = "hello hello 1.23 this 5 (1+2)"; - let expected: Vec = vec![ - Token::Identifier("hello".to_string()), - Token::Identifier("hello".to_string()), - Token::Literal(Literal::Double(1.23)), - Token::Identifier("this".to_string()), - Token::Literal(Literal::Integer(5)), - Token::Paren('('), - Token::Literal(Literal::Integer(1)), + fn test_single_token() { + assert_eq!( + Tokenizer::new("1").unwrap().tokens, + Vec::from([Token::Literal(Literal::Number(1.0))]) + ); + assert_eq!( + Tokenizer::new("2.0").unwrap().tokens, + Vec::from([Token::Literal(Literal::Number(2.0))]) + ); + assert_eq!( + Tokenizer::new("\"hello\"").unwrap().tokens, + Vec::from([Token::Literal(Literal::String("hello".into()))]) + ); + assert_eq!( + Tokenizer::new("\'hello\'").unwrap().tokens, + Vec::from([Token::Literal(Literal::String("hello".into()))]) + ); + assert_eq!( + Tokenizer::new("hello").unwrap().tokens, + Vec::from([Token::Identifier("hello".into())]) + ); + assert_eq!( + Tokenizer::new("+").unwrap().tokens, + Vec::from([Token::Operator('+')]) + ); + assert_eq!( + Tokenizer::new(",").unwrap().tokens, + Vec::from([Token::Comma]) + ); + assert_eq!( + Tokenizer::new(")").unwrap().tokens, + Vec::from([Token::CloseParen]) + ); + assert_eq!( + Tokenizer::new("(").unwrap().tokens, + Vec::from([Token::OpenParen]) + ); + } + + #[test] + fn test_token_punctuation() { + let mut exp = Vec::from([ + Token::Comma, + Token::CloseParen, + Token::Comma, + Token::OpenParen, + ]); + exp.reverse(); + + assert_eq!(Tokenizer::new(", ) , (").unwrap().tokens, exp); + } + + #[test] + fn test_token_operators() { + let mut exp = Vec::from([ Token::Operator('+'), - Token::Literal(Literal::Integer(2)), - Token::Paren(')'), + Token::Operator('-'), + Token::Operator('*'), + Token::Operator('/'), + Token::Operator('^'), + Token::Operator('!'), + Token::Operator('%'), + Token::Operator('&'), + Token::Operator('|'), + ]); + exp.reverse(); + + assert_eq!(Tokenizer::new("+-*/^!%&|").unwrap().tokens, exp); + } + + #[test] + fn test_token_string() { + let raw = "\"hello\" \'world\'"; + let mut expected: Vec = vec![ + Token::Literal(Literal::String("hello".into())), + Token::Literal(Literal::String("world".into())), ]; + expected.reverse(); + let t = Tokenizer::new(&raw).unwrap(); + assert_eq!(t.tokens, expected); + } + + #[test] + fn test_token_number() { + let raw = "123 4.56"; + let mut expected: Vec = vec![ + Token::Literal(Literal::Number(123.0)), + Token::Literal(Literal::Number(4.56)), + ]; + expected.reverse(); + let t = Tokenizer::new(&raw).unwrap(); + assert_eq!(t.tokens, expected); + } + + #[test] + fn test_token_boolean() { + let raw = "false true"; + let mut expected: Vec = vec![ + Token::Literal(Literal::Boolean(false)), + Token::Literal(Literal::Boolean(true)), + ]; + expected.reverse(); + let t = Tokenizer::new(&raw).unwrap(); + assert_eq!(t.tokens, expected); + } + + #[test] + fn test_token_identifier() { + let raw = "hello test"; + let mut expected: Vec = vec![ + Token::Identifier("hello".to_string()), + Token::Identifier("test".to_string()), + ]; + expected.reverse(); + let t = Tokenizer::new(&raw).unwrap(); + assert_eq!(t.tokens, expected); + } + + #[test] + fn test_token_mix() { + let raw = "hello test 1.23 this 5 (1+2)"; + let mut expected: Vec = vec![ + Token::Identifier("hello".to_string()), + Token::Identifier("test".to_string()), + Token::Literal(Literal::Number(1.23)), + Token::Identifier("this".to_string()), + Token::Literal(Literal::Number(5.0)), + Token::OpenParen, + Token::Literal(Literal::Number(1.0)), + Token::Operator('+'), + Token::Literal(Literal::Number(2.0)), + Token::CloseParen, + ]; + expected.reverse(); let t = Tokenizer::new(&raw).unwrap(); assert_eq!(t.tokens, expected); } diff --git a/frontend/src/app.css b/frontend/src/app.css index 05decf4..f7e7bb8 100644 --- a/frontend/src/app.css +++ b/frontend/src/app.css @@ -111,6 +111,28 @@ --color-sidebar-ring: var(--sidebar-ring); } +:root { + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); +} + +.dark { + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.439 0 0); +} + @layer base { * { @apply border-border outline-ring/50; diff --git a/frontend/src/lib/components/grid/cell-header.svelte b/frontend/src/lib/components/grid/cell-header.svelte index e654e08..87ed670 100644 --- a/frontend/src/lib/components/grid/cell-header.svelte +++ b/frontend/src/lib/components/grid/cell-header.svelte @@ -18,7 +18,7 @@ val: string; active: boolean; resizeable?: boolean; - direction?: 'col' | 'row'; + direction?: 'col' | 'row' | 'blank'; } = $props(); // --- Drag Logic --- @@ -62,7 +62,12 @@
{val} @@ -88,20 +93,34 @@ background-color: var(--color-background); } + .placeholder.blank { + border: 1px solid var(--input); + } + + .placeholder.blank, + .placeholder.row { + border-left: none; + } + + .placeholder.blank, + .placeholder.col { + border-top: none; + } + .active { - border: 1px solid var(--color-primary); background-color: color-mix(in oklab, var(--color-primary) 80%, var(--color-background) 80%); + font-weight: bold; + /* border: 1px solid var(--color-primary); */ } /* --- Resizer Styles --- */ .resizer { position: absolute; - /* Make it easier to grab */ - z-index: 10; /* Subtle visual cue, becomes more visible on hover */ background-color: var(--color-primary); opacity: 0; transition: opacity 0.1s ease-in-out; + z-index: 60; } /* Style for vertical (column) resizing */ @@ -113,7 +132,6 @@ height: 100%; } - /* Style for horizontal (row) resizing */ .resizer-row { cursor: row-resize; bottom: -5px; diff --git a/frontend/src/lib/components/grid/cell.svelte b/frontend/src/lib/components/grid/cell.svelte index e835991..36b88a1 100644 --- a/frontend/src/lib/components/grid/cell.svelte +++ b/frontend/src/lib/components/grid/cell.svelte @@ -3,6 +3,7 @@ import clsx from 'clsx'; let { + cla = '', width = '80px', height = '30px', raw_val = $bindable(''), @@ -13,10 +14,11 @@ active = false, editing = false }: { + cla?: string; width?: string; height?: string; raw_val?: string; - val?: number | string | undefined; + val?: LiteralValue | undefined; onmousedown?: (e: MouseEvent) => void; startediting?: () => void; stopediting?: () => void; @@ -51,9 +53,8 @@
{ raw_val = (e.target as HTMLInputElement).value; stopediting(); @@ -66,7 +67,7 @@ {onmousedown} style:width style:height - class={clsx('placeholder bg-background p-1 dark:bg-input/30', { active, 'z-20': active })} + class={clsx('placeholder bg-background p-1', { active }, cla)} > {#if raw_val !== '' || val !== ''} @@ -89,6 +90,7 @@ } .active { + z-index: 20; border: 1px solid var(--color-primary); outline: 1px solid var(--color-primary); } diff --git a/frontend/src/lib/components/grid/grid.svelte b/frontend/src/lib/components/grid/grid.svelte index 5b5c98d..425ae6d 100644 --- a/frontend/src/lib/components/grid/grid.svelte +++ b/frontend/src/lib/components/grid/grid.svelte @@ -3,84 +3,57 @@ import Cell from '$lib/components/grid/cell.svelte'; import { onDestroy, onMount } from 'svelte'; import CellHeader from './cell-header.svelte'; - import { - refFromStr, - splitErrorString, - colToStr, - refFromPos, - type CellData, - type CellValue - } from './utils'; + import { colToStr, refToStr, type CellT } from './utils'; + import clsx from 'clsx'; let { - socket + socket, + class: className = '' }: { + class?: string; socket: WebSocket; } = $props(); socket.onmessage = (msg: MessageEvent) => { - const input = msg.data.toString().trim(); - - if (input.startsWith('ERR')) { - let split = splitErrorString(input); - toast.error(split[0], { - description: split[1] - }); + let res: LeadMsg; + try { + res = JSON.parse(msg.data); + console.log(res); + } catch (err) { + console.error('Failed to parse LeadMsg:', err); return; } - let strRef: string | undefined; - let evalStr: string | undefined; - - // Case 1: "Cell D4 = Integer(4)" - let match = input.match(/^Cell\s+([A-Z]+\d+)\s*=\s*(.+)$/); - if (match) { - [, strRef, evalStr] = match; - } - - // Case 2: "D6 String("hello")" or "E9 Double(4.0)" - if (!match) { - match = input.match(/^([A-Z]+\d+)\s+(.+)$/); - if (match) { - [, strRef, evalStr] = match; + switch (res.msg_type) { + case 'error': { + toast.error('Error', { + description: res.raw + }); + break; + } + case 'set': { + if (res.cell === undefined) { + console.error('Expected cell ref for SET response from server.'); + return; + } else if (res.eval === undefined) { + console.error('Expected cell value for SET response from server.'); + return; + } + setCellVal(res.cell.row, res.cell.col, res.eval.value); + break; } - } - - if (!strRef || !evalStr) { - console.warn('Unrecognized message:', input); - return; - } - - console.log(`Cell: ${strRef}`); - console.log(`Eval: ${evalStr}`); - - let { row, col } = refFromStr(strRef); - - // Parse eval types - if (evalStr.startsWith('Integer(')) { - const num = parseInt(evalStr.match(/^Integer\(([-\d]+)\)$/)?.[1] ?? 'NaN', 10); - console.log(`Parsed integer:`, num); - setCellVal(row, col, num); - } else if (evalStr.startsWith('Double(')) { - const num = parseFloat(evalStr.match(/^Double\(([-\d.]+)\)$/)?.[1] ?? 'NaN'); - console.log(`Parsed double:`, num); - setCellVal(row, col, num); - } else if (evalStr.startsWith('String(')) { - const str = evalStr.match(/^String\("(.+)"\)$/)?.[1]; - console.log(`Parsed string:`, str); - setCellVal(row, col, str); } }; - let rows = 100; - let cols = 60; + let rows = 50; + let cols = 40; let default_row_height = '30px'; let default_col_width = '80px'; // Only store touched cells - let grid_vals: Record = $state({}); + let grid_vals: Record = $state({}); let row_heights: Record = $state({}); let col_widths: Record = $state({}); @@ -136,7 +109,7 @@ } }; const getCellVal = (i: number, j: number) => getCell(i, j)?.val ?? undefined; - const setCellVal = (i: number, j: number, val: CellValue) => { + const setCellVal = (i: number, j: number, val: LiteralValue) => { if (grid_vals[key(i, j)] === undefined) { console.warn('Cell raw value was undefined but recieved eval.'); } else { @@ -144,20 +117,22 @@ } }; - const setCell = (i: number, j: number, v: string | undefined) => { + const setCell = (row: number, col: number, v: string | undefined) => { // ignore “no value” so we don’t create keys on mount - if (v == null || v === '') delete grid_vals[key(i, j)]; + if (v == null || v === '') delete grid_vals[key(row, col)]; else { - setCellRaw(i, j, v); - console.log(i, j); - socket.send(`set ${refFromPos(i, j).str} ${v}`); + setCellRaw(row, col, v); + + let msg: LeadMsg = { + msg_type: 'set', + cell: { row, col }, + raw: v + }; + + socket.send(JSON.stringify(msg)); } }; - // $effect(() => { - // $inspect(grid_vals); - // }); - function handleCellInteraction(i: number, j: number, e: MouseEvent) { if (editing_cell) { // Get the actual input element that's being edited @@ -170,7 +145,7 @@ e.preventDefault(); // --- This is the same reference-inserting logic as before --- - const ref = refFromPos(i, j).str; + const ref = refToStr(i, j); if (el) { const { selectionStart, selectionEnd } = el; const before = el.value.slice(0, selectionStart ?? 0); @@ -202,15 +177,18 @@ }); -
-
-
+
+
+
@@ -226,8 +204,8 @@ {/each}
{#each Array(rows) as _, i} -
-
+
+
+ import CalendarIcon from '@lucide/svelte/icons/calendar'; + import HouseIcon from '@lucide/svelte/icons/house'; + import InboxIcon from '@lucide/svelte/icons/inbox'; + import SearchIcon from '@lucide/svelte/icons/search'; + import SettingsIcon from '@lucide/svelte/icons/settings'; + import SunIcon from '@lucide/svelte/icons/sun'; + import MoonIcon from '@lucide/svelte/icons/moon'; + import * as Sidebar from '$lib/components/ui/sidebar/index.js'; + import Button from '../button/button.svelte'; + import { toggleMode } from 'mode-watcher'; + + // Menu items. + const items = [ + { + title: 'Home', + url: '#', + icon: HouseIcon + }, + { + title: 'Inbox', + url: '#', + icon: InboxIcon + }, + { + title: 'Calendar', + url: '#', + icon: CalendarIcon + }, + { + title: 'Search', + url: '#', + icon: SearchIcon + }, + { + title: 'Settings', + url: '#', + icon: SettingsIcon + } + ]; + + + + + + +
+
Lead
+ +
+
+ + + {#each items as item (item.title)} + + + {#snippet child({ props })} + + + {item.title} + + {/snippet} + + + {/each} + + +
+
+ + + + +
diff --git a/frontend/src/lib/components/ui/separator/index.ts b/frontend/src/lib/components/ui/separator/index.ts new file mode 100644 index 0000000..82442d2 --- /dev/null +++ b/frontend/src/lib/components/ui/separator/index.ts @@ -0,0 +1,7 @@ +import Root from "./separator.svelte"; + +export { + Root, + // + Root as Separator, +}; diff --git a/frontend/src/lib/components/ui/separator/separator.svelte b/frontend/src/lib/components/ui/separator/separator.svelte new file mode 100644 index 0000000..09d88f4 --- /dev/null +++ b/frontend/src/lib/components/ui/separator/separator.svelte @@ -0,0 +1,20 @@ + + + diff --git a/frontend/src/lib/components/ui/sheet/index.ts b/frontend/src/lib/components/ui/sheet/index.ts new file mode 100644 index 0000000..01d40c8 --- /dev/null +++ b/frontend/src/lib/components/ui/sheet/index.ts @@ -0,0 +1,36 @@ +import { Dialog as SheetPrimitive } from "bits-ui"; +import Trigger from "./sheet-trigger.svelte"; +import Close from "./sheet-close.svelte"; +import Overlay from "./sheet-overlay.svelte"; +import Content from "./sheet-content.svelte"; +import Header from "./sheet-header.svelte"; +import Footer from "./sheet-footer.svelte"; +import Title from "./sheet-title.svelte"; +import Description from "./sheet-description.svelte"; + +const Root = SheetPrimitive.Root; +const Portal = SheetPrimitive.Portal; + +export { + Root, + Close, + Trigger, + Portal, + Overlay, + Content, + Header, + Footer, + Title, + Description, + // + Root as Sheet, + Close as SheetClose, + Trigger as SheetTrigger, + Portal as SheetPortal, + Overlay as SheetOverlay, + Content as SheetContent, + Header as SheetHeader, + Footer as SheetFooter, + Title as SheetTitle, + Description as SheetDescription, +}; diff --git a/frontend/src/lib/components/ui/sheet/sheet-close.svelte b/frontend/src/lib/components/ui/sheet/sheet-close.svelte new file mode 100644 index 0000000..ae382c1 --- /dev/null +++ b/frontend/src/lib/components/ui/sheet/sheet-close.svelte @@ -0,0 +1,7 @@ + + + diff --git a/frontend/src/lib/components/ui/sheet/sheet-content.svelte b/frontend/src/lib/components/ui/sheet/sheet-content.svelte new file mode 100644 index 0000000..856922e --- /dev/null +++ b/frontend/src/lib/components/ui/sheet/sheet-content.svelte @@ -0,0 +1,58 @@ + + + + + + + + {@render children?.()} + + + Close + + + diff --git a/frontend/src/lib/components/ui/sheet/sheet-description.svelte b/frontend/src/lib/components/ui/sheet/sheet-description.svelte new file mode 100644 index 0000000..333b17a --- /dev/null +++ b/frontend/src/lib/components/ui/sheet/sheet-description.svelte @@ -0,0 +1,17 @@ + + + diff --git a/frontend/src/lib/components/ui/sheet/sheet-footer.svelte b/frontend/src/lib/components/ui/sheet/sheet-footer.svelte new file mode 100644 index 0000000..dd9ed84 --- /dev/null +++ b/frontend/src/lib/components/ui/sheet/sheet-footer.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/frontend/src/lib/components/ui/sheet/sheet-header.svelte b/frontend/src/lib/components/ui/sheet/sheet-header.svelte new file mode 100644 index 0000000..757a6a5 --- /dev/null +++ b/frontend/src/lib/components/ui/sheet/sheet-header.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/frontend/src/lib/components/ui/sheet/sheet-overlay.svelte b/frontend/src/lib/components/ui/sheet/sheet-overlay.svelte new file mode 100644 index 0000000..345e197 --- /dev/null +++ b/frontend/src/lib/components/ui/sheet/sheet-overlay.svelte @@ -0,0 +1,20 @@ + + + diff --git a/frontend/src/lib/components/ui/sheet/sheet-title.svelte b/frontend/src/lib/components/ui/sheet/sheet-title.svelte new file mode 100644 index 0000000..9fda327 --- /dev/null +++ b/frontend/src/lib/components/ui/sheet/sheet-title.svelte @@ -0,0 +1,17 @@ + + + diff --git a/frontend/src/lib/components/ui/sheet/sheet-trigger.svelte b/frontend/src/lib/components/ui/sheet/sheet-trigger.svelte new file mode 100644 index 0000000..e266975 --- /dev/null +++ b/frontend/src/lib/components/ui/sheet/sheet-trigger.svelte @@ -0,0 +1,7 @@ + + + diff --git a/frontend/src/lib/components/ui/sidebar/constants.ts b/frontend/src/lib/components/ui/sidebar/constants.ts new file mode 100644 index 0000000..4de4435 --- /dev/null +++ b/frontend/src/lib/components/ui/sidebar/constants.ts @@ -0,0 +1,6 @@ +export const SIDEBAR_COOKIE_NAME = "sidebar:state"; +export const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7; +export const SIDEBAR_WIDTH = "16rem"; +export const SIDEBAR_WIDTH_MOBILE = "18rem"; +export const SIDEBAR_WIDTH_ICON = "3rem"; +export const SIDEBAR_KEYBOARD_SHORTCUT = "b"; diff --git a/frontend/src/lib/components/ui/sidebar/context.svelte.ts b/frontend/src/lib/components/ui/sidebar/context.svelte.ts new file mode 100644 index 0000000..15248ad --- /dev/null +++ b/frontend/src/lib/components/ui/sidebar/context.svelte.ts @@ -0,0 +1,81 @@ +import { IsMobile } from "$lib/hooks/is-mobile.svelte.js"; +import { getContext, setContext } from "svelte"; +import { SIDEBAR_KEYBOARD_SHORTCUT } from "./constants.js"; + +type Getter = () => T; + +export type SidebarStateProps = { + /** + * A getter function that returns the current open state of the sidebar. + * We use a getter function here to support `bind:open` on the `Sidebar.Provider` + * component. + */ + open: Getter; + + /** + * A function that sets the open state of the sidebar. To support `bind:open`, we need + * a source of truth for changing the open state to ensure it will be synced throughout + * the sub-components and any `bind:` references. + */ + setOpen: (open: boolean) => void; +}; + +class SidebarState { + readonly props: SidebarStateProps; + open = $derived.by(() => this.props.open()); + openMobile = $state(false); + setOpen: SidebarStateProps["setOpen"]; + #isMobile: IsMobile; + state = $derived.by(() => (this.open ? "expanded" : "collapsed")); + + constructor(props: SidebarStateProps) { + this.setOpen = props.setOpen; + this.#isMobile = new IsMobile(); + this.props = props; + } + + // Convenience getter for checking if the sidebar is mobile + // without this, we would need to use `sidebar.isMobile.current` everywhere + get isMobile() { + return this.#isMobile.current; + } + + // Event handler to apply to the `` + handleShortcutKeydown = (e: KeyboardEvent) => { + if (e.key === SIDEBAR_KEYBOARD_SHORTCUT && (e.metaKey || e.ctrlKey)) { + e.preventDefault(); + this.toggle(); + } + }; + + setOpenMobile = (value: boolean) => { + this.openMobile = value; + }; + + toggle = () => { + return this.#isMobile.current + ? (this.openMobile = !this.openMobile) + : this.setOpen(!this.open); + }; +} + +const SYMBOL_KEY = "scn-sidebar"; + +/** + * Instantiates a new `SidebarState` instance and sets it in the context. + * + * @param props The constructor props for the `SidebarState` class. + * @returns The `SidebarState` instance. + */ +export function setSidebar(props: SidebarStateProps): SidebarState { + return setContext(Symbol.for(SYMBOL_KEY), new SidebarState(props)); +} + +/** + * Retrieves the `SidebarState` instance from the context. This is a class instance, + * so you cannot destructure it. + * @returns The `SidebarState` instance. + */ +export function useSidebar(): SidebarState { + return getContext(Symbol.for(SYMBOL_KEY)); +} diff --git a/frontend/src/lib/components/ui/sidebar/index.ts b/frontend/src/lib/components/ui/sidebar/index.ts new file mode 100644 index 0000000..318a341 --- /dev/null +++ b/frontend/src/lib/components/ui/sidebar/index.ts @@ -0,0 +1,75 @@ +import { useSidebar } from "./context.svelte.js"; +import Content from "./sidebar-content.svelte"; +import Footer from "./sidebar-footer.svelte"; +import GroupAction from "./sidebar-group-action.svelte"; +import GroupContent from "./sidebar-group-content.svelte"; +import GroupLabel from "./sidebar-group-label.svelte"; +import Group from "./sidebar-group.svelte"; +import Header from "./sidebar-header.svelte"; +import Input from "./sidebar-input.svelte"; +import Inset from "./sidebar-inset.svelte"; +import MenuAction from "./sidebar-menu-action.svelte"; +import MenuBadge from "./sidebar-menu-badge.svelte"; +import MenuButton from "./sidebar-menu-button.svelte"; +import MenuItem from "./sidebar-menu-item.svelte"; +import MenuSkeleton from "./sidebar-menu-skeleton.svelte"; +import MenuSubButton from "./sidebar-menu-sub-button.svelte"; +import MenuSubItem from "./sidebar-menu-sub-item.svelte"; +import MenuSub from "./sidebar-menu-sub.svelte"; +import Menu from "./sidebar-menu.svelte"; +import Provider from "./sidebar-provider.svelte"; +import Rail from "./sidebar-rail.svelte"; +import Separator from "./sidebar-separator.svelte"; +import Trigger from "./sidebar-trigger.svelte"; +import Root from "./sidebar.svelte"; + +export { + Content, + Footer, + Group, + GroupAction, + GroupContent, + GroupLabel, + Header, + Input, + Inset, + Menu, + MenuAction, + MenuBadge, + MenuButton, + MenuItem, + MenuSkeleton, + MenuSub, + MenuSubButton, + MenuSubItem, + Provider, + Rail, + Root, + Separator, + // + Root as Sidebar, + Content as SidebarContent, + Footer as SidebarFooter, + Group as SidebarGroup, + GroupAction as SidebarGroupAction, + GroupContent as SidebarGroupContent, + GroupLabel as SidebarGroupLabel, + Header as SidebarHeader, + Input as SidebarInput, + Inset as SidebarInset, + Menu as SidebarMenu, + MenuAction as SidebarMenuAction, + MenuBadge as SidebarMenuBadge, + MenuButton as SidebarMenuButton, + MenuItem as SidebarMenuItem, + MenuSkeleton as SidebarMenuSkeleton, + MenuSub as SidebarMenuSub, + MenuSubButton as SidebarMenuSubButton, + MenuSubItem as SidebarMenuSubItem, + Provider as SidebarProvider, + Rail as SidebarRail, + Separator as SidebarSeparator, + Trigger as SidebarTrigger, + Trigger, + useSidebar, +}; diff --git a/frontend/src/lib/components/ui/sidebar/sidebar-content.svelte b/frontend/src/lib/components/ui/sidebar/sidebar-content.svelte new file mode 100644 index 0000000..f121800 --- /dev/null +++ b/frontend/src/lib/components/ui/sidebar/sidebar-content.svelte @@ -0,0 +1,24 @@ + + +
+ {@render children?.()} +
diff --git a/frontend/src/lib/components/ui/sidebar/sidebar-footer.svelte b/frontend/src/lib/components/ui/sidebar/sidebar-footer.svelte new file mode 100644 index 0000000..6259cb9 --- /dev/null +++ b/frontend/src/lib/components/ui/sidebar/sidebar-footer.svelte @@ -0,0 +1,21 @@ + + +
+ {@render children?.()} +
diff --git a/frontend/src/lib/components/ui/sidebar/sidebar-group-action.svelte b/frontend/src/lib/components/ui/sidebar/sidebar-group-action.svelte new file mode 100644 index 0000000..fb84e4a --- /dev/null +++ b/frontend/src/lib/components/ui/sidebar/sidebar-group-action.svelte @@ -0,0 +1,36 @@ + + +{#if child} + {@render child({ props: mergedProps })} +{:else} + +{/if} diff --git a/frontend/src/lib/components/ui/sidebar/sidebar-group-content.svelte b/frontend/src/lib/components/ui/sidebar/sidebar-group-content.svelte new file mode 100644 index 0000000..415255f --- /dev/null +++ b/frontend/src/lib/components/ui/sidebar/sidebar-group-content.svelte @@ -0,0 +1,21 @@ + + +
+ {@render children?.()} +
diff --git a/frontend/src/lib/components/ui/sidebar/sidebar-group-label.svelte b/frontend/src/lib/components/ui/sidebar/sidebar-group-label.svelte new file mode 100644 index 0000000..e292945 --- /dev/null +++ b/frontend/src/lib/components/ui/sidebar/sidebar-group-label.svelte @@ -0,0 +1,34 @@ + + +{#if child} + {@render child({ props: mergedProps })} +{:else} +
+ {@render children?.()} +
+{/if} diff --git a/frontend/src/lib/components/ui/sidebar/sidebar-group.svelte b/frontend/src/lib/components/ui/sidebar/sidebar-group.svelte new file mode 100644 index 0000000..ec18a69 --- /dev/null +++ b/frontend/src/lib/components/ui/sidebar/sidebar-group.svelte @@ -0,0 +1,21 @@ + + +
+ {@render children?.()} +
diff --git a/frontend/src/lib/components/ui/sidebar/sidebar-header.svelte b/frontend/src/lib/components/ui/sidebar/sidebar-header.svelte new file mode 100644 index 0000000..a1b2db1 --- /dev/null +++ b/frontend/src/lib/components/ui/sidebar/sidebar-header.svelte @@ -0,0 +1,21 @@ + + +
+ {@render children?.()} +
diff --git a/frontend/src/lib/components/ui/sidebar/sidebar-input.svelte b/frontend/src/lib/components/ui/sidebar/sidebar-input.svelte new file mode 100644 index 0000000..19b3666 --- /dev/null +++ b/frontend/src/lib/components/ui/sidebar/sidebar-input.svelte @@ -0,0 +1,21 @@ + + + diff --git a/frontend/src/lib/components/ui/sidebar/sidebar-inset.svelte b/frontend/src/lib/components/ui/sidebar/sidebar-inset.svelte new file mode 100644 index 0000000..d862761 --- /dev/null +++ b/frontend/src/lib/components/ui/sidebar/sidebar-inset.svelte @@ -0,0 +1,24 @@ + + +
+ {@render children?.()} +
diff --git a/frontend/src/lib/components/ui/sidebar/sidebar-menu-action.svelte b/frontend/src/lib/components/ui/sidebar/sidebar-menu-action.svelte new file mode 100644 index 0000000..fa3fb0c --- /dev/null +++ b/frontend/src/lib/components/ui/sidebar/sidebar-menu-action.svelte @@ -0,0 +1,43 @@ + + +{#if child} + {@render child({ props: mergedProps })} +{:else} + +{/if} diff --git a/frontend/src/lib/components/ui/sidebar/sidebar-menu-badge.svelte b/frontend/src/lib/components/ui/sidebar/sidebar-menu-badge.svelte new file mode 100644 index 0000000..69e5a3c --- /dev/null +++ b/frontend/src/lib/components/ui/sidebar/sidebar-menu-badge.svelte @@ -0,0 +1,29 @@ + + +
+ {@render children?.()} +
diff --git a/frontend/src/lib/components/ui/sidebar/sidebar-menu-button.svelte b/frontend/src/lib/components/ui/sidebar/sidebar-menu-button.svelte new file mode 100644 index 0000000..4bef683 --- /dev/null +++ b/frontend/src/lib/components/ui/sidebar/sidebar-menu-button.svelte @@ -0,0 +1,103 @@ + + + + +{#snippet Button({ props }: { props?: Record })} + {@const mergedProps = mergeProps(buttonProps, props)} + {#if child} + {@render child({ props: mergedProps })} + {:else} + + {/if} +{/snippet} + +{#if !tooltipContent} + {@render Button({})} +{:else} + + + {#snippet child({ props })} + {@render Button({ props })} + {/snippet} + + + +{/if} diff --git a/frontend/src/lib/components/ui/sidebar/sidebar-menu-item.svelte b/frontend/src/lib/components/ui/sidebar/sidebar-menu-item.svelte new file mode 100644 index 0000000..4db4453 --- /dev/null +++ b/frontend/src/lib/components/ui/sidebar/sidebar-menu-item.svelte @@ -0,0 +1,21 @@ + + +
  • + {@render children?.()} +
  • diff --git a/frontend/src/lib/components/ui/sidebar/sidebar-menu-skeleton.svelte b/frontend/src/lib/components/ui/sidebar/sidebar-menu-skeleton.svelte new file mode 100644 index 0000000..cc63b04 --- /dev/null +++ b/frontend/src/lib/components/ui/sidebar/sidebar-menu-skeleton.svelte @@ -0,0 +1,36 @@ + + +
    + {#if showIcon} + + {/if} + + {@render children?.()} +
    diff --git a/frontend/src/lib/components/ui/sidebar/sidebar-menu-sub-button.svelte b/frontend/src/lib/components/ui/sidebar/sidebar-menu-sub-button.svelte new file mode 100644 index 0000000..987f104 --- /dev/null +++ b/frontend/src/lib/components/ui/sidebar/sidebar-menu-sub-button.svelte @@ -0,0 +1,43 @@ + + +{#if child} + {@render child({ props: mergedProps })} +{:else} + + {@render children?.()} + +{/if} diff --git a/frontend/src/lib/components/ui/sidebar/sidebar-menu-sub-item.svelte b/frontend/src/lib/components/ui/sidebar/sidebar-menu-sub-item.svelte new file mode 100644 index 0000000..681d0f1 --- /dev/null +++ b/frontend/src/lib/components/ui/sidebar/sidebar-menu-sub-item.svelte @@ -0,0 +1,21 @@ + + +
  • + {@render children?.()} +
  • diff --git a/frontend/src/lib/components/ui/sidebar/sidebar-menu-sub.svelte b/frontend/src/lib/components/ui/sidebar/sidebar-menu-sub.svelte new file mode 100644 index 0000000..8ab1111 --- /dev/null +++ b/frontend/src/lib/components/ui/sidebar/sidebar-menu-sub.svelte @@ -0,0 +1,25 @@ + + +
      + {@render children?.()} +
    diff --git a/frontend/src/lib/components/ui/sidebar/sidebar-menu.svelte b/frontend/src/lib/components/ui/sidebar/sidebar-menu.svelte new file mode 100644 index 0000000..946ccce --- /dev/null +++ b/frontend/src/lib/components/ui/sidebar/sidebar-menu.svelte @@ -0,0 +1,21 @@ + + +
      + {@render children?.()} +
    diff --git a/frontend/src/lib/components/ui/sidebar/sidebar-provider.svelte b/frontend/src/lib/components/ui/sidebar/sidebar-provider.svelte new file mode 100644 index 0000000..2c13b5d --- /dev/null +++ b/frontend/src/lib/components/ui/sidebar/sidebar-provider.svelte @@ -0,0 +1,53 @@ + + + + + +
    + {@render children?.()} +
    +
    diff --git a/frontend/src/lib/components/ui/sidebar/sidebar-rail.svelte b/frontend/src/lib/components/ui/sidebar/sidebar-rail.svelte new file mode 100644 index 0000000..5df80f7 --- /dev/null +++ b/frontend/src/lib/components/ui/sidebar/sidebar-rail.svelte @@ -0,0 +1,36 @@ + + + diff --git a/frontend/src/lib/components/ui/sidebar/sidebar-separator.svelte b/frontend/src/lib/components/ui/sidebar/sidebar-separator.svelte new file mode 100644 index 0000000..5a7deda --- /dev/null +++ b/frontend/src/lib/components/ui/sidebar/sidebar-separator.svelte @@ -0,0 +1,19 @@ + + + diff --git a/frontend/src/lib/components/ui/sidebar/sidebar-trigger.svelte b/frontend/src/lib/components/ui/sidebar/sidebar-trigger.svelte new file mode 100644 index 0000000..1825182 --- /dev/null +++ b/frontend/src/lib/components/ui/sidebar/sidebar-trigger.svelte @@ -0,0 +1,35 @@ + + + diff --git a/frontend/src/lib/components/ui/sidebar/sidebar.svelte b/frontend/src/lib/components/ui/sidebar/sidebar.svelte new file mode 100644 index 0000000..f825157 --- /dev/null +++ b/frontend/src/lib/components/ui/sidebar/sidebar.svelte @@ -0,0 +1,101 @@ + + +{#if collapsible === 'none'} +
    + {@render children?.()} +
    +{:else if sidebar.isMobile} + sidebar.openMobile, (v) => sidebar.setOpenMobile(v)} {...restProps}> + + + Sidebar + Displays the mobile sidebar. + +
    + {@render children?.()} +
    +
    +
    +{:else} + +{/if} diff --git a/frontend/src/lib/components/ui/skeleton/index.ts b/frontend/src/lib/components/ui/skeleton/index.ts new file mode 100644 index 0000000..186db21 --- /dev/null +++ b/frontend/src/lib/components/ui/skeleton/index.ts @@ -0,0 +1,7 @@ +import Root from "./skeleton.svelte"; + +export { + Root, + // + Root as Skeleton, +}; diff --git a/frontend/src/lib/components/ui/skeleton/skeleton.svelte b/frontend/src/lib/components/ui/skeleton/skeleton.svelte new file mode 100644 index 0000000..c7e3d26 --- /dev/null +++ b/frontend/src/lib/components/ui/skeleton/skeleton.svelte @@ -0,0 +1,17 @@ + + +
    diff --git a/frontend/src/lib/components/ui/tooltip/index.ts b/frontend/src/lib/components/ui/tooltip/index.ts new file mode 100644 index 0000000..313a7f0 --- /dev/null +++ b/frontend/src/lib/components/ui/tooltip/index.ts @@ -0,0 +1,21 @@ +import { Tooltip as TooltipPrimitive } from "bits-ui"; +import Trigger from "./tooltip-trigger.svelte"; +import Content from "./tooltip-content.svelte"; + +const Root = TooltipPrimitive.Root; +const Provider = TooltipPrimitive.Provider; +const Portal = TooltipPrimitive.Portal; + +export { + Root, + Trigger, + Content, + Provider, + Portal, + // + Root as Tooltip, + Content as TooltipContent, + Trigger as TooltipTrigger, + Provider as TooltipProvider, + Portal as TooltipPortal, +}; diff --git a/frontend/src/lib/components/ui/tooltip/tooltip-content.svelte b/frontend/src/lib/components/ui/tooltip/tooltip-content.svelte new file mode 100644 index 0000000..e495efe --- /dev/null +++ b/frontend/src/lib/components/ui/tooltip/tooltip-content.svelte @@ -0,0 +1,47 @@ + + + + + {@render children?.()} + + {#snippet child({ props })} +
    + {/snippet} +
    +
    +
    diff --git a/frontend/src/lib/components/ui/tooltip/tooltip-trigger.svelte b/frontend/src/lib/components/ui/tooltip/tooltip-trigger.svelte new file mode 100644 index 0000000..1acdaa4 --- /dev/null +++ b/frontend/src/lib/components/ui/tooltip/tooltip-trigger.svelte @@ -0,0 +1,7 @@ + + + diff --git a/frontend/src/lib/hooks/is-mobile.svelte.ts b/frontend/src/lib/hooks/is-mobile.svelte.ts new file mode 100644 index 0000000..4829c00 --- /dev/null +++ b/frontend/src/lib/hooks/is-mobile.svelte.ts @@ -0,0 +1,9 @@ +import { MediaQuery } from "svelte/reactivity"; + +const DEFAULT_MOBILE_BREAKPOINT = 768; + +export class IsMobile extends MediaQuery { + constructor(breakpoint: number = DEFAULT_MOBILE_BREAKPOINT) { + super(`max-width: ${breakpoint - 1}px`); + } +} diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte index 921aa44..f4e4afd 100644 --- a/frontend/src/routes/+layout.svelte +++ b/frontend/src/routes/+layout.svelte @@ -3,6 +3,8 @@ import { ModeWatcher } from 'mode-watcher'; import { Toaster } from '$lib/components/ui/sonner/index.js'; import favicon from '$lib/assets/favicon.svg'; + import * as Sidebar from '$lib/components/ui/sidebar/index.js'; + import LeadSidebar from '$lib/components/ui/lead-sidebar/lead-sidebar.svelte'; let { children } = $props(); @@ -13,4 +15,10 @@ -{@render children?.()} + + + +
    + {@render children?.()} +
    +
    diff --git a/frontend/src/routes/+layout.ts b/frontend/src/routes/+layout.ts new file mode 100644 index 0000000..189f71e --- /dev/null +++ b/frontend/src/routes/+layout.ts @@ -0,0 +1 @@ +export const prerender = true; diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte index f4ab7e6..e412480 100644 --- a/frontend/src/routes/+page.svelte +++ b/frontend/src/routes/+page.svelte @@ -1,24 +1,37 @@ - - - - - - - - - +{#if sidebar?.openMobile || sidebar?.open} + +{/if} -
    - +
    +
    +
    + +
    + +
    + +
    +
    + +