Merge remote-tracking branch 'origin'
This commit is contained in:
@@ -1,14 +1,16 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::cell::CellRef;
|
use crate::{
|
||||||
use crate::common::LeadErr;
|
cell::CellRef,
|
||||||
use crate::common::LeadErrCode;
|
common::{LeadErr, LeadErrCode, Literal},
|
||||||
use crate::common::Literal;
|
evaluator::utils::*,
|
||||||
use crate::grid::Grid;
|
grid::Grid,
|
||||||
use crate::parser::*;
|
parser::*,
|
||||||
use std::collections::HashSet;
|
};
|
||||||
use std::f64;
|
|
||||||
use std::fmt;
|
use std::{collections::HashSet, f64, fmt};
|
||||||
|
|
||||||
|
mod utils;
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)]
|
#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)]
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
@@ -266,37 +268,6 @@ fn eval_avg(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn eval_single_arg_numeric(
|
|
||||||
args: &Vec<Expr>,
|
|
||||||
precs: &mut HashSet<CellRef>,
|
|
||||||
grid: Option<&Grid>,
|
|
||||||
func: fn(f64) -> f64,
|
|
||||||
func_name: String,
|
|
||||||
) -> Result<Eval, LeadErr> {
|
|
||||||
if args.len() != 1 {
|
|
||||||
return Err(LeadErr {
|
|
||||||
title: "Evaluation error.".into(),
|
|
||||||
desc: format!("{func_name} function requires a single argument."),
|
|
||||||
code: LeadErrCode::Invalid,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let err = LeadErr {
|
|
||||||
title: "Evaluation error.".into(),
|
|
||||||
desc: format!("{func_name} function requires a numeric argument."),
|
|
||||||
code: LeadErrCode::TypeErr,
|
|
||||||
};
|
|
||||||
|
|
||||||
match evaluate_expr(&args[0], precs, grid)? {
|
|
||||||
Eval::Literal(Literal::Number(num)) => Ok(Eval::Literal(Literal::Number(func(num)))),
|
|
||||||
Eval::CellRef { eval, .. } => match *eval {
|
|
||||||
Eval::Literal(Literal::Number(n)) => Ok(Eval::Literal(Literal::Number(func(n)))),
|
|
||||||
_ => Err(err),
|
|
||||||
},
|
|
||||||
_ => Err(err),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn eval_const(args: &Vec<Expr>, value: Eval) -> Result<Eval, LeadErr> {
|
fn eval_const(args: &Vec<Expr>, value: Eval) -> Result<Eval, LeadErr> {
|
||||||
if args.len() != 0 {
|
if args.len() != 0 {
|
||||||
return Err(LeadErr {
|
return Err(LeadErr {
|
||||||
76
backend/src/evaluator/utils.rs
Normal file
76
backend/src/evaluator/utils.rs
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
cell::CellRef,
|
||||||
|
common::{LeadErr, LeadErrCode, Literal},
|
||||||
|
evaluator::{Eval, evaluate_expr},
|
||||||
|
grid::Grid,
|
||||||
|
parser::Expr,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn eval_single_arg_numeric(
|
||||||
|
args: &Vec<Expr>,
|
||||||
|
precs: &mut HashSet<CellRef>,
|
||||||
|
grid: Option<&Grid>,
|
||||||
|
func: fn(f64) -> f64,
|
||||||
|
func_name: String,
|
||||||
|
) -> Result<Eval, LeadErr> {
|
||||||
|
if args.len() != 1 {
|
||||||
|
return Err(LeadErr {
|
||||||
|
title: "Evaluation error.".into(),
|
||||||
|
desc: format!("{func_name} function requires a single argument."),
|
||||||
|
code: LeadErrCode::Invalid,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let err = LeadErr {
|
||||||
|
title: "Evaluation error.".into(),
|
||||||
|
desc: format!("{func_name} function requires a numeric argument."),
|
||||||
|
code: LeadErrCode::TypeErr,
|
||||||
|
};
|
||||||
|
match evaluate_expr(&args[0], precs, grid)? {
|
||||||
|
Eval::Literal(Literal::Number(num)) => Ok(Eval::Literal(Literal::Number(func(num)))),
|
||||||
|
Eval::CellRef { eval, .. } => match *eval {
|
||||||
|
Eval::Literal(Literal::Number(n)) => Ok(Eval::Literal(Literal::Number(func(n)))),
|
||||||
|
_ => Err(err),
|
||||||
|
},
|
||||||
|
_ => Err(err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn eval_n_arg_numeric(
|
||||||
|
n: usize,
|
||||||
|
args: &Vec<Expr>,
|
||||||
|
precs: &mut HashSet<CellRef>,
|
||||||
|
grid: Option<&Grid>,
|
||||||
|
func: fn(Vec<f64>) -> f64,
|
||||||
|
func_name: String,
|
||||||
|
) -> Result<Eval, LeadErr> {
|
||||||
|
if args.len() != n {
|
||||||
|
return Err(LeadErr {
|
||||||
|
title: "Evaluation error.".into(),
|
||||||
|
desc: format!("{func_name} function requires {n} argument(s)."),
|
||||||
|
code: LeadErrCode::Invalid,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let err = LeadErr {
|
||||||
|
title: "Evaluation error.".into(),
|
||||||
|
desc: format!("{func_name} function requires numeric argument(s)."),
|
||||||
|
code: LeadErrCode::TypeErr,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut numbers = Vec::with_capacity(n);
|
||||||
|
|
||||||
|
for arg in args {
|
||||||
|
match evaluate_expr(arg, precs, grid)? {
|
||||||
|
Eval::Literal(Literal::Number(num)) => numbers.push(num),
|
||||||
|
Eval::CellRef { eval, .. } => match *eval {
|
||||||
|
Eval::Literal(Literal::Number(num)) => numbers.push(num),
|
||||||
|
_ => return Err(err.clone()),
|
||||||
|
},
|
||||||
|
_ => return Err(err.clone()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Eval::Literal(Literal::Number(func(numbers))))
|
||||||
|
}
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
# sv
|
|
||||||
|
|
||||||
Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli).
|
|
||||||
|
|
||||||
## Creating a project
|
|
||||||
|
|
||||||
If you're seeing this, you've probably already done this step. Congrats!
|
|
||||||
|
|
||||||
```sh
|
|
||||||
# create a new project in the current directory
|
|
||||||
npx sv create
|
|
||||||
|
|
||||||
# create a new project in my-app
|
|
||||||
npx sv create my-app
|
|
||||||
```
|
|
||||||
|
|
||||||
## Developing
|
|
||||||
|
|
||||||
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
npm run dev
|
|
||||||
|
|
||||||
# or start the server and open the app in a new browser tab
|
|
||||||
npm run dev -- --open
|
|
||||||
```
|
|
||||||
|
|
||||||
## Building
|
|
||||||
|
|
||||||
To create a production version of your app:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
npm run build
|
|
||||||
```
|
|
||||||
|
|
||||||
You can preview the production build with `npm run preview`.
|
|
||||||
|
|
||||||
> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.
|
|
||||||
@@ -3,32 +3,31 @@
|
|||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { getErrDesc, getErrTitle, getEvalLiteral, isErr } from './utils';
|
import { getErrDesc, getErrTitle, getEvalLiteral, isErr } from './utils';
|
||||||
import * as HoverCard from '$lib/components/ui/hover-card/index.js';
|
import * as HoverCard from '$lib/components/ui/hover-card/index.js';
|
||||||
import type { CellT } from './messages';
|
import { Position, type Grid } from './grid.svelte.ts';
|
||||||
|
|
||||||
let {
|
let {
|
||||||
cla = '',
|
cla = '',
|
||||||
width = '80px',
|
pos,
|
||||||
height = '30px',
|
|
||||||
cell = $bindable(undefined),
|
|
||||||
onmousedown = () => {},
|
onmousedown = () => {},
|
||||||
startediting = () => {},
|
grid
|
||||||
stopediting = () => {},
|
|
||||||
active = false,
|
|
||||||
editing = false,
|
|
||||||
externalediting = false
|
|
||||||
}: {
|
}: {
|
||||||
cla?: string;
|
cla?: string;
|
||||||
width?: string;
|
width?: string;
|
||||||
height?: string;
|
height?: string;
|
||||||
cell?: CellT;
|
grid: Grid;
|
||||||
|
pos: Position;
|
||||||
onmousedown?: (e: MouseEvent) => void;
|
onmousedown?: (e: MouseEvent) => void;
|
||||||
startediting?: () => void;
|
|
||||||
stopediting?: () => void;
|
|
||||||
active?: boolean;
|
|
||||||
editing?: boolean;
|
|
||||||
externalediting?: boolean;
|
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
|
let cell = $derived(grid.getCell(pos));
|
||||||
|
let active = $derived(grid.isActive(pos));
|
||||||
|
let primaryactive = $derived(grid.isPrimaryActive(pos));
|
||||||
|
let editing = $derived(grid.isEditing(pos));
|
||||||
|
let externalediting = $derived(grid.isExternalEditing(pos));
|
||||||
|
let width = $derived(grid.getColWidth(pos.col));
|
||||||
|
let height = $derived(grid.getRowHeight(pos.row));
|
||||||
|
let showPreview = $derived(getPreview() !== '');
|
||||||
|
|
||||||
// focus the first focusable descendant (the inner <input>)
|
// focus the first focusable descendant (the inner <input>)
|
||||||
function autofocusWithin(node: HTMLElement) {
|
function autofocusWithin(node: HTMLElement) {
|
||||||
queueMicrotask(() => {
|
queueMicrotask(() => {
|
||||||
@@ -47,22 +46,21 @@
|
|||||||
el?.blur(); // triggers on:blur below
|
el?.blur(); // triggers on:blur below
|
||||||
} else if (e.key == 'Escape') {
|
} else if (e.key == 'Escape') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
stopediting();
|
grid.stopEditing(pos);
|
||||||
|
grid.resetCellTemp(pos);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPreview() {
|
function getPreview() {
|
||||||
return !isErr(cell?.temp_eval) ? getEvalLiteral(cell?.temp_eval) : '';
|
return !isErr(cell?.temp_eval) ? getEvalLiteral(cell?.temp_eval) : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
let showPreview = $derived(getPreview() !== '');
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if editing}
|
{#if editing}
|
||||||
<div class="relative inline-block">
|
<div class="relative inline-block">
|
||||||
{#if showPreview}
|
{#if showPreview}
|
||||||
<h3
|
<h3
|
||||||
class="bubble pointer-events-none absolute -top-[6px] -left-1 z-[500] -translate-y-full text-sm font-semibold tracking-tight text-foreground select-none"
|
class="bubble pointer-events-none absolute top-1/2 left-[2px] z-[500] -translate-y-[calc(50%+2.5em)] text-sm font-semibold tracking-tight text-foreground select-none"
|
||||||
role="tooltip"
|
role="tooltip"
|
||||||
>
|
>
|
||||||
{getPreview()}
|
{getPreview()}
|
||||||
@@ -74,16 +72,16 @@
|
|||||||
style="width: {width}; height: {height}"
|
style="width: {width}; height: {height}"
|
||||||
class="relative rounded-none p-1 !transition-none delay-0 duration-0
|
class="relative rounded-none p-1 !transition-none delay-0 duration-0
|
||||||
focus:z-20 focus:shadow-[0_0_0_1px_var(--color-primary)] focus:outline-none"
|
focus:z-20 focus:shadow-[0_0_0_1px_var(--color-primary)] focus:outline-none"
|
||||||
bind:value={
|
bind:value={() => cell?.temp_raw ?? '', (v) => grid.setCellTemp(pos, v)}
|
||||||
() => cell?.temp_raw ?? '',
|
onblur={() => {
|
||||||
(v) => (cell = { eval: cell?.eval, raw: cell?.raw ?? '', temp_raw: v })
|
grid.stopEditing(cell?.pos);
|
||||||
}
|
grid.setCell(cell?.pos);
|
||||||
onblur={stopediting}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else if cell && isErr(cell.eval)}
|
{:else if cell && isErr(cell.eval)}
|
||||||
<HoverCard.Root openDelay={500} closeDelay={500}>
|
<HoverCard.Root openDelay={500} closeDelay={100}>
|
||||||
<HoverCard.Trigger>
|
<HoverCard.Trigger>
|
||||||
{@render InnerCell()}
|
{@render InnerCell()}
|
||||||
</HoverCard.Trigger>
|
</HoverCard.Trigger>
|
||||||
@@ -100,15 +98,36 @@
|
|||||||
|
|
||||||
{#snippet InnerCell()}
|
{#snippet InnerCell()}
|
||||||
<div
|
<div
|
||||||
ondblclick={startediting}
|
ondblclick={() => grid.startEditing(pos)}
|
||||||
{onmousedown}
|
{onmousedown}
|
||||||
|
data-row={pos.row}
|
||||||
|
data-col={pos.col}
|
||||||
|
ondragstart={(e) => e.preventDefault()}
|
||||||
style:width
|
style:width
|
||||||
style:height
|
style:height
|
||||||
class={clsx('placeholder bg-background p-1', { active }, cla)}
|
class={clsx(
|
||||||
|
'placeholder bg-background p-1',
|
||||||
|
{
|
||||||
|
primaryactive,
|
||||||
|
active,
|
||||||
|
'active-top': grid.isActiveTop(pos),
|
||||||
|
'active-bottom': grid.isActiveBottom(pos),
|
||||||
|
'active-right': grid.isActiveRight(pos),
|
||||||
|
'active-left': grid.isActiveLeft(pos),
|
||||||
|
'only-active': grid.isActive(pos) && grid.isSingleActive()
|
||||||
|
},
|
||||||
|
cla
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{#if cell && (cell.raw !== '' || getEvalLiteral(cell.eval) !== '')}
|
{#if cell && (cell.raw !== '' || getEvalLiteral(cell.eval) !== '')}
|
||||||
<span class={clsx('pointer-events-none select-none', { err: isErr(cell.eval) })}>
|
<span
|
||||||
{#if cell.eval && !externalediting}
|
class={clsx('pointer-events-none select-none', {
|
||||||
|
err: isErr(cell.eval)
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{#if externalediting}
|
||||||
|
{cell.temp_raw}
|
||||||
|
{:else if cell.eval}
|
||||||
{getEvalLiteral(cell.eval)}
|
{getEvalLiteral(cell.eval)}
|
||||||
{:else}
|
{:else}
|
||||||
{cell.raw}
|
{cell.raw}
|
||||||
@@ -126,10 +145,38 @@
|
|||||||
text-overflow: clip;
|
text-overflow: clip;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.primaryactive {
|
||||||
|
z-index: 30 !important;
|
||||||
|
border: 1px solid var(--color-primary) !important;
|
||||||
|
outline: 1px solid var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
.active {
|
.active {
|
||||||
z-index: 20;
|
z-index: 20;
|
||||||
border: 1px solid var(--color-primary);
|
background-color: color-mix(in oklab, var(--color-primary) 20%, var(--color-background) 80%);
|
||||||
outline: 1px solid var(--color-primary);
|
border: 1px solid color-mix(in oklab, var(--input) 100%, var(--color-foreground) 5%);
|
||||||
|
/* outline: 1px solid var(--color-primary); */
|
||||||
|
}
|
||||||
|
|
||||||
|
.only-active {
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Borders for edges */
|
||||||
|
.active-top {
|
||||||
|
border-top: 1px solid var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-bottom {
|
||||||
|
border-bottom: 1px solid var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-left {
|
||||||
|
border-left: 1px solid var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-right {
|
||||||
|
border-right: 1px solid var(--color-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.active:has(.err),
|
.active:has(.err),
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Omega } from '@lucide/svelte';
|
import { EllipsisVertical, Omega } from '@lucide/svelte';
|
||||||
|
import * as Alert from '$lib/components/ui/alert/index.js';
|
||||||
import Cell from '$lib/components/grid/cell.svelte';
|
import Cell from '$lib/components/grid/cell.svelte';
|
||||||
import { onMount } from 'svelte';
|
|
||||||
import CellHeader from './cell-header.svelte';
|
import CellHeader from './cell-header.svelte';
|
||||||
import { colToStr, refToStr } from './utils';
|
import { colToStr, refToStr } from './utils';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { Input } from '../ui/input';
|
import { Input } from '../ui/input';
|
||||||
import type { LeadMsg } from './messages';
|
import type { LeadMsg } from './messages';
|
||||||
import { Grid, Position } from './grid.svelte.ts';
|
import { Grid, Position } from './grid.svelte.ts';
|
||||||
|
import { onDestroy, onMount } from 'svelte';
|
||||||
|
|
||||||
let {
|
let {
|
||||||
socket,
|
socket,
|
||||||
@@ -31,14 +32,16 @@
|
|||||||
grid.handle_msg(res);
|
grid.handle_msg(res);
|
||||||
};
|
};
|
||||||
|
|
||||||
const grid = new Grid(socket);
|
const grid = $state(new Grid(socket));
|
||||||
let rows = 10;
|
let rows = 100;
|
||||||
let cols = 10;
|
let cols = 50;
|
||||||
|
|
||||||
function handleCellInteraction(i: number, j: number, e: MouseEvent) {
|
let dragging = $state(false);
|
||||||
|
|
||||||
|
function handleCellMouseDown(i: number, j: number, e: MouseEvent) {
|
||||||
let pos = new Position(i, j);
|
let pos = new Position(i, j);
|
||||||
|
|
||||||
if (grid.isEditing(pos)) {
|
if (grid.anyIsEditing()) {
|
||||||
// Get the actual input element that's being edited
|
// Get the actual input element that's being edited
|
||||||
const el = document.querySelector<HTMLInputElement>('input:focus');
|
const el = document.querySelector<HTMLInputElement>('input:focus');
|
||||||
const currentInputValue = el?.value ?? '';
|
const currentInputValue = el?.value ?? '';
|
||||||
@@ -66,35 +69,92 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// We are not editing, so this is a normal cell selection OR this is not a formula
|
// We are not editing, so this is a normal cell selection OR this is not a formula
|
||||||
grid.setActive(pos);
|
grid.setActive(pos, pos);
|
||||||
|
dragging = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
// const handler = (e: MouseEvent) => {
|
const handler = (e: MouseEvent) => {
|
||||||
// optional: check if click target is outside grid container
|
// optional: check if click target is outside grid container
|
||||||
// if (!(e.target as HTMLElement).closest('.grid-wrapper')) {
|
if (!(e.target as HTMLElement).closest('.grid-wrapper')) {
|
||||||
// active_cell = null;
|
grid.stopEditingActive();
|
||||||
// }
|
}
|
||||||
// };
|
};
|
||||||
// window.addEventListener('click', handler);
|
|
||||||
// onDestroy(() => window.removeEventListener('click', handler));
|
const handleMouseMove = (e: MouseEvent) => {
|
||||||
|
if (!dragging) return;
|
||||||
|
|
||||||
|
const el = document.elementFromPoint(e.clientX, e.clientY);
|
||||||
|
|
||||||
|
if (el && el instanceof HTMLElement && el.dataset.row && el.dataset.col) {
|
||||||
|
const row = parseInt(el.dataset.row, 10);
|
||||||
|
const col = parseInt(el.dataset.col, 10);
|
||||||
|
|
||||||
|
grid.setActive(grid.primary_active, new Position(row, col));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseUp = (e: MouseEvent) => {
|
||||||
|
dragging = false; // stop tracking
|
||||||
|
//
|
||||||
|
// const el = document.elementFromPoint(e.clientX, e.clientY);
|
||||||
|
//
|
||||||
|
// if (el && el instanceof HTMLElement && el.dataset.row && el.dataset.col) {
|
||||||
|
// const row = parseInt(el.dataset.row, 10);
|
||||||
|
// const col = parseInt(el.dataset.col, 10);
|
||||||
|
//
|
||||||
|
// // expand selection as you drag
|
||||||
|
// let pos = new Position(row, col);
|
||||||
|
//
|
||||||
|
// if (grid.isActive(pos) && grid.isEditing(pos)) return;
|
||||||
|
//
|
||||||
|
// grid.stopAnyEditing();
|
||||||
|
// }
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('click', handler);
|
||||||
|
window.addEventListener('mousemove', handleMouseMove);
|
||||||
|
window.addEventListener('mouseup', handleMouseUp);
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
window.removeEventListener('click', handler);
|
||||||
|
window.removeEventListener('mousemove', handleMouseMove);
|
||||||
|
window.removeEventListener('mouseup', handleMouseUp);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="relative mb-5 ml-5 flex items-center gap-[5px]">
|
<div class="relative mb-5 ml-5 flex items-center gap-[5px]">
|
||||||
|
<Alert.Root
|
||||||
|
class={clsx(
|
||||||
|
'flex h-9 w-fit min-w-[80px] rounded-md border border-input bg-transparent px-2 text-sm font-medium shadow-xs ring-offset-background transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30',
|
||||||
|
'focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50',
|
||||||
|
'flex items-center justify-center aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{grid.getActiveRangeStr()}
|
||||||
|
</Alert.Root>
|
||||||
|
|
||||||
|
<EllipsisVertical class="text-muted-foreground" size="20px" />
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="relative"
|
class="relative"
|
||||||
onkeydown={(e: KeyboardEvent) => {
|
onkeydown={(e: KeyboardEvent) => {
|
||||||
|
const target = e.currentTarget as HTMLElement;
|
||||||
|
const input = target.querySelector('input') as HTMLInputElement | null;
|
||||||
|
|
||||||
if (e.key === 'Enter' || e.key === 'NumpadEnter') {
|
if (e.key === 'Enter' || e.key === 'NumpadEnter') {
|
||||||
e.preventDefault(); // avoid form submit/line break
|
e.preventDefault(); // avoid form submit/line break
|
||||||
const el = (e.currentTarget as HTMLElement).querySelector(
|
|
||||||
'input'
|
grid.stopExternalEdit(grid.getActivePos());
|
||||||
) as HTMLInputElement | null;
|
grid.setCell(grid.getActivePos());
|
||||||
el?.blur(); // triggers on:blur below
|
input?.blur();
|
||||||
} else if (e.key == 'Escape') {
|
} else if (e.key == 'Escape') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
grid.stopEditingActive();
|
grid.stopExternalEdit(grid.getActivePos());
|
||||||
|
grid.resetCellTemp(grid.getActivePos());
|
||||||
|
input?.blur();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -103,24 +163,22 @@
|
|||||||
class="absolute top-1/2 left-2 z-10 -translate-y-1/2 text-muted-foreground"
|
class="absolute top-1/2 left-2 z-10 -translate-y-1/2 text-muted-foreground"
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
onmousedown={() => grid.setExternalEdit(grid.getActivePos())}
|
disabled={grid.getActivePos() === null}
|
||||||
onblur={() => grid.setCell(grid.getActivePos())}
|
onmousedown={() => grid.startExternalEdit(grid.getActivePos())}
|
||||||
|
onblur={() => grid.stopExternalEdit(grid.getActivePos())}
|
||||||
bind:value={
|
bind:value={
|
||||||
() => grid.getActiveCell()?.temp_raw ?? '',
|
() => grid.getActiveCell()?.temp_raw ?? '',
|
||||||
(v) => {
|
(v) => {
|
||||||
grid.setCellTemp(grid.getActivePos(), v);
|
grid.setCellTemp(grid.getActivePos(), v);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class="relative w-[200px] pl-9"
|
class="relative w-fit min-w-[300px] pl-9"
|
||||||
></Input>
|
></Input>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class={clsx(
|
class={clsx(' grid-wrapper relative h-full min-h-0 max-w-full min-w-0 overflow-auto', className)}
|
||||||
' grid-wrapper relative h-full min-h-0 max-w-full min-w-0 overflow-auto overflow-visible',
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
<div class="sticky top-0 flex w-fit" style="z-index: {rows + 70}">
|
<div class="sticky top-0 flex w-fit" style="z-index: {rows + 70}">
|
||||||
<div class="sticky top-0 left-0" style="z-index: {rows + 70}">
|
<div class="sticky top-0 left-0" style="z-index: {rows + 70}">
|
||||||
@@ -141,7 +199,10 @@
|
|||||||
setColWidth={(width) => grid.setColWidth(j, width)}
|
setColWidth={(width) => grid.setColWidth(j, width)}
|
||||||
direction="col"
|
direction="col"
|
||||||
val={colToStr(j)}
|
val={colToStr(j)}
|
||||||
active={grid.getActivePos() !== null && grid.getActivePos()?.col === j}
|
active={grid.primary_active !== null &&
|
||||||
|
grid.secondary_active !== null &&
|
||||||
|
j >= Math.min(grid.primary_active.col, grid.secondary_active.col) &&
|
||||||
|
j <= Math.max(grid.primary_active.col, grid.secondary_active.col)}
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
@@ -154,25 +215,21 @@
|
|||||||
width={grid.getDefaultColWidth()}
|
width={grid.getDefaultColWidth()}
|
||||||
setRowHeight={(height) => grid.setRowHeight(i, height)}
|
setRowHeight={(height) => grid.setRowHeight(i, height)}
|
||||||
val={(i + 1).toString()}
|
val={(i + 1).toString()}
|
||||||
active={grid.getActivePos() !== null && grid.getActivePos()?.row === i}
|
active={grid.primary_active !== null &&
|
||||||
|
grid.secondary_active !== null &&
|
||||||
|
i >= Math.min(grid.primary_active.row, grid.secondary_active.row) &&
|
||||||
|
i <= Math.max(grid.primary_active.row, grid.secondary_active.row)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{#each Array(cols) as _, j}
|
{#each Array(cols) as _, j}
|
||||||
<Cell
|
<Cell
|
||||||
|
{grid}
|
||||||
|
pos={new Position(i, j)}
|
||||||
height={grid.getRowHeight(i)}
|
height={grid.getRowHeight(i)}
|
||||||
width={grid.getColWidth(j)}
|
width={grid.getColWidth(j)}
|
||||||
editing={grid.isEditing(new Position(i, j))}
|
|
||||||
externalediting={grid.isExternalEditing(new Position(i, j))}
|
|
||||||
startediting={() => grid.startEditing(new Position(i, j))}
|
|
||||||
stopediting={() => grid.stopEditing(new Position(i, j))}
|
|
||||||
onmousedown={(e) => {
|
onmousedown={(e) => {
|
||||||
handleCellInteraction(i, j, e);
|
handleCellMouseDown(i, j, e);
|
||||||
}}
|
}}
|
||||||
bind:cell={
|
|
||||||
() => grid.getCell(new Position(i, j)),
|
|
||||||
(v) => grid.setCellTemp(new Position(i, j), v?.temp_raw)
|
|
||||||
}
|
|
||||||
active={grid.isActive(new Position(i, j))}
|
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import type { CellRef, CellT, Eval, LeadMsg } from './messages';
|
import type { CellRef, CellT, Eval, LeadMsg } from './messages';
|
||||||
|
import { refToStr } from './utils';
|
||||||
|
|
||||||
class Position {
|
class Position {
|
||||||
public row: number;
|
public row: number;
|
||||||
@@ -22,6 +23,10 @@ class Position {
|
|||||||
return { row: this.row, col: this.col };
|
return { row: this.row, col: this.col };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public str(): string {
|
||||||
|
return refToStr(this.row, this.col);
|
||||||
|
}
|
||||||
|
|
||||||
public equals(other: CellRef | null | undefined): boolean {
|
public equals(other: CellRef | null | undefined): boolean {
|
||||||
return !!other && this.row === other.row && this.col === other.col;
|
return !!other && this.row === other.row && this.col === other.col;
|
||||||
}
|
}
|
||||||
@@ -38,7 +43,8 @@ class Grid {
|
|||||||
col_widths: Record<number, string> = $state({});
|
col_widths: Record<number, string> = $state({});
|
||||||
default_row_height: string;
|
default_row_height: string;
|
||||||
default_col_width: string;
|
default_col_width: string;
|
||||||
active_cell: Position | null = $state(null);
|
primary_active: Position | null = $state(null);
|
||||||
|
secondary_active: Position | null = $state(null);
|
||||||
editing_cell: Position | null = $state(null);
|
editing_cell: Position | null = $state(null);
|
||||||
external_editing_cell: Position | null = $state(null);
|
external_editing_cell: Position | null = $state(null);
|
||||||
editing_preview: [Eval, boolean] | null = $state(null); // [Eval, dirty]
|
editing_preview: [Eval, boolean] | null = $state(null); // [Eval, dirty]
|
||||||
@@ -53,8 +59,8 @@ class Grid {
|
|||||||
return this.data[pos.key()];
|
return this.data[pos.key()];
|
||||||
}
|
}
|
||||||
|
|
||||||
public setCell(pos: Position | null) {
|
public setCell(pos: Position | null | undefined) {
|
||||||
if (pos === null) return;
|
if (pos === null || pos === undefined) return;
|
||||||
let data = this.data[pos.key()];
|
let data = this.data[pos.key()];
|
||||||
if (data === undefined) return;
|
if (data === undefined) return;
|
||||||
|
|
||||||
@@ -83,6 +89,7 @@ class Grid {
|
|||||||
this.data[pos.key()] = {
|
this.data[pos.key()] = {
|
||||||
raw: x?.raw ?? '',
|
raw: x?.raw ?? '',
|
||||||
temp_raw: raw,
|
temp_raw: raw,
|
||||||
|
pos: pos,
|
||||||
eval: x?.eval ?? undefined,
|
eval: x?.eval ?? undefined,
|
||||||
temp_eval: x?.temp_eval ?? undefined
|
temp_eval: x?.temp_eval ?? undefined
|
||||||
};
|
};
|
||||||
@@ -90,6 +97,20 @@ class Grid {
|
|||||||
this.quickEval(pos, raw);
|
this.quickEval(pos, raw);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public resetCellTemp(pos: Position | null | undefined) {
|
||||||
|
if (!pos) return;
|
||||||
|
|
||||||
|
let x = this.data[pos.key()];
|
||||||
|
|
||||||
|
this.data[pos.key()] = {
|
||||||
|
raw: x?.raw ?? '',
|
||||||
|
pos: pos,
|
||||||
|
temp_raw: x?.raw ?? '',
|
||||||
|
eval: x?.eval ?? undefined,
|
||||||
|
temp_eval: undefined
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
public getRowHeight(row: number) {
|
public getRowHeight(row: number) {
|
||||||
return this.row_heights[row] ?? this.default_row_height;
|
return this.row_heights[row] ?? this.default_row_height;
|
||||||
}
|
}
|
||||||
@@ -105,6 +126,30 @@ class Grid {
|
|||||||
return this.default_row_height;
|
return this.default_row_height;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public isActiveTop(pos: Position): boolean {
|
||||||
|
const tl = this.getActiveTopLeft();
|
||||||
|
if (!tl) return false;
|
||||||
|
return this.isActive(pos) && pos.row === tl.row;
|
||||||
|
}
|
||||||
|
|
||||||
|
public isActiveBottom(pos: Position): boolean {
|
||||||
|
const br = this.getActiveBottomRight();
|
||||||
|
if (!br) return false;
|
||||||
|
return this.isActive(pos) && pos.row === br.row;
|
||||||
|
}
|
||||||
|
|
||||||
|
public isActiveLeft(pos: Position): boolean {
|
||||||
|
const tl = this.getActiveTopLeft();
|
||||||
|
if (!tl) return false;
|
||||||
|
return this.isActive(pos) && pos.col === tl.col;
|
||||||
|
}
|
||||||
|
|
||||||
|
public isActiveRight(pos: Position): boolean {
|
||||||
|
const br = this.getActiveBottomRight();
|
||||||
|
if (!br) return false;
|
||||||
|
return this.isActive(pos) && pos.col === br.col;
|
||||||
|
}
|
||||||
|
|
||||||
public setRowHeight(row: number, height: string) {
|
public setRowHeight(row: number, height: string) {
|
||||||
if (height === this.default_row_height) {
|
if (height === this.default_row_height) {
|
||||||
delete this.row_heights[row];
|
delete this.row_heights[row];
|
||||||
@@ -121,8 +166,10 @@ class Grid {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public startEditing(pos: Position) {
|
public startEditing(pos: Position | undefined) {
|
||||||
this.active_cell = pos;
|
if (!pos) return;
|
||||||
|
|
||||||
|
this.setActive(pos, pos);
|
||||||
this.editing_cell = pos;
|
this.editing_cell = pos;
|
||||||
|
|
||||||
let cell = this.getCell(pos);
|
let cell = this.getCell(pos);
|
||||||
@@ -130,52 +177,139 @@ class Grid {
|
|||||||
cell.temp_eval = undefined;
|
cell.temp_eval = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
public stopEditing(pos: Position) {
|
public stopEditing(pos: Position | null | undefined) {
|
||||||
|
if (!pos) return;
|
||||||
|
this.editing_cell = null;
|
||||||
|
// this.setCell(pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
public stopAnyEditing() {
|
||||||
this.editing_cell = null;
|
this.editing_cell = null;
|
||||||
this.setCell(pos);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public stopEditingActive() {
|
public stopEditingActive() {
|
||||||
if (this.active_cell == null) return;
|
if (!this.anyIsActive() || !this.primary_active?.equals(this.secondary_active)) return;
|
||||||
this.stopEditing(this.active_cell);
|
this.stopEditing(this.primary_active);
|
||||||
}
|
}
|
||||||
|
|
||||||
public isEditing(pos: Position): boolean {
|
public isEditing(pos: Position): boolean {
|
||||||
if (this.editing_cell == null) return false;
|
if (this.editing_cell === null) return false;
|
||||||
return this.editing_cell.equals(pos);
|
return this.editing_cell.equals(pos);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public anyIsEditing(): boolean {
|
||||||
|
return this.editing_cell !== null;
|
||||||
|
}
|
||||||
|
|
||||||
public isExternalEditing(pos: Position): boolean {
|
public isExternalEditing(pos: Position): boolean {
|
||||||
if (this.external_editing_cell == null) return false;
|
if (this.external_editing_cell === null) return false;
|
||||||
return this.external_editing_cell.equals(pos);
|
return this.external_editing_cell.equals(pos);
|
||||||
}
|
}
|
||||||
|
|
||||||
public setActive(pos: Position | null) {
|
public setActive(primary: Position | null, secondary: Position | null) {
|
||||||
this.active_cell = pos;
|
this.primary_active = primary;
|
||||||
|
this.secondary_active = secondary;
|
||||||
}
|
}
|
||||||
|
|
||||||
public setExternalEdit(pos: Position | null) {
|
public setInactive() {
|
||||||
|
this.primary_active = null;
|
||||||
|
this.secondary_active = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public startExternalEdit(pos: Position | null) {
|
||||||
this.external_editing_cell = pos;
|
this.external_editing_cell = pos;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public stopExternalEdit(pos: Position | null) {
|
||||||
|
this.external_editing_cell = null;
|
||||||
|
}
|
||||||
|
|
||||||
public getActiveCell(): CellT | undefined {
|
public getActiveCell(): CellT | undefined {
|
||||||
if (this.active_cell === null)
|
if (this.primary_active === null || this.secondary_active === null) {
|
||||||
return {
|
return {
|
||||||
raw: '',
|
raw: '',
|
||||||
temp_raw: '',
|
temp_raw: '',
|
||||||
|
pos: new Position(-1, -1),
|
||||||
eval: undefined
|
eval: undefined
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return this.getCell(this.active_cell);
|
if (!this.primary_active.equals(this.secondary_active)) {
|
||||||
|
return {
|
||||||
|
raw: '',
|
||||||
|
temp_raw: '',
|
||||||
|
pos: new Position(-1, -1),
|
||||||
|
eval: undefined
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.getCell(this.primary_active);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getActiveRangeStr(): string {
|
||||||
|
const tl = this.getActiveTopLeft();
|
||||||
|
const br = this.getActiveBottomRight();
|
||||||
|
|
||||||
|
if (tl === null || br === null) return '';
|
||||||
|
|
||||||
|
// Single-cell selection
|
||||||
|
if (tl.equals(br)) return tl.str();
|
||||||
|
|
||||||
|
// Range selection
|
||||||
|
return `${tl.str()}:${br.str()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getActivePos(): Position | null {
|
public getActivePos(): Position | null {
|
||||||
return this.active_cell;
|
if (
|
||||||
|
this.primary_active === null ||
|
||||||
|
this.secondary_active === null ||
|
||||||
|
!this.primary_active.equals(this.secondary_active)
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return this.primary_active;
|
||||||
}
|
}
|
||||||
|
|
||||||
public isActive(pos: Position): boolean {
|
public isActive(pos: Position): boolean {
|
||||||
if (this.active_cell == null) return false;
|
if (this.primary_active === null || this.secondary_active === null) return false;
|
||||||
return this.active_cell.equals(pos);
|
|
||||||
|
return (
|
||||||
|
pos.row >= Math.min(this.primary_active.row, this.secondary_active.row) &&
|
||||||
|
pos.row <= Math.max(this.primary_active.row, this.secondary_active.row) &&
|
||||||
|
pos.col >= Math.min(this.primary_active.col, this.secondary_active.col) &&
|
||||||
|
pos.col <= Math.max(this.primary_active.col, this.secondary_active.col)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getActiveTopLeft(): Position | null {
|
||||||
|
if (this.primary_active === null || this.secondary_active === null) return null;
|
||||||
|
|
||||||
|
return new Position(
|
||||||
|
Math.min(this.primary_active.row, this.secondary_active.row),
|
||||||
|
Math.min(this.primary_active.col, this.secondary_active.col)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getActiveBottomRight(): Position | null {
|
||||||
|
if (this.primary_active === null || this.secondary_active === null) return null;
|
||||||
|
|
||||||
|
return new Position(
|
||||||
|
Math.max(this.primary_active.row, this.secondary_active.row),
|
||||||
|
Math.max(this.primary_active.col, this.secondary_active.col)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public isPrimaryActive(pos: Position): boolean {
|
||||||
|
if (this.primary_active === null) return false;
|
||||||
|
return this.primary_active.equals(pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
public isSingleActive(): boolean {
|
||||||
|
return this.getActivePos() !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public anyIsActive(): boolean {
|
||||||
|
return this.primary_active !== null && this.secondary_active !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public quickEval(pos: Position | null, raw: string) {
|
public quickEval(pos: Position | null, raw: string) {
|
||||||
@@ -214,6 +348,7 @@ class Grid {
|
|||||||
this.data[pos.key()] = {
|
this.data[pos.key()] = {
|
||||||
raw: msg.raw ?? '',
|
raw: msg.raw ?? '',
|
||||||
eval: msg.eval,
|
eval: msg.eval,
|
||||||
|
pos: pos,
|
||||||
temp_raw: x?.temp_raw ?? '',
|
temp_raw: x?.temp_raw ?? '',
|
||||||
temp_eval: x?.temp_eval ?? undefined
|
temp_eval: x?.temp_eval ?? undefined
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import type { Position } from "./grid.svelte.ts";
|
||||||
|
|
||||||
interface LeadMsg {
|
interface LeadMsg {
|
||||||
msg_type: 'set' | 'get' | 'error' | 'bulk' | 'eval';
|
msg_type: 'set' | 'get' | 'error' | 'bulk' | 'eval';
|
||||||
cell?: CellRef;
|
cell?: CellRef;
|
||||||
@@ -48,6 +50,7 @@ type Eval =
|
|||||||
interface CellT {
|
interface CellT {
|
||||||
raw: string;
|
raw: string;
|
||||||
temp_raw: string;
|
temp_raw: string;
|
||||||
|
pos: Position;
|
||||||
temp_eval?: Eval;
|
temp_eval?: Eval;
|
||||||
eval?: Eval;
|
eval?: Eval;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="alert-description"
|
||||||
|
class={cn(
|
||||||
|
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
20
frontend/src/lib/components/ui/alert/alert-title.svelte
Normal file
20
frontend/src/lib/components/ui/alert/alert-title.svelte
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="alert-title"
|
||||||
|
class={cn("col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
44
frontend/src/lib/components/ui/alert/alert.svelte
Normal file
44
frontend/src/lib/components/ui/alert/alert.svelte
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<script lang="ts" module>
|
||||||
|
import { type VariantProps, tv } from "tailwind-variants";
|
||||||
|
|
||||||
|
export const alertVariants = tv({
|
||||||
|
base: "relative grid w-full grid-cols-[0_1fr] items-start gap-y-0.5 rounded-lg border px-4 py-3 text-sm has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] has-[>svg]:gap-x-3 [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-card text-card-foreground",
|
||||||
|
destructive:
|
||||||
|
"text-destructive bg-card *:data-[slot=alert-description]:text-destructive/90 [&>svg]:text-current",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export type AlertVariant = VariantProps<typeof alertVariants>["variant"];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
variant = "default",
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
|
||||||
|
variant?: AlertVariant;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="alert"
|
||||||
|
class={cn(alertVariants({ variant }), className)}
|
||||||
|
{...restProps}
|
||||||
|
role="alert"
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
14
frontend/src/lib/components/ui/alert/index.ts
Normal file
14
frontend/src/lib/components/ui/alert/index.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import Root from "./alert.svelte";
|
||||||
|
import Description from "./alert-description.svelte";
|
||||||
|
import Title from "./alert-title.svelte";
|
||||||
|
export { alertVariants, type AlertVariant } from "./alert.svelte";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
Description,
|
||||||
|
Title,
|
||||||
|
//
|
||||||
|
Root as Alert,
|
||||||
|
Description as AlertDescription,
|
||||||
|
Title as AlertTitle,
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user