🙃
This commit is contained in:
@@ -28,9 +28,9 @@ impl Evaluator {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_cell(&mut self, cell_ref: CellRef, raw_val: String) -> Result<(), String> {
|
pub fn set_cell(&mut self, cell_ref: CellRef, raw_val: String) -> Result<Eval, String> {
|
||||||
if self.cells.contains_key(&cell_ref) && self.cells[&cell_ref].raw() == raw_val {
|
if self.cells.contains_key(&cell_ref) && self.cells[&cell_ref].raw() == raw_val {
|
||||||
return Ok(());
|
return self.get_cell(cell_ref);
|
||||||
}
|
}
|
||||||
|
|
||||||
let eval: Eval;
|
let eval: Eval;
|
||||||
@@ -50,19 +50,23 @@ impl Evaluator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.cells.insert(cell_ref, Cell::new(eval, raw_val));
|
self.cells
|
||||||
Ok(())
|
.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<(String, Eval), String> {
|
||||||
|
pub fn get_cell(&mut self, cell_ref: CellRef) -> Result<Eval, String> {
|
||||||
if !self.cells.contains_key(&cell_ref) {
|
if !self.cells.contains_key(&cell_ref) {
|
||||||
return Err(format!("Cell at {:?} not found.", cell_ref));
|
return Err(format!("Cell at {:?} not found.", cell_ref));
|
||||||
}
|
}
|
||||||
|
|
||||||
let cell = &self.cells[&cell_ref];
|
let cell = &self.cells[&cell_ref];
|
||||||
|
|
||||||
Ok((cell.raw(), cell.eval()))
|
// Ok((cell.raw(), cell.eval()))
|
||||||
|
Ok(cell.eval())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_cell_dep(&mut self, cell_ref: CellRef, dep_ref: CellRef) -> Result<(), String> {
|
pub fn add_cell_dep(&mut self, cell_ref: CellRef, dep_ref: CellRef) -> Result<(), String> {
|
||||||
if !self.cells.contains_key(&cell_ref) {
|
if !self.cells.contains_key(&cell_ref) {
|
||||||
return Err(format!("Cell at {:?} not found.", cell_ref));
|
return Err(format!("Cell at {:?} not found.", cell_ref));
|
||||||
@@ -87,7 +91,7 @@ impl Evaluator {
|
|||||||
fn evaluate_expr(&mut self, expr: &Expr) -> Result<Eval, String> {
|
fn evaluate_expr(&mut self, expr: &Expr) -> Result<Eval, String> {
|
||||||
let res = match expr {
|
let res = match expr {
|
||||||
Expr::Literal(lit) => Eval::Literal(lit.clone()),
|
Expr::Literal(lit) => Eval::Literal(lit.clone()),
|
||||||
Expr::CellRef(re) => self.get_cell(re.to_owned())?.1,
|
Expr::CellRef(re) => self.get_cell(re.to_owned())?,
|
||||||
Expr::Infix { op, lhs, rhs } => {
|
Expr::Infix { op, lhs, rhs } => {
|
||||||
let lval = self.evaluate_expr(lhs)?;
|
let lval = self.evaluate_expr(lhs)?;
|
||||||
let rval = self.evaluate_expr(rhs)?;
|
let rval = self.evaluate_expr(rhs)?;
|
||||||
@@ -151,6 +155,14 @@ fn eval_mul(lval: &Eval, rval: &Eval) -> Result<Eval, String> {
|
|||||||
fn eval_div(lval: &Eval, rval: &Eval) -> Result<Eval, String> {
|
fn eval_div(lval: &Eval, rval: &Eval) -> Result<Eval, String> {
|
||||||
match (lval, rval) {
|
match (lval, rval) {
|
||||||
(Eval::Literal(a), Eval::Literal(b)) => {
|
(Eval::Literal(a), Eval::Literal(b)) => {
|
||||||
|
if let (Literal::Integer(_), Literal::Integer(y)) = (a, b) {
|
||||||
|
if *y == 0 {
|
||||||
|
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, |x, y| x / y) {
|
||||||
return Ok(Eval::Literal(res));
|
return Ok(Eval::Literal(res));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,6 +80,36 @@ async fn main() -> Result<(), Error> {
|
|||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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) {
|
async fn accept_connection(stream: TcpStream) {
|
||||||
let addr = stream
|
let addr = stream
|
||||||
.peer_addr()
|
.peer_addr()
|
||||||
@@ -94,15 +124,57 @@ async fn accept_connection(stream: TcpStream) {
|
|||||||
|
|
||||||
let (mut write, mut read) = ws_stream.split();
|
let (mut write, mut read) = ws_stream.split();
|
||||||
|
|
||||||
// We should not forward messages other than text or binary.
|
// Each connection gets its own evaluator
|
||||||
|
let mut evaluator = Evaluator::new();
|
||||||
|
|
||||||
while let Some(msg) = read.try_next().await.unwrap_or(None) {
|
while let Some(msg) = read.try_next().await.unwrap_or(None) {
|
||||||
if msg.is_text() || msg.is_binary() {
|
if msg.is_text() {
|
||||||
if let Err(e) = write
|
let input = msg.to_text().unwrap_or("").trim().to_string();
|
||||||
.send(format!("This is a message {}!", msg.to_text().unwrap_or("")).into())
|
|
||||||
.await
|
let cmds = ["set", "get"];
|
||||||
{
|
let cmd = &input[0..3.min(input.len())]; // avoid panic on short strings
|
||||||
eprintln!("send error: {}", e);
|
|
||||||
break;
|
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;
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let _ = write
|
||||||
|
.send(format!("ERR invalid cell reference: {}", raw_ref).into())
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@
|
|||||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||||
"svelte": "^5.0.0",
|
"svelte": "^5.0.0",
|
||||||
"svelte-check": "^4.0.0",
|
"svelte-check": "^4.0.0",
|
||||||
|
"svelte-sonner": "^1.0.5",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
"tailwind-variants": "^1.0.0",
|
"tailwind-variants": "^1.0.0",
|
||||||
"tailwindcss": "^4.0.0",
|
"tailwindcss": "^4.0.0",
|
||||||
|
|||||||
23
frontend/pnpm-lock.yaml
generated
23
frontend/pnpm-lock.yaml
generated
@@ -57,6 +57,9 @@ importers:
|
|||||||
svelte-check:
|
svelte-check:
|
||||||
specifier: ^4.0.0
|
specifier: ^4.0.0
|
||||||
version: 4.3.1(picomatch@4.0.3)(svelte@5.38.6)(typescript@5.9.2)
|
version: 4.3.1(picomatch@4.0.3)(svelte@5.38.6)(typescript@5.9.2)
|
||||||
|
svelte-sonner:
|
||||||
|
specifier: ^1.0.5
|
||||||
|
version: 1.0.5(svelte@5.38.6)
|
||||||
tailwind-merge:
|
tailwind-merge:
|
||||||
specifier: ^3.3.1
|
specifier: ^3.3.1
|
||||||
version: 3.3.1
|
version: 3.3.1
|
||||||
@@ -859,6 +862,11 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
svelte: ^5.7.0
|
svelte: ^5.7.0
|
||||||
|
|
||||||
|
runed@0.28.0:
|
||||||
|
resolution: {integrity: sha512-k2xx7RuO9hWcdd9f+8JoBeqWtYrm5CALfgpkg2YDB80ds/QE4w0qqu34A7fqiAwiBBSBQOid7TLxwxVC27ymWQ==}
|
||||||
|
peerDependencies:
|
||||||
|
svelte: ^5.7.0
|
||||||
|
|
||||||
runed@0.29.2:
|
runed@0.29.2:
|
||||||
resolution: {integrity: sha512-0cq6cA6sYGZwl/FvVqjx9YN+1xEBu9sDDyuWdDW1yWX7JF2wmvmVKfH+hVCZs+csW+P3ARH92MjI3H9QTagOQA==}
|
resolution: {integrity: sha512-0cq6cA6sYGZwl/FvVqjx9YN+1xEBu9sDDyuWdDW1yWX7JF2wmvmVKfH+hVCZs+csW+P3ARH92MjI3H9QTagOQA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -890,6 +898,11 @@ packages:
|
|||||||
svelte: ^4.0.0 || ^5.0.0-next.0
|
svelte: ^4.0.0 || ^5.0.0-next.0
|
||||||
typescript: '>=5.0.0'
|
typescript: '>=5.0.0'
|
||||||
|
|
||||||
|
svelte-sonner@1.0.5:
|
||||||
|
resolution: {integrity: sha512-9dpGPFqKb/QWudYqGnEz93vuY+NgCEvyNvxoCLMVGw6sDN/3oVeKV1xiEirW2E1N3vJEyj5imSBNOGltQHA7mg==}
|
||||||
|
peerDependencies:
|
||||||
|
svelte: ^5.0.0
|
||||||
|
|
||||||
svelte-toolbelt@0.7.1:
|
svelte-toolbelt@0.7.1:
|
||||||
resolution: {integrity: sha512-HcBOcR17Vx9bjaOceUvxkY3nGmbBmCBBbuWLLEWO6jtmWH8f/QoWmbyUfQZrpDINH39en1b8mptfPQT9VKQ1xQ==}
|
resolution: {integrity: sha512-HcBOcR17Vx9bjaOceUvxkY3nGmbBmCBBbuWLLEWO6jtmWH8f/QoWmbyUfQZrpDINH39en1b8mptfPQT9VKQ1xQ==}
|
||||||
engines: {node: '>=18', pnpm: '>=8.7.0'}
|
engines: {node: '>=18', pnpm: '>=8.7.0'}
|
||||||
@@ -1584,6 +1597,11 @@ snapshots:
|
|||||||
esm-env: 1.2.2
|
esm-env: 1.2.2
|
||||||
svelte: 5.38.6
|
svelte: 5.38.6
|
||||||
|
|
||||||
|
runed@0.28.0(svelte@5.38.6):
|
||||||
|
dependencies:
|
||||||
|
esm-env: 1.2.2
|
||||||
|
svelte: 5.38.6
|
||||||
|
|
||||||
runed@0.29.2(svelte@5.38.6):
|
runed@0.29.2(svelte@5.38.6):
|
||||||
dependencies:
|
dependencies:
|
||||||
esm-env: 1.2.2
|
esm-env: 1.2.2
|
||||||
@@ -1619,6 +1637,11 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- picomatch
|
- picomatch
|
||||||
|
|
||||||
|
svelte-sonner@1.0.5(svelte@5.38.6):
|
||||||
|
dependencies:
|
||||||
|
runed: 0.28.0(svelte@5.38.6)
|
||||||
|
svelte: 5.38.6
|
||||||
|
|
||||||
svelte-toolbelt@0.7.1(svelte@5.38.6):
|
svelte-toolbelt@0.7.1(svelte@5.38.6):
|
||||||
dependencies:
|
dependencies:
|
||||||
clsx: 2.1.1
|
clsx: 2.1.1
|
||||||
|
|||||||
@@ -1,76 +1,76 @@
|
|||||||
@import "tailwindcss";
|
@import 'tailwindcss';
|
||||||
|
|
||||||
@import "tw-animate-css";
|
@import 'tw-animate-css';
|
||||||
|
|
||||||
@custom-variant dark (&:is(.dark *));
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--radius: 0.625rem;
|
--radius: 0.65rem;
|
||||||
--background: oklch(1 0 0);
|
--background: oklch(1 0 0);
|
||||||
--foreground: oklch(0.129 0.042 264.695);
|
--foreground: oklch(0.141 0.005 285.823);
|
||||||
--card: oklch(1 0 0);
|
--card: oklch(1 0 0);
|
||||||
--card-foreground: oklch(0.129 0.042 264.695);
|
--card-foreground: oklch(0.141 0.005 285.823);
|
||||||
--popover: oklch(1 0 0);
|
--popover: oklch(1 0 0);
|
||||||
--popover-foreground: oklch(0.129 0.042 264.695);
|
--popover-foreground: oklch(0.141 0.005 285.823);
|
||||||
--primary: oklch(0.208 0.042 265.755);
|
--primary: oklch(0.623 0.214 259.815);
|
||||||
--primary-foreground: oklch(0.984 0.003 247.858);
|
--primary-foreground: oklch(0.97 0.014 254.604);
|
||||||
--secondary: oklch(0.968 0.007 247.896);
|
--secondary: oklch(0.967 0.001 286.375);
|
||||||
--secondary-foreground: oklch(0.208 0.042 265.755);
|
--secondary-foreground: oklch(0.21 0.006 285.885);
|
||||||
--muted: oklch(0.968 0.007 247.896);
|
--muted: oklch(0.967 0.001 286.375);
|
||||||
--muted-foreground: oklch(0.554 0.046 257.417);
|
--muted-foreground: oklch(0.552 0.016 285.938);
|
||||||
--accent: oklch(0.968 0.007 247.896);
|
--accent: oklch(0.967 0.001 286.375);
|
||||||
--accent-foreground: oklch(0.208 0.042 265.755);
|
--accent-foreground: oklch(0.21 0.006 285.885);
|
||||||
--destructive: oklch(0.577 0.245 27.325);
|
--destructive: oklch(0.577 0.245 27.325);
|
||||||
--border: oklch(0.929 0.013 255.508);
|
--border: oklch(0.92 0.004 286.32);
|
||||||
--input: oklch(0.929 0.013 255.508);
|
--input: oklch(0.92 0.004 286.32);
|
||||||
--ring: oklch(0.704 0.04 256.788);
|
--ring: oklch(0.623 0.214 259.815);
|
||||||
--chart-1: oklch(0.646 0.222 41.116);
|
--chart-1: oklch(0.646 0.222 41.116);
|
||||||
--chart-2: oklch(0.6 0.118 184.704);
|
--chart-2: oklch(0.6 0.118 184.704);
|
||||||
--chart-3: oklch(0.398 0.07 227.392);
|
--chart-3: oklch(0.398 0.07 227.392);
|
||||||
--chart-4: oklch(0.828 0.189 84.429);
|
--chart-4: oklch(0.828 0.189 84.429);
|
||||||
--chart-5: oklch(0.769 0.188 70.08);
|
--chart-5: oklch(0.769 0.188 70.08);
|
||||||
--sidebar: oklch(0.984 0.003 247.858);
|
--sidebar: oklch(0.985 0 0);
|
||||||
--sidebar-foreground: oklch(0.129 0.042 264.695);
|
--sidebar-foreground: oklch(0.141 0.005 285.823);
|
||||||
--sidebar-primary: oklch(0.208 0.042 265.755);
|
--sidebar-primary: oklch(0.623 0.214 259.815);
|
||||||
--sidebar-primary-foreground: oklch(0.984 0.003 247.858);
|
--sidebar-primary-foreground: oklch(0.97 0.014 254.604);
|
||||||
--sidebar-accent: oklch(0.968 0.007 247.896);
|
--sidebar-accent: oklch(0.967 0.001 286.375);
|
||||||
--sidebar-accent-foreground: oklch(0.208 0.042 265.755);
|
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
|
||||||
--sidebar-border: oklch(0.929 0.013 255.508);
|
--sidebar-border: oklch(0.92 0.004 286.32);
|
||||||
--sidebar-ring: oklch(0.704 0.04 256.788);
|
--sidebar-ring: oklch(0.623 0.214 259.815);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
--background: oklch(0.129 0.042 264.695);
|
--background: oklch(0.141 0.005 285.823);
|
||||||
--foreground: oklch(0.984 0.003 247.858);
|
--foreground: oklch(0.985 0 0);
|
||||||
--card: oklch(0.208 0.042 265.755);
|
--card: oklch(0.21 0.006 285.885);
|
||||||
--card-foreground: oklch(0.984 0.003 247.858);
|
--card-foreground: oklch(0.985 0 0);
|
||||||
--popover: oklch(0.208 0.042 265.755);
|
--popover: oklch(0.21 0.006 285.885);
|
||||||
--popover-foreground: oklch(0.984 0.003 247.858);
|
--popover-foreground: oklch(0.985 0 0);
|
||||||
--primary: oklch(0.929 0.013 255.508);
|
--primary: oklch(0.546 0.245 262.881);
|
||||||
--primary-foreground: oklch(0.208 0.042 265.755);
|
--primary-foreground: oklch(0.379 0.146 265.522);
|
||||||
--secondary: oklch(0.279 0.041 260.031);
|
--secondary: oklch(0.274 0.006 286.033);
|
||||||
--secondary-foreground: oklch(0.984 0.003 247.858);
|
--secondary-foreground: oklch(0.985 0 0);
|
||||||
--muted: oklch(0.279 0.041 260.031);
|
--muted: oklch(0.274 0.006 286.033);
|
||||||
--muted-foreground: oklch(0.704 0.04 256.788);
|
--muted-foreground: oklch(0.705 0.015 286.067);
|
||||||
--accent: oklch(0.279 0.041 260.031);
|
--accent: oklch(0.274 0.006 286.033);
|
||||||
--accent-foreground: oklch(0.984 0.003 247.858);
|
--accent-foreground: oklch(0.985 0 0);
|
||||||
--destructive: oklch(0.704 0.191 22.216);
|
--destructive: oklch(0.704 0.191 22.216);
|
||||||
--border: oklch(1 0 0 / 10%);
|
--border: oklch(1 0 0 / 10%);
|
||||||
--input: oklch(1 0 0 / 15%);
|
--input: oklch(1 0 0 / 15%);
|
||||||
--ring: oklch(0.551 0.027 264.364);
|
--ring: oklch(0.488 0.243 264.376);
|
||||||
--chart-1: oklch(0.488 0.243 264.376);
|
--chart-1: oklch(0.488 0.243 264.376);
|
||||||
--chart-2: oklch(0.696 0.17 162.48);
|
--chart-2: oklch(0.696 0.17 162.48);
|
||||||
--chart-3: oklch(0.769 0.188 70.08);
|
--chart-3: oklch(0.769 0.188 70.08);
|
||||||
--chart-4: oklch(0.627 0.265 303.9);
|
--chart-4: oklch(0.627 0.265 303.9);
|
||||||
--chart-5: oklch(0.645 0.246 16.439);
|
--chart-5: oklch(0.645 0.246 16.439);
|
||||||
--sidebar: oklch(0.208 0.042 265.755);
|
--sidebar: oklch(0.21 0.006 285.885);
|
||||||
--sidebar-foreground: oklch(0.984 0.003 247.858);
|
--sidebar-foreground: oklch(0.985 0 0);
|
||||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
--sidebar-primary: oklch(0.546 0.245 262.881);
|
||||||
--sidebar-primary-foreground: oklch(0.984 0.003 247.858);
|
--sidebar-primary-foreground: oklch(0.379 0.146 265.522);
|
||||||
--sidebar-accent: oklch(0.279 0.041 260.031);
|
--sidebar-accent: oklch(0.274 0.006 286.033);
|
||||||
--sidebar-accent-foreground: oklch(0.984 0.003 247.858);
|
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||||
--sidebar-border: oklch(1 0 0 / 10%);
|
--sidebar-border: oklch(1 0 0 / 10%);
|
||||||
--sidebar-ring: oklch(0.551 0.027 264.364);
|
--sidebar-ring: oklch(0.488 0.243 264.376);
|
||||||
}
|
}
|
||||||
|
|
||||||
@theme inline {
|
@theme inline {
|
||||||
|
|||||||
125
frontend/src/lib/components/grid/cell-header.svelte
Normal file
125
frontend/src/lib/components/grid/cell-header.svelte
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import clsx from 'clsx';
|
||||||
|
|
||||||
|
let {
|
||||||
|
width = '80px',
|
||||||
|
height = '30px',
|
||||||
|
setColWidth = () => {},
|
||||||
|
setRowHeight = () => {},
|
||||||
|
val,
|
||||||
|
active,
|
||||||
|
direction = 'col' // New prop: 'col' for right-side drag, 'row' for bottom-side
|
||||||
|
}: {
|
||||||
|
width?: string;
|
||||||
|
height?: string;
|
||||||
|
setColWidth?: (width: string) => void;
|
||||||
|
setRowHeight?: (height: string) => void;
|
||||||
|
val: string;
|
||||||
|
active: boolean;
|
||||||
|
direction?: 'col' | 'row';
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
// --- Drag Logic ---
|
||||||
|
const handleMouseDown = (startEvent: MouseEvent) => {
|
||||||
|
// Prevent text selection during drag
|
||||||
|
startEvent.preventDefault();
|
||||||
|
|
||||||
|
const target = startEvent.currentTarget as HTMLElement;
|
||||||
|
const parent = target.parentElement!;
|
||||||
|
|
||||||
|
// Store the initial position and size
|
||||||
|
const startX = startEvent.clientX;
|
||||||
|
const startY = startEvent.clientY;
|
||||||
|
const startWidth = parent.offsetWidth;
|
||||||
|
const startHeight = parent.offsetHeight;
|
||||||
|
|
||||||
|
const handleMouseMove = (moveEvent: MouseEvent) => {
|
||||||
|
if (direction === 'col') {
|
||||||
|
const dx = moveEvent.clientX - startX;
|
||||||
|
// Enforce a minimum width of 40px
|
||||||
|
setColWidth(`${Math.max(40, startWidth + dx)}px`);
|
||||||
|
} else {
|
||||||
|
const dy = moveEvent.clientY - startY;
|
||||||
|
// Enforce a minimum height of 20px
|
||||||
|
setRowHeight(`${Math.max(30, startHeight + dy)}px`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseUp = () => {
|
||||||
|
// Cleanup: remove the global listeners
|
||||||
|
window.removeEventListener('mousemove', handleMouseMove);
|
||||||
|
window.removeEventListener('mouseup', handleMouseUp);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add global listeners to track mouse movement anywhere on the page
|
||||||
|
window.addEventListener('mousemove', handleMouseMove);
|
||||||
|
window.addEventListener('mouseup', handleMouseUp);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style:width
|
||||||
|
style:height
|
||||||
|
class={clsx('placeholder group relative bg-background p-1 dark:bg-input/30', { active })}
|
||||||
|
>
|
||||||
|
<span class="pointer-events-none flex h-full w-full items-center justify-center select-none">
|
||||||
|
{val}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div
|
||||||
|
role="separator"
|
||||||
|
aria-label="Resize handle"
|
||||||
|
onmousedown={handleMouseDown}
|
||||||
|
class={clsx('resizer', {
|
||||||
|
'resizer-col': direction === 'col',
|
||||||
|
'resizer-row': direction === 'row'
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.placeholder {
|
||||||
|
font-size: 14px;
|
||||||
|
border: 1px solid var(--input);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active {
|
||||||
|
border: 1px solid var(--color-primary);
|
||||||
|
background-color: color-mix(in oklab, var(--color-primary) 40%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Resizer Styles --- */
|
||||||
|
.resizer {
|
||||||
|
position: absolute;
|
||||||
|
/* Make it easier to grab */
|
||||||
|
z-index: 10;
|
||||||
|
/* Subtle visual cue, becomes more visible on hover */
|
||||||
|
background-color: transparent;
|
||||||
|
transition: background-color 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style for vertical (column) resizing */
|
||||||
|
.resizer-col {
|
||||||
|
cursor: col-resize;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
width: 8px; /* Larger grab area */
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style for horizontal (row) resizing */
|
||||||
|
.resizer-row {
|
||||||
|
cursor: row-resize;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 8px; /* Larger grab area */
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Make the handle visible when hovering over the component */
|
||||||
|
.group:hover > .resizer {
|
||||||
|
background-color: var(--color-primary);
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
90
frontend/src/lib/components/grid/cell.svelte
Normal file
90
frontend/src/lib/components/grid/cell.svelte
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Input } from '$lib/components/ui/input/index.js';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
|
||||||
|
let {
|
||||||
|
width = '80px',
|
||||||
|
height = '30px',
|
||||||
|
raw_val = $bindable(''),
|
||||||
|
val = undefined,
|
||||||
|
onmousedown = () => {},
|
||||||
|
startediting = () => {},
|
||||||
|
stopediting = () => {},
|
||||||
|
active = false,
|
||||||
|
editing = false
|
||||||
|
}: {
|
||||||
|
width?: string;
|
||||||
|
height?: string;
|
||||||
|
raw_val?: string;
|
||||||
|
val?: number | string | undefined;
|
||||||
|
onmousedown?: (e: MouseEvent) => void;
|
||||||
|
startediting?: () => void;
|
||||||
|
stopediting?: () => void;
|
||||||
|
active?: boolean;
|
||||||
|
editing?: boolean;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
// focus the first focusable descendant (the inner <input>)
|
||||||
|
function autofocusWithin(node: HTMLElement) {
|
||||||
|
queueMicrotask(() => {
|
||||||
|
const el = node.querySelector('input') as HTMLInputElement | null;
|
||||||
|
if (el !== null) {
|
||||||
|
el.value = raw_val;
|
||||||
|
el.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleKeydown(e: KeyboardEvent) {
|
||||||
|
if (e.key === 'Enter' || e.key === 'NumpadEnter' || e.key == 'Escape') {
|
||||||
|
e.preventDefault(); // avoid form submit/line break
|
||||||
|
const el = (e.currentTarget as HTMLElement).querySelector('input') as HTMLInputElement | null;
|
||||||
|
el?.blur(); // triggers on:blur below
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if editing}
|
||||||
|
<div use:autofocusWithin onkeydown={handleKeydown}>
|
||||||
|
<Input
|
||||||
|
style="width: {width}; height: {height}"
|
||||||
|
class="relative rounded-none p-1
|
||||||
|
!transition-none delay-0 duration-0
|
||||||
|
focus:z-50"
|
||||||
|
onblur={(e) => {
|
||||||
|
raw_val = (e.target as HTMLInputElement).value;
|
||||||
|
stopediting();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div
|
||||||
|
ondblclick={startediting}
|
||||||
|
{onmousedown}
|
||||||
|
style:width
|
||||||
|
style:height
|
||||||
|
class={clsx('placeholder bg-background p-1 dark:bg-input/30', { active, 'z-50': active })}
|
||||||
|
>
|
||||||
|
{#if raw_val !== '' || val !== ''}
|
||||||
|
<span class="pointer-events-none select-none">
|
||||||
|
{#if val !== undefined}
|
||||||
|
{val}
|
||||||
|
{:else}
|
||||||
|
{raw_val}
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.placeholder {
|
||||||
|
border: 1px solid var(--input);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active {
|
||||||
|
border: 1px solid var(--color-primary);
|
||||||
|
outline: 1px solid var(--color-primary);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,260 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { toast } from 'svelte-sonner';
|
||||||
|
import Cell from '$lib/components/grid/cell.svelte';
|
||||||
|
import { onDestroy, onMount } from 'svelte';
|
||||||
|
import CellHeader from './cell-header.svelte';
|
||||||
|
import { fromGridRef, toColLetter, toGridRef, type CellData, type CellValue } from './utils';
|
||||||
|
|
||||||
|
let {
|
||||||
|
socket
|
||||||
|
}: {
|
||||||
|
socket: WebSocket;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
function splitErrorString(errorString: string) {
|
||||||
|
// Remove the "ERR " prefix.
|
||||||
|
const content = errorString.substring(4);
|
||||||
|
|
||||||
|
// Find the index of the first colon.
|
||||||
|
const colonIndex = content.indexOf(':');
|
||||||
|
|
||||||
|
// If no colon is found, return the whole content as the first element.
|
||||||
|
if (colonIndex === -1) {
|
||||||
|
return [content.trim(), ''];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract the part before the colon (the error type).
|
||||||
|
const errorType = content.substring(0, colonIndex).trim();
|
||||||
|
|
||||||
|
// Extract the part after the colon (the error message).
|
||||||
|
const errorMessage = content.substring(colonIndex + 1).trim();
|
||||||
|
|
||||||
|
return [errorType, errorMessage];
|
||||||
|
}
|
||||||
|
|
||||||
|
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]
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cellRef: 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) {
|
||||||
|
[, cellRef, evalStr] = match;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Case 2: "D6 String("hello")" or "E9 Double(4.0)"
|
||||||
|
if (!match) {
|
||||||
|
match = input.match(/^([A-Z]+\d+)\s+(.+)$/);
|
||||||
|
if (match) {
|
||||||
|
[, cellRef, evalStr] = match;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!cellRef || !evalStr) {
|
||||||
|
console.warn('Unrecognized message:', input);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Cell: ${cellRef}`);
|
||||||
|
console.log(`Eval: ${evalStr}`);
|
||||||
|
|
||||||
|
let [i, j] = fromGridRef(cellRef);
|
||||||
|
|
||||||
|
// Parse eval types
|
||||||
|
if (evalStr.startsWith('Integer(')) {
|
||||||
|
const num = parseInt(evalStr.match(/^Integer\(([-\d]+)\)$/)?.[1] ?? 'NaN', 10);
|
||||||
|
console.log(`Parsed integer:`, num);
|
||||||
|
setCellVal(i, j, num);
|
||||||
|
} else if (evalStr.startsWith('Double(')) {
|
||||||
|
const num = parseFloat(evalStr.match(/^Double\(([-\d.]+)\)$/)?.[1] ?? 'NaN');
|
||||||
|
console.log(`Parsed double:`, num);
|
||||||
|
setCellVal(i, j, num);
|
||||||
|
} else if (evalStr.startsWith('String(')) {
|
||||||
|
const str = evalStr.match(/^String\("(.+)"\)$/)?.[1];
|
||||||
|
console.log(`Parsed string:`, str);
|
||||||
|
setCellVal(i, j, str);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let rows = 50;
|
||||||
|
let cols = 50;
|
||||||
|
|
||||||
|
let default_row_height = '30px';
|
||||||
|
let default_col_width = '60px';
|
||||||
|
|
||||||
|
// Only store touched cells
|
||||||
|
let grid_vals: Record<string, CellData> = $state({});
|
||||||
|
let row_heights: Record<number, string> = $state({});
|
||||||
|
let col_widths: Record<number, string> = $state({});
|
||||||
|
|
||||||
|
function getRowHeight(row: number) {
|
||||||
|
return row_heights[row] ?? default_row_height;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getColWidth(col: number) {
|
||||||
|
return col_widths[col] ?? default_col_width;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setRowHeight(row: number, height: string) {
|
||||||
|
if (height === default_row_height) {
|
||||||
|
delete row_heights[row];
|
||||||
|
} else {
|
||||||
|
row_heights[row] = height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setColWidth(col: number, width: string) {
|
||||||
|
if (width === default_col_width) {
|
||||||
|
delete col_widths[col];
|
||||||
|
} else {
|
||||||
|
col_widths[col] = width;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let active_cell: [number, number] | null = $state(null);
|
||||||
|
let editing_cell: [number, number] | null = $state(null);
|
||||||
|
|
||||||
|
function startEditing(i: number, j: number) {
|
||||||
|
active_cell = [i, j];
|
||||||
|
editing_cell = [i, j];
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopEditing() {
|
||||||
|
editing_cell = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = (i: number, j: number) => `${i}:${j}`;
|
||||||
|
|
||||||
|
const getCell = (i: number, j: number) => grid_vals[key(i, j)] ?? undefined;
|
||||||
|
|
||||||
|
const getCellRaw = (i: number, j: number) => getCell(i, j)?.raw_val ?? '';
|
||||||
|
const setCellRaw = (i: number, j: number, val: string) => {
|
||||||
|
if (grid_vals[key(i, j)] === undefined) {
|
||||||
|
grid_vals[key(i, j)] = {
|
||||||
|
raw_val: val,
|
||||||
|
val: undefined
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
grid_vals[key(i, j)].raw_val = val;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const getCellVal = (i: number, j: number) => getCell(i, j)?.val ?? undefined;
|
||||||
|
const setCellVal = (i: number, j: number, val: CellValue) => {
|
||||||
|
if (grid_vals[key(i, j)] === undefined) {
|
||||||
|
console.warn('Cell raw value was undefined but recieved eval.');
|
||||||
|
} else {
|
||||||
|
grid_vals[key(i, j)].val = val;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const setCell = (i: number, j: 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)];
|
||||||
|
else {
|
||||||
|
setCellRaw(i, j, v);
|
||||||
|
console.log(i, j);
|
||||||
|
socket.send(`set ${toGridRef(i, j)} ${v}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// $effect(() => {
|
||||||
|
// $inspect(grid_vals);
|
||||||
|
// });
|
||||||
|
|
||||||
|
function handleCellInteraction(i: number, j: number, e: MouseEvent) {
|
||||||
|
if (editing_cell) {
|
||||||
|
// Get the actual input element that's being edited
|
||||||
|
const el = document.querySelector<HTMLInputElement>('input:focus');
|
||||||
|
const currentInputValue = el?.value ?? '';
|
||||||
|
|
||||||
|
// ONLY treat this as a reference insert if it's a formula
|
||||||
|
if (currentInputValue.trim().startsWith('=')) {
|
||||||
|
// Prevent the input from losing focus
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
// --- This is the same reference-inserting logic as before ---
|
||||||
|
const ref = toGridRef(i, j);
|
||||||
|
if (el) {
|
||||||
|
const { selectionStart, selectionEnd } = el;
|
||||||
|
const before = el.value.slice(0, selectionStart ?? 0);
|
||||||
|
const after = el.value.slice(selectionEnd ?? 0);
|
||||||
|
el.value = before + ref + after;
|
||||||
|
const newPos = (selectionStart ?? 0) + ref.length;
|
||||||
|
el.setSelectionRange(newPos, newPos);
|
||||||
|
el.dispatchEvent(new Event('input', { bubbles: true }));
|
||||||
|
el.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We are not editing, so this is a normal cell selection OR this is not a formula
|
||||||
|
active_cell = [i, j];
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
const handler = (e: MouseEvent) => {
|
||||||
|
// optional: check if click target is outside grid container
|
||||||
|
if (!(e.target as HTMLElement).closest('.grid-wrapper')) {
|
||||||
|
active_cell = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener('click', handler);
|
||||||
|
onDestroy(() => window.removeEventListener('click', handler));
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="grid-wrapper">
|
||||||
|
<div class="flex w-fit">
|
||||||
|
<CellHeader height={default_row_height} width={default_col_width} val="" active={false} />
|
||||||
|
{#each Array(cols) as _, j}
|
||||||
|
<CellHeader
|
||||||
|
height={default_row_height}
|
||||||
|
width={getColWidth(j)}
|
||||||
|
setColWidth={(width) => setColWidth(j, width)}
|
||||||
|
direction="col"
|
||||||
|
val={toColLetter(j + 1)}
|
||||||
|
active={active_cell !== null && active_cell[1] === j}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{#each Array(rows) as _, i}
|
||||||
|
<div class="flex w-fit">
|
||||||
|
<CellHeader
|
||||||
|
direction="row"
|
||||||
|
width={default_col_width}
|
||||||
|
height={getRowHeight(i)}
|
||||||
|
setRowHeight={(height) => setRowHeight(i, height)}
|
||||||
|
val={(i + 1).toString()}
|
||||||
|
active={active_cell !== null && active_cell[0] === i}
|
||||||
|
/>
|
||||||
|
{#each Array(cols) as _, j}
|
||||||
|
<Cell
|
||||||
|
height={getRowHeight(i)}
|
||||||
|
width={getColWidth(j)}
|
||||||
|
editing={editing_cell?.[0] === i && editing_cell?.[1] === j}
|
||||||
|
startediting={() => startEditing(i, j)}
|
||||||
|
stopediting={stopEditing}
|
||||||
|
onmousedown={(e) => {
|
||||||
|
handleCellInteraction(i, j, e);
|
||||||
|
}}
|
||||||
|
bind:raw_val={() => getCellRaw(i, j), (v) => setCell(i, j, v)}
|
||||||
|
val={getCellVal(i, j)}
|
||||||
|
active={active_cell !== null && active_cell[0] === i && active_cell[1] === j}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|||||||
39
frontend/src/lib/components/grid/utils.ts
Normal file
39
frontend/src/lib/components/grid/utils.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
export function fromGridRef(ref: string): [number, number] {
|
||||||
|
const match = ref.match(/^([A-Z]+)([0-9]+)$/i);
|
||||||
|
if (!match) throw new Error('Invalid reference');
|
||||||
|
|
||||||
|
const [, letters, rowStr] = match;
|
||||||
|
|
||||||
|
let col = 0;
|
||||||
|
for (let i = 0; i < letters.length; i++) {
|
||||||
|
col = col * 26 + (letters.charCodeAt(i) - 64); // 'A' = 65 → 1
|
||||||
|
}
|
||||||
|
|
||||||
|
const row = parseInt(rowStr, 10);
|
||||||
|
return [row - 1, col - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toColLetter(col: number): string {
|
||||||
|
let result = '';
|
||||||
|
let n = col;
|
||||||
|
while (n > 0) {
|
||||||
|
const rem = (n - 1) % 26;
|
||||||
|
result = String.fromCharCode(65 + rem) + result; // 65 = 'A'
|
||||||
|
n = Math.floor((n - 1) / 26);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toGridRef(row: number, col: number): string {
|
||||||
|
row++;
|
||||||
|
col++;
|
||||||
|
return toColLetter(col) + row.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CellValue = number | string | undefined;
|
||||||
|
|
||||||
|
export interface CellData {
|
||||||
|
raw_val: string;
|
||||||
|
val: CellValue;
|
||||||
|
}
|
||||||
1
frontend/src/lib/components/ui/sonner/index.ts
Normal file
1
frontend/src/lib/components/ui/sonner/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { default as Toaster } from "./sonner.svelte";
|
||||||
13
frontend/src/lib/components/ui/sonner/sonner.svelte
Normal file
13
frontend/src/lib/components/ui/sonner/sonner.svelte
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Toaster as Sonner, type ToasterProps as SonnerProps } from "svelte-sonner";
|
||||||
|
import { mode } from "mode-watcher";
|
||||||
|
|
||||||
|
let { ...restProps }: SonnerProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Sonner
|
||||||
|
theme={mode.current}
|
||||||
|
class="toaster group"
|
||||||
|
style="--normal-bg: var(--color-popover); --normal-text: var(--color-popover-foreground); --normal-border: var(--color-border);"
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
@@ -1,12 +1,14 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import '../app.css';
|
import '../app.css';
|
||||||
import { ModeWatcher } from 'mode-watcher';
|
import { ModeWatcher } from 'mode-watcher';
|
||||||
|
import { Toaster } from '$lib/components/ui/sonner/index.js';
|
||||||
import favicon from '$lib/assets/favicon.svg';
|
import favicon from '$lib/assets/favicon.svg';
|
||||||
|
|
||||||
let { children } = $props();
|
let { children } = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ModeWatcher defaultMode="light" />
|
<ModeWatcher defaultMode="light" />
|
||||||
|
<Toaster richColors position="bottom-right" />
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<link rel="icon" href={favicon} />
|
<link rel="icon" href={favicon} />
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|||||||
@@ -5,18 +5,20 @@
|
|||||||
import { Button } from '$lib/components/ui/button/index.js';
|
import { Button } from '$lib/components/ui/button/index.js';
|
||||||
import { toggleMode } from 'mode-watcher';
|
import { toggleMode } from 'mode-watcher';
|
||||||
import Grid from '$lib/components/grid/grid.svelte';
|
import Grid from '$lib/components/grid/grid.svelte';
|
||||||
|
|
||||||
|
let socket = new WebSocket('ws://localhost:7050');
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Button onclick={toggleMode} variant="outline" size="icon">
|
<!-- <Button onclick={toggleMode} variant="outline" size="icon"> -->
|
||||||
<SunIcon
|
<!-- <SunIcon -->
|
||||||
class="h-[1.2rem] w-[1.2rem] scale-100 rotate-0 !transition-all dark:scale-0 dark:-rotate-90"
|
<!-- class="h-[1.2rem] w-[1.2rem] scale-100 rotate-0 !transition-all dark:scale-0 dark:-rotate-90" -->
|
||||||
/>
|
<!-- /> -->
|
||||||
<MoonIcon
|
<!-- <MoonIcon -->
|
||||||
class="absolute h-[1.2rem] w-[1.2rem] scale-0 rotate-90 !transition-all dark:scale-100 dark:rotate-0"
|
<!-- class="absolute h-[1.2rem] w-[1.2rem] scale-0 rotate-90 !transition-all dark:scale-100 dark:rotate-0" -->
|
||||||
/>
|
<!-- /> -->
|
||||||
<span class="sr-only">Toggle theme</span>
|
<!-- <span class="sr-only">Toggle theme</span> -->
|
||||||
</Button>
|
<!-- </Button> -->
|
||||||
|
|
||||||
<div class="h-[80vh] overflow-hidden rounded-lg border">
|
<div class="m-0">
|
||||||
<Grid />
|
<Grid {socket} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user