logic-mesh 0.9.0

Control logic engine using event based and reactive blocks written in Rust.
Documentation
<script lang="ts">
	import type { Snippet } from 'svelte';
	import { tick } from 'svelte';
	import { X } from 'lucide-svelte';
	import { Button } from '$lib/components/ui/button';
	import { model } from '$lib/model.svelte';
	import type { Block } from '$lib/Block';

	interface Props {
		data: Block;
		children: Snippet;
	}

	let { data, children }: Props = $props();

	const isSelected = $derived(
		(model.currentBlock?.data as { value: Block } | undefined)?.value?.id === data.id
	);

	let editing = $state(false);
	let inputEl: HTMLInputElement | undefined = $state(undefined);

	async function startEdit() {
		editing = true;
		await tick();
		inputEl?.focus();
		inputEl?.select();
	}

	function commitEdit(e: Event) {
		const v = (e.target as HTMLInputElement).value.trim();
		data.label = v;
		editing = false;
	}

	function onKey(e: KeyboardEvent) {
		if (e.key === 'Enter') (e.target as HTMLInputElement).blur();
		else if (e.key === 'Escape') {
			editing = false;
		}
	}

	function remove() {
		model.removeBlock(data.id);
	}
</script>

<div
	class="node-container"
	class:node-faulted={data.state === 'fault'}
	title={data.faultReason ?? data.desc.doc}
	style={isSelected ? 'box-shadow: 0 0 0 2px var(--primary);' : ''}
>
	<div class="node-header">
		<span class="node-title-row">
			<span class="node-title" title={data.desc.doc}>{data.desc.name}</span>
			{#if editing}
				<input
					bind:this={inputEl}
					class="node-label-input nodrag"
					type="text"
					value={data.label ?? ''}
					placeholder="add label"
					onblur={commitEdit}
					onkeydown={onKey}
				/>
			{:else}
				<button
					type="button"
					class="node-label nodrag"
					class:node-label-empty={!data.label}
					title="Click to edit label"
					onclick={startEdit}
				>
					{data.label ? data.label : 'add label'}
				</button>
			{/if}
		</span>
		<Button
			variant="ghost"
			size="icon"
			class="h-4 w-4 text-inherit opacity-50 hover:opacity-100"
			onclick={remove}
			aria-label="Remove block"
		>
			<X class="h-3 w-3" />
		</Button>
	</div>
	<div class="node-body">
		{@render children()}
	</div>
</div>

<style>
	.node-container {
		width: 100%;
		border-radius: 5px;
		overflow: hidden;
	}

	.node-faulted {
		box-shadow: 0 0 0 2px #e0405b !important;
	}

	:global(.faulted-edge .svelte-flow__edge-path) {
		stroke: #e0405b !important;
		stroke-width: 2px;
	}

	.node-header {
		display: flex;
		align-items: center;
		justify-content: space-between;
		padding: 2px 6px;
		background: var(--muted);
		border-bottom: 1px solid var(--border);
		gap: 6px;
		min-width: 0;
	}

	.node-title-row {
		display: flex;
		align-items: baseline;
		gap: 6px;
		min-width: 0;
		flex: 1;
	}

	.node-title {
		font-size: 11px;
		font-weight: 600;
		flex-shrink: 0;
	}

	.node-label {
		font-size: 11px;
		font-weight: 400;
		font-style: italic;
		opacity: 0.75;
		background: transparent;
		border: none;
		padding: 0;
		margin: 0;
		cursor: text;
		text-align: left;
		overflow: hidden;
		text-overflow: ellipsis;
		white-space: nowrap;
		min-width: 0;
		max-width: 160px;
	}

	.node-label:hover {
		opacity: 1;
		text-decoration: underline dotted;
		text-underline-offset: 2px;
	}

	.node-label-empty {
		opacity: 0.4;
	}

	.node-label-input {
		font-size: 11px;
		font-style: italic;
		background: transparent;
		border: none;
		border-bottom: 1px solid var(--primary);
		padding: 0;
		margin: 0;
		outline: none;
		min-width: 0;
		max-width: 160px;
		flex: 1;
	}

	.node-title-row :global(.node-label),
	.node-title-row :global(.node-label-input) {
		font-family: inherit;
	}

	.node-body {
		padding: 4px 0;
	}
</style>