logic-mesh 0.9.0

Control logic engine using event based and reactive blocks written in Rust.
Documentation
<script lang="ts">
	import { Handle, Position } from '@xyflow/svelte';
	import BlockCommons from '../BlockCommons.svelte';
	import type { Block } from '$lib/Block';
	import { numericValue } from '$lib/utils';

	interface Props {
		data: { value: Block };
	}

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

	const block = $derived(data.value);

	const value = $derived(numericValue(block.inputs.in?.value) ?? 0);
	const min = $derived(numericValue(block.inputs.min?.value) ?? 0);
	const max = $derived(numericValue(block.inputs.max?.value) ?? 100);
	const label = $derived(String(block.inputs.label?.value ?? ''));

	const pct = $derived.by(() => {
		const span = max - min;
		if (span === 0) return 0;
		return Math.max(0, Math.min(100, ((value - min) / span) * 100));
	});

	const display = $derived(
		Number.isFinite(value)
			? Intl.NumberFormat(undefined, { maximumFractionDigits: 2 }).format(value)
			: '-'
	);
</script>

<BlockCommons data={block}>
	<div class="ui-block-body">
		<div class="pin-stack">
			<div class="pin-row">
				<Handle id="in" type="target" position={Position.Left} class="handle-dot handle-input" />
				<span class="pin-name">in</span>
			</div>
			<div class="pin-row">
				<Handle id="min" type="target" position={Position.Left} class="handle-dot handle-input" />
				<span class="pin-name">min</span>
			</div>
			<div class="pin-row">
				<Handle id="max" type="target" position={Position.Left} class="handle-dot handle-input" />
				<span class="pin-name">max</span>
			</div>
			<div class="pin-row">
				<Handle id="label" type="target" position={Position.Left} class="handle-dot handle-input" />
				<span class="pin-name">label</span>
			</div>
		</div>

		<div class="bar-area">
			<span class="bar-value">{display}</span>
			<div class="bar-track">
				<div class="bar-fill" style:height={`${pct}%`}></div>
			</div>
			{#if label}
				<span class="bar-label">{label}</span>
			{/if}
		</div>
	</div>
</BlockCommons>

<style>
	.ui-block-body {
		display: flex;
		align-items: stretch;
		gap: 10px;
		padding: 6px 10px;
		position: relative;
	}

	.pin-stack {
		display: flex;
		flex-direction: column;
		gap: 2px;
	}

	.pin-row {
		display: flex;
		align-items: center;
		padding: 1px 8px;
		gap: 6px;
		min-height: 18px;
		position: relative;
	}

	.pin-name {
		font-size: 11px;
		opacity: 0.85;
	}

	.bar-area {
		display: flex;
		flex-direction: column;
		align-items: center;
		gap: 4px;
		min-width: 50px;
	}

	.bar-value {
		font-size: 11px;
		font-weight: 600;
	}

	.bar-track {
		width: 14px;
		height: 64px;
		background: var(--muted);
		border: 1px solid var(--border);
		border-radius: 3px;
		display: flex;
		flex-direction: column-reverse;
		overflow: hidden;
	}

	.bar-fill {
		width: 100%;
		background: linear-gradient(180deg, #6bcf7f 0%, #3ecf6b 100%);
		transition: height 150ms ease;
	}

	.bar-label {
		font-size: 10px;
		opacity: 0.75;
		max-width: 100px;
		text-align: center;
		overflow: hidden;
		text-overflow: ellipsis;
		white-space: nowrap;
	}

	:global(.handle-dot) {
		width: 8px !important;
		height: 8px !important;
		border-radius: 50% !important;
		min-width: 0 !important;
		border: 1.5px solid white !important;
	}
	:global(.handle-input) {
		background: #6b9eff !important;
	}
</style>