This commit is contained in:
2025-09-10 02:17:25 +10:00
parent c88d1965c3
commit 2c058a654f
11 changed files with 236 additions and 63 deletions

View File

@@ -1,7 +1,8 @@
<script lang="ts">
import { Input } from '$lib/components/ui/input/index.js';
import clsx from 'clsx';
import type { CellT } from './utils';
import { getErrDesc, getErrTitle, getEvalLiteral, isErr, type CellT } from './utils';
import * as HoverCard from '$lib/components/ui/hover-card/index.js';
let {
cla = '',
@@ -56,15 +57,30 @@
focus:z-20 focus:shadow-[0_0_0_1px_var(--color-primary)] focus:outline-none"
onblur={(e) => {
cell = {
isErr: false,
val: undefined,
val: cell?.val,
raw_val: (e.target as HTMLInputElement).value
};
stopediting();
}}
/>
</div>
{:else if cell && isErr(cell.val)}
<HoverCard.Root openDelay={500} closeDelay={500}>
<HoverCard.Trigger>
{@render InnerCell()}
</HoverCard.Trigger>
<HoverCard.Content side="right">
<h2 class="text-md font-semibold tracking-tight transition-colors">
{getErrTitle(cell.val)}
</h2>
{getErrDesc(cell.val)}
</HoverCard.Content>
</HoverCard.Root>
{:else}
{@render InnerCell()}
{/if}
{#snippet InnerCell()}
<div
ondblclick={startediting}
{onmousedown}
@@ -72,17 +88,17 @@
style:height
class={clsx('placeholder bg-background p-1', { active }, cla)}
>
{#if cell && (cell.raw_val !== '' || cell.val !== '')}
<span class={clsx('pointer-events-none select-none', { err: cell.isErr })}>
{#if cell && (cell.raw_val !== '' || getEvalLiteral(cell.val) !== '')}
<span class={clsx('pointer-events-none select-none', { err: isErr(cell.val) })}>
{#if cell.val}
{cell.val}
{getEvalLiteral(cell.val)}
{:else}
{cell.raw_val}
{/if}
</span>
{/if}
</div>
{/if}
{/snippet}
<style>
.placeholder {
@@ -101,6 +117,7 @@
.active:has(.err),
.placeholder:has(.err) {
position: relative; /* needed for absolute positioning */
color: red;
}
.active:has(.err)::after,

View File

@@ -1,10 +1,12 @@
<script lang="ts">
import { toast } from 'svelte-sonner';
import { Pencil } from '@lucide/svelte';
import Cell from '$lib/components/grid/cell.svelte';
import { onDestroy, onMount } from 'svelte';
import CellHeader from './cell-header.svelte';
import { colToStr, getEvalLiteral, isErr, refToStr, type CellT } from './utils';
import { colToStr, refToStr, type CellT } from './utils';
import clsx from 'clsx';
import { Input } from '../ui/input';
let {
socket,
@@ -44,7 +46,12 @@
console.error('Expected cell value for SET msgponse from server.');
return;
}
setCellVal(msg.cell.row, msg.cell.col, getEvalLiteral(msg.eval), isErr(msg.eval));
grid_vals[key(msg.cell.row, msg.cell.col)] = {
raw_val: msg.raw ?? '',
val: msg.eval
};
break;
}
case 'bulk': {
@@ -108,44 +115,24 @@
const key = (i: number, j: number) => `${i}:${j}`;
const getCell = (i: number, j: number) => grid_vals[key(i, j)];
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,
isErr: false,
val: undefined
};
} else {
grid_vals[key(i, j)].raw_val = val;
const setCell = (row: number, col: number, v: CellT) => {
if (v?.raw_val == null || v.raw_val === '') {
delete grid_vals[key(row, col)];
return;
}
};
const getCellVal = (i: number, j: number) => getCell(i, j);
const setCellVal = (i: number, j: number, val: LiteralValue, isErr: boolean) => {
if (grid_vals[key(i, j)] === undefined) {
console.warn('Cell raw value was undefined but recieved eval.');
} else {
let cell = grid_vals[key(i, j)];
cell.val = val;
cell.isErr = isErr;
}
};
const setCell = (row: number, col: number, v: CellT | undefined) => {
// ignore “no value” so we dont create keys on mount
if (v?.raw_val == null || v.raw_val === '') delete grid_vals[key(row, col)];
else {
setCellRaw(row, col, v.raw_val);
grid_vals[key(row, col)] = {
raw_val: v.raw_val,
val: v.val
};
let msg: LeadMsg = {
msg_type: 'set',
cell: { row, col },
raw: v.raw_val
};
let msg: LeadMsg = {
msg_type: 'set',
cell: { row, col },
raw: v.raw_val
};
socket.send(JSON.stringify(msg));
}
socket.send(JSON.stringify(msg));
};
function handleCellInteraction(i: number, j: number, e: MouseEvent) {
@@ -180,6 +167,33 @@
active_cell = [i, j];
}
function getActiveCell(): CellT {
if (active_cell != null && grid_vals[key(active_cell[0], active_cell[1])])
return grid_vals[key(active_cell[0], active_cell[1])];
else
return {
raw_val: '',
val: undefined
};
}
function setActiveCellRaw(raw: string): void {
if (active_cell == null) return;
if (grid_vals[key(active_cell[0], active_cell[1])]) {
let cell = grid_vals[key(active_cell[0], active_cell[1])];
setCell(active_cell[0], active_cell[1], {
raw_val: raw,
val: cell.val
});
} else {
setCell(active_cell[0], active_cell[1], {
raw_val: raw,
val: undefined
});
}
}
onMount(() => {
const handler = (e: MouseEvent) => {
// optional: check if click target is outside grid container
@@ -192,6 +206,15 @@
});
</script>
<div class="mb-5 ml-5 flex items-center gap-5">
<Pencil />
<Input
bind:value={() => getActiveCell().raw_val, (raw) => setActiveCellRaw(raw)}
class="relative w-[200px] 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"
></Input>
</div>
<div
class={clsx('grid-wrapper relative h-full min-h-0 max-w-full min-w-0 overflow-auto', className)}
>

View File

@@ -1,7 +1,6 @@
export interface CellT {
raw_val: string;
val: LiteralValue | undefined;
isErr: boolean;
val?: Eval;
}
/**
@@ -44,16 +43,38 @@ export function refToStr(row: number, col: number): string {
return colToStr(col) + (row + 1).toString();
}
export function getEvalLiteral(value: Eval): LiteralValue {
export function getEvalLiteral(value: Eval | undefined): LiteralValue {
if (value === undefined) return '';
if (value === 'unset') return '';
if ('literal' in value) return value.literal.value;
if ('literal' in value) {
if (value.literal.value == null) return 'NaN';
return value.literal.value;
}
if ('cellref' in value) return getEvalLiteral(value.cellref.eval);
if ('err' in value) return `err: ${value.err.code}`;
if ('err' in value) return `#${value.err.code.toUpperCase()}`;
// if ('range' in value) return 'err';
return 'todo!';
}
export function isErr(value: Eval): boolean {
export function isErr(value: Eval | undefined): boolean {
if (value === undefined) return false;
if (value === 'unset') return false;
if ('cellref' in value) return isErr(value.cellref.eval);
return 'err' in value;
}
export function getErrTitle(value: Eval | undefined): string {
if (value === undefined) return '';
if (value === 'unset') return '';
if ('cellref' in value) return getErrTitle(value.cellref.eval);
if (!('err' in value)) return '';
return value.err.title;
}
export function getErrDesc(value: Eval | undefined): string {
if (value === undefined) return '';
if (value === 'unset') return '';
if ('cellref' in value) return getErrDesc(value.cellref.eval);
if (!('err' in value)) return '';
return value.err.desc;
}

View File

@@ -0,0 +1,29 @@
<script lang="ts">
import { LinkPreview as HoverCardPrimitive } from 'bits-ui';
import { cn } from '$lib/utils.js';
let {
ref = $bindable(null),
class: className,
align = 'center',
sideOffset = 4,
portalProps,
...restProps
}: HoverCardPrimitive.ContentProps & {
portalProps?: HoverCardPrimitive.PortalProps;
} = $props();
</script>
<HoverCardPrimitive.Portal {...portalProps}>
<HoverCardPrimitive.Content
bind:ref
data-slot="hover-card-content"
{align}
{sideOffset}
class={cn(
'z-[200] mt-3 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-hidden outline-none data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
className
)}
{...restProps}
/>
</HoverCardPrimitive.Portal>

View File

@@ -0,0 +1,7 @@
<script lang="ts">
import { LinkPreview as HoverCardPrimitive } from "bits-ui";
let { ref = $bindable(null), ...restProps }: HoverCardPrimitive.TriggerProps = $props();
</script>
<HoverCardPrimitive.Trigger bind:ref data-slot="hover-card-trigger" {...restProps} />

View File

@@ -0,0 +1,14 @@
import { LinkPreview as HoverCardPrimitive } from "bits-ui";
import Content from "./hover-card-content.svelte";
import Trigger from "./hover-card-trigger.svelte";
const Root = HoverCardPrimitive.Root;
export {
Root,
Content,
Trigger,
Root as HoverCard,
Content as HoverCardContent,
Trigger as HoverCardTrigger,
};

View File

@@ -21,7 +21,9 @@
<div class="absolute left-0 min-h-0 w-full">
<div class="flex h-[100vh] flex-col">
<div class="h-[60px] w-full p-3">
<Sidebar.Trigger />
<div class="flex items-center gap-5">
<Sidebar.Trigger />
</div>
</div>
<div class="grid-wrapper min-h-0 w-full flex-1">
@@ -32,6 +34,6 @@
<style>
.grid-wrapper {
border-top: 2px solid var(--color-input);
/* border-top: 2px solid var(--color-input); */
}
</style>