feat: implement node sockets ui
📊 Benchmark the Runtime / benchmark (pull_request) Successful in 1m22s
🚀 Lint & Test & Deploy / quality (pull_request) Failing after 1m6s
🚀 Lint & Test & Deploy / test-unit (pull_request) Failing after 43s
🚀 Lint & Test & Deploy / test-e2e (pull_request) Successful in 2m5s
🚀 Lint & Test & Deploy / deploy (pull_request) Has been skipped

This commit is contained in:
2026-05-05 21:07:57 +02:00
parent ed11195327
commit 7f082ad8f6
12 changed files with 331 additions and 40 deletions
+1 -1
View File
@@ -9,7 +9,7 @@
@source inline("{hover:,}{bg-,outline-,text-,}layer-3{/30,/50,}");
@source inline("{hover:,}{bg-,outline-,text-,}active");
@source inline("{hover:,}{bg-,outline-,text-,}selected");
@source inline("{hover:,}{bg-,outline-,text-,}outline{!,}");
@source inline("{hover:,}{bg-,outline-,text-,border-,divide-}outline{!,}");
@source inline("{hover:,}{bg-,outline-,text-,}connection");
@source inline("{hover:,}{bg-,outline-,text-,}text");
+1
View File
@@ -5,6 +5,7 @@ export { default as InputNumber } from './inputs/InputNumber.svelte';
export { default as InputSelect } from './inputs/InputSelect.svelte';
export { default as InputShape } from './inputs/InputShape.svelte';
export { default as InputVec3 } from './inputs/InputVec3.svelte';
export { default as SocketTable } from './inputs/SocketTable.svelte';
export { default as Details } from './Details.svelte';
export { default as JsonViewer } from './JsonViewer.svelte';
@@ -0,0 +1,118 @@
<script lang="ts">
import type { NodeInput } from '@nodarium/types';
type Props = {
inputs?: Record<string, NodeInput>;
colors: Record<string, string>;
onremove?: (key: string) => void;
types: string[];
};
let { inputs = $bindable(), onremove, colors = {}, types = ['seed', 'float', 'path'] }: Props =
$props();
let potentialRow = $state<
{
type: string;
label: string;
} | undefined
>();
function showPotentialRow() {
potentialRow = {
type: types[0],
label: 'Input ' + Object.keys(inputs ?? {}).length
};
}
function realizePotentialRow() {
if (inputs) inputs[`input_${Object.keys(inputs).length}`] = potentialRow as NodeInput;
potentialRow = undefined;
}
function removeRow(key?: string) {
if (!key) {
potentialRow = undefined;
} else if (inputs) {
onremove?.(key);
}
}
function getColor(type: string) {
if (type in colors) {
return colors[type];
}
return '#f00';
}
</script>
{#snippet row(input: { type: string; label?: string }, remove: () => void, add?: () => void)}
<div class="flex min-w-0">
<span
style:background={getColor(input.type)}
data-type={input.type}
class="block opacity-50 min-w-2 ml-2 w-2 h-2 my-auto rounded-sm"
></span>
<select
class="text-[0.9em] border-r w-19 shrink-0 px-2 py-1 border-outline"
bind:value={input.type}
>
{#each types as type (type)}
<option>
<span
style="background: {getColor(type)}; width: 5px; height: 5px; border-radius: 5px;"
></span>
{type}
</option>
{/each}
</select>
<input
class="px-2 grow min-w-30 border-r border-outline text-[0.9em]"
type="text"
bind:value={input.label}
/>
<button
class="px-2 cursor-pointer opacity-50 hover:opacity-100 hover:bg-red-400"
onclick={remove}
aria-label="remove"
>
{#if add}
<span class="py-1 block i-[tabler--cancel]"></span>
{:else}
<span class="py-1 block i-[tabler--trash]"></span>
{/if}
</button>
{#if add}
<button
class="px-2 border-l hover:bg-green-300 opacity-50 hover:opacity-100 hover:text-layer-1 border-outline cursor-pointer"
onclick={add}
aria-label="add"
>
<span class="py-1 block i-[tabler--circle-plus]"></span>
</button>
{/if}
</div>
{/snippet}
<div class="rounded-sm overflow-hidden bg-layer-2 divide-y divide-outline outline-1 outline-outline">
{#each Object.entries(inputs ?? {}) as [key, input] (key)}
{@render row(input, () => removeRow(key))}
{/each}
{#if potentialRow}
<div class="opacity-80">
{@render row(potentialRow, () => removeRow(), () => realizePotentialRow())}
</div>
{:else}
<div class="opacity-40">
<div class="flex h-[27px]">
<div class="flex-1"></div>
<button
class="border-l hover:bg-green-300 hover:text-layer-1 border-outline py-1 px-2 cursor-pointer"
onclick={() => showPotentialRow()}
aria-label="remove"
>
<span class="block i-[tabler--circle-plus]"></span>
</button>
</div>
</div>
{/if}
</div>
+24
View File
@@ -11,6 +11,7 @@
JsonViewer,
ShortCut
} from '$lib';
import SocketTable from '$lib/inputs/SocketTable.svelte';
import Section from './Section.svelte';
import Theme from './Theme.svelte';
import ThemeSelector from './ThemeSelector.svelte';
@@ -38,6 +39,17 @@
settings: { seed: 42, enabled: true }
});
let socketTypes = $state({
input_0: {
'label': 'Input 0',
'type': 'path'
},
input_1: {
'label': 'Input 1',
'type': 'float'
}
});
function randomlyUpdateJson() {
const rand = Math.floor(Math.random() * 5);
if (rand === 0) {
@@ -150,6 +162,18 @@
</div>
</Section>
<Section title="Socket Table">
<SocketTable
colors={{
seed: '#f00',
float: '#0f0',
path: '#00f'
}}
types={['seed', 'float', 'path']}
bind:inputs={socketTypes}
/>
</Section>
<Section title="Shortcut">
<div class="flex gap-4">
<ShortCut ctrl key="S" />
+80 -6
View File
@@ -1,3 +1,60 @@
interface LogEntry {
time: string;
scope: string;
level: string;
args: unknown[];
}
const logBuffer: LogEntry[] = [];
const startTime = Date.now();
function formatTime(): string {
const ms = Date.now() - startTime;
const h = Math.floor(ms / 3600000).toString().padStart(2, '0');
const m = Math.floor((ms % 3600000) / 60000).toString().padStart(2, '0');
const s = Math.floor((ms % 60000) / 1000).toString().padStart(2, '0');
const mss = (ms % 1000).toString().padStart(3, '0');
return `${h}:${m}:${s}.${mss}`;
}
function serialize(arg: unknown): string {
if (typeof arg === 'string') return arg;
try {
return JSON.stringify(arg);
} catch {
return String(arg);
}
}
function formatEntry(entry: LogEntry, scopeWidth: number): string {
const scope = `[${entry.scope}]`.padEnd(scopeWidth + 2);
const level = entry.level.toUpperCase().padEnd(5);
const msg = entry.args.map(serialize).join(' ');
return `${entry.time} ${scope} ${level} ${msg}`;
}
(globalThis as Record<string, unknown>).copyLogs = () => {
if (logBuffer.length === 0) {
console.log('%c[logger] No log entries to copy', 'color: #888');
return;
}
const scopeWidth = logBuffer.reduce((max, e) => Math.max(max, e.scope.length), 0);
const lines = [
`=== Log Export (${logBuffer.length} entries) ===`,
'',
...logBuffer.map(e => formatEntry(e, scopeWidth))
].join('\n');
navigator.clipboard.writeText(lines).then(() => {
console.log(`%c[logger] Copied ${logBuffer.length} entries to clipboard`, 'color: #4f4');
});
};
(globalThis as Record<string, unknown>).clearLogs = () => {
logBuffer.length = 0;
console.log('%c[logger] Log buffer cleared', 'color: #888');
};
export const createLogger = (() => {
let maxLength = 5;
return (scope: string) => {
@@ -6,18 +63,35 @@ export const createLogger = (() => {
let isGrouped = false;
function s(color: string, ...args: any) {
function s(color: string, ...args: unknown[]) {
return isGrouped
? [...args]
: [`[%c${scope.padEnd(maxLength, ' ')}]:`, `color: ${color}`, ...args];
}
function record(level: string, args: unknown[]) {
logBuffer.push({ time: formatTime(), scope, level, args });
}
return {
log: (...args: any[]) => !muted && console.log(...s('#888', ...args)),
info: (...args: any[]) => !muted && console.info(...s('#888', ...args)),
warn: (...args: any[]) => !muted && console.warn(...s('#888', ...args)),
error: (...args: any[]) => console.error(...s('#f88', ...args)),
group: (...args: any[]) => {
log: (...args: unknown[]) => {
record('log', args);
!muted && console.log(...s('#888', ...args));
},
info: (...args: unknown[]) => {
record('info', args);
!muted && console.info(...s('#888', ...args));
},
warn: (...args: unknown[]) => {
record('warn', args);
!muted && console.warn(...s('#888', ...args));
},
error: (...args: unknown[]) => {
record('error', args);
console.error(...s('#f88', ...args));
},
group: (...args: unknown[]) => {
record('group', args);
if (!muted) {
console.groupCollapsed(...s('#888', ...args));
isGrouped = true;