3 Commits

Author SHA1 Message Date
33f10da396 feat(ui): make details stand out
Some checks failed
🚀 Lint & Test & Deploy / release (pull_request) Failing after 2m6s
2026-02-09 15:26:48 +01:00
af5b3b23ba fix: make sure that CHANGELOG.md is in correct place 2026-02-09 15:26:40 +01:00
64d75b9686 feat(ui): add InputColor and custom theme 2026-02-09 15:26:18 +01:00
13 changed files with 279 additions and 81 deletions

View File

@@ -5,6 +5,7 @@
"type": "module",
"scripts": {
"dev": "vite dev",
"predev": "rm static/CHANGELOG.md && ln -s ../../CHANGELOG.md static/CHANGELOG.md",
"build": "svelte-kit sync && vite build",
"test:unit": "vitest",
"test": "npm run test:unit -- --run && npm run test:e2e",

View File

@@ -2,19 +2,19 @@
import { colors } from '../graph/colors.svelte';
const circleMaterial = new MeshBasicMaterial({
color: colors.connection.clone(),
color: colors.outline.clone(),
toneMapped: false
});
let lineColor = $state(colors.connection.clone().convertSRGBToLinear());
let lineColor = $state(colors.outline.clone().convertSRGBToLinear());
$effect.root(() => {
$effect(() => {
if (appSettings.value.theme === undefined) {
return;
}
circleMaterial.color = colors.connection.clone().convertSRGBToLinear();
lineColor = colors.connection.clone().convertSRGBToLinear();
circleMaterial.color = colors.outline.clone().convertSRGBToLinear();
lineColor = colors.outline.clone().convertSRGBToLinear();
});
});

View File

@@ -57,7 +57,7 @@
uniforms={{
uColorBright: { value: colors['layer-2'] },
uColorDark: { value: colors['layer-1'] },
uStrokeColor: { value: colors.outline.clone() },
uStrokeColor: { value: colors['layer-2'] },
uStrokeWidth: { value: 1.0 },
uWidth: { value: 20 },
uHeight: { value: height }

View File

@@ -87,8 +87,6 @@
width: 30px;
z-index: 100;
border-radius: 50%;
/* background: red; */
/* opacity: 0.2; */
}
.click-target:hover + svg path {
@@ -108,8 +106,10 @@
svg path {
stroke-width: 0.2px;
transition: d 0.3s ease, fill 0.3s ease;
fill: var(--color-layer-2);
transition:
d 0.3s ease,
fill 0.3s ease;
fill: var(--color-outline);
stroke: var(--stroke);
stroke-width: var(--stroke-width);
d: var(--path);

View File

@@ -7,14 +7,19 @@
open?: boolean;
class?: string;
}
let { title = 'Details', transparent = false, children, open = $bindable(false), class: _class }:
Props = $props();
let {
title = 'Details',
transparent = false,
children,
open = $bindable(false),
class: _class
}: Props = $props();
</script>
<details
class:transparent
bind:open
class="text-text outline-1 outline-outline bg-layer-1 {_class}"
class="text-text outline-1 outline-outline bg-layer-2 {_class}"
>
<summary>{title}</summary>
<div>

View File

@@ -1,7 +1,14 @@
<script lang="ts">
import type { NodeInput } from '@nodarium/types';
import { InputCheckbox, InputNumber, InputSelect, InputShape, InputVec3 } from './index';
import {
InputCheckbox,
InputColor,
InputNumber,
InputSelect,
InputShape,
InputVec3
} from './index';
interface Props {
input: NodeInput;
@@ -21,8 +28,15 @@
/>
{:else if input.type === 'shape'}
<InputShape bind:value={value as number[]} />
{:else if input.type === 'color'}
<InputColor bind:value={value as number[]} />
{:else if input.type === 'integer'}
<InputNumber bind:value={value as number} min={input?.min} max={input?.max} step={1} />
<InputNumber
bind:value={value as number}
min={input?.min}
max={input?.max}
step={1}
/>
{:else if input.type === 'boolean'}
<InputCheckbox bind:value={value as boolean} {id} />
{:else if input.type === 'select'}

View File

@@ -140,12 +140,12 @@ html.theme-catppuccin {
}
html.theme-high-contrast {
--color-text: #ffffff;
--color-text: white;
--color-outline: white;
--color-layer-0: #000000;
--color-layer-0: black;
--color-layer-1: black;
--color-layer-2: #222222;
--color-layer-3: #ffffff;
--color-layer-2: #ababab;
--color-layer-3: white;
--color-connection: #fff;
}

View File

@@ -1,5 +1,6 @@
export { default as Input } from './Input.svelte';
export { default as InputCheckbox } from './inputs/InputCheckbox.svelte';
export { default as InputColor } from './inputs/InputColor.svelte';
export { default as InputNumber } from './inputs/InputNumber.svelte';
export { default as InputSelect } from './inputs/InputSelect.svelte';
export { default as InputShape } from './inputs/InputShape.svelte';

View File

@@ -0,0 +1,85 @@
<script lang="ts">
interface Props {
value?: [number, number, number];
id?: string;
}
let {
value = $bindable([255, 255, 255] as [number, number, number]),
id
}: Props = $props();
let hexValue = $derived(
`#${value.map((c) => c.toString(16).padStart(2, '0')).join('')}`
);
let rValue = $state(value[0]);
let gValue = $state(value[1]);
let bValue = $state(value[2]);
$effect(() => {
rValue = value[0];
gValue = value[1];
bValue = value[2];
});
function handleHexInput(e: Event) {
const target = e.target as HTMLInputElement;
let val = target.value.replace(/[^0-9a-fA-F]/g, '');
if (val.length > 6) val = val.slice(0, 6);
if (val.length === 3) {
val = val
.split('')
.map((c) => c + c)
.join('');
}
if (val.length === 6) {
value = [
parseInt(val.slice(0, 2), 16),
parseInt(val.slice(2, 4), 16),
parseInt(val.slice(4, 6), 16)
] as [number, number, number];
}
}
function handleHexBlur() {
rValue = value[0];
gValue = value[1];
bValue = value[2];
}
</script>
<div class="flex overflow-hidden rounded-sm border border-outline bg-layer-2 w-min">
<label
class="-ml-px w-8 shrink-0 overflow-hidden"
style={`background-color: ${hexValue}`}
>
<input
type="color"
bind:value={hexValue}
{id}
oninput={handleHexInput}
class="h-full w-8 cursor-pointer appearance-none p-0"
/>
</label>
<div class="flex items-center gap-1 px-2 py-1">
<span class="pointer-events-none text-text opacity-30">#</span>
<input
type="text"
value={hexValue.slice(1)}
{id}
oninput={handleHexInput}
onblur={handleHexBlur}
maxlength={6}
class="w-15 bg-transparent text-text outline-none"
/>
</div>
</div>
<style>
input[type="color"] {
margin-top: -1px;
margin-right: -1px;
height: calc(100% + 2px);
}
</style>

View File

@@ -30,7 +30,20 @@
$effect(() => {
if (!points.length) {
points = [47.8, 100, 47.8, 82.8, 30.9, 69.1, 23.2, 40.7, 27.1, 14.5, 42.5, 0];
points = [
47.8,
100,
47.8,
82.8,
30.9,
69.1,
23.2,
40.7,
27.1,
14.5,
42.5,
0
];
}
});
@@ -38,20 +51,21 @@
if (mirror) {
const _points: [number, number, number][] = [];
for (let i = 0; i < points.length / 2; i++) {
_points.push([...getPt(i), i]);
const pt = [...getPt(i), i];
if (pt[0] > 50) {
pt[0] = 100 - pt[0];
}
_points.push(pt);
}
const sortedPoints = _points
.sort((a, b) => {
if (a[1] !== b[1]) return b[1] - a[1];
return a[0] - b[0];
});
const sortedPoints = _points.sort((a, b) => {
if (a[1] !== b[1]) return b[1] - a[1];
return a[0] - b[0];
});
const newIndices = new Map(sortedPoints.map((p, i) => [p[2], i]));
const sorted = sortedPoints
.map(p => [p[0], p[1]])
.flat();
const sorted = sortedPoints.map((p) => [p[0], p[1]]).flat();
let sortChanged = false;
@@ -109,14 +123,20 @@
const x = 100 - arr[i];
d += ` L ${x} ${arr[i + 1]}`;
}
d += ' Z';
}
d += ' Z';
return d;
}
function handleMouseMove(ev: MouseEvent) {
if (mouseDown === undefined || draggingIndex === undefined || !downCirclePosition) return;
if (
mouseDown === undefined
|| draggingIndex === undefined
|| !downCirclePosition
) {
return;
}
let vx = (mouseDown[0] - ev.clientX) * (100 / svgRect.width);
let vy = (mouseDown[1] - ev.clientY) * (100 / svgRect.height);
@@ -126,7 +146,7 @@
vy /= 10;
}
let x = downCirclePosition[0] + ((isMirroredEvent ? 1 : -1) * vx);
let x = downCirclePosition[0] + (isMirroredEvent ? 1 : -1) * vx;
let y = downCirclePosition[1] - vy;
x = clamp(x, 0, mirror ? 50 : 100);
@@ -147,7 +167,7 @@
const x = ((ev.clientX - svgRect.left) / svgRect.width) * 100;
const y = ((ev.clientY - svgRect.top) / svgRect.height) * 100;
isMirroredEvent = x > 50;
isMirroredEvent = mirror && x > 50;
if (indexText !== undefined) {
draggingIndex = parseInt(indexText);
@@ -155,10 +175,7 @@
} else {
draggingIndex = undefined;
const pt = [
round(clamp(x, 0, 100)),
round(clamp(y, 0, 100))
] as [
const pt = [round(clamp(x, 0, 100)), round(clamp(y, 0, 100))] as [
number,
number
];
@@ -255,7 +272,7 @@
stroke: transparent;
transition: fill 0.2s ease;
stroke-width: 1px;
stroke: var(--color-layer-3);
stroke: var(--color-layer-3);
fill: var(--color-layer-2);
}
circle.active,

View File

@@ -3,6 +3,7 @@
import {
Details,
InputCheckbox,
InputColor,
InputNumber,
InputSelect,
InputShape,
@@ -10,6 +11,8 @@
ShortCut
} from '$lib';
import Section from './Section.svelte';
import Theme from './Theme.svelte';
import ThemeSelector from './ThemeSelector.svelte';
let intValue = $state(0);
let floatValue = $state(0.2);
@@ -19,61 +22,22 @@
let selectValue = $state(0);
const d = $derived(options[selectValue]);
let checked = $state(false);
let colorValue = $state<[number, number, number]>([59, 130, 246]);
let mirrorShape = $state(true);
let detailsOpen = $state(false);
let points = $state([]);
const themes = [
'dark',
'light',
'solarized',
'catppuccin',
'high-contrast',
'nord',
'dracula'
];
let themeIndex = $state(0);
$effect(() => {
const classList = document.documentElement.classList;
for (const c of classList) {
if (c.startsWith('theme-')) document.documentElement.classList.remove(c);
}
document.documentElement.classList.add(`theme-${themes[themeIndex]}`);
});
const colors = [
'layer-0',
'layer-1',
'layer-2',
'layer-3',
'active',
'selected',
'outline',
'connection',
'text'
];
let theme = $state('dark');
</script>
<main class="flex flex-col gap-8 py-8">
<div class="flex gap-4">
<h1 class="text-4xl">@nodarium/ui</h1>
<InputSelect bind:value={themeIndex} options={themes}></InputSelect>
<ThemeSelector bind:theme />
</div>
<Section title="Colors">
<table>
<tbody>
{#each colors as color (color)}
<tr>
<td>
<div class="w-6 h-6 mr-2 my-1 rounded-sm outline-1 bg-{color}"></div>
</td>
<td>{color}</td>
</tr>
{/each}
</tbody>
</table>
<Section title="InputNumber">
<Theme theme />
</Section>
<Section title="InputNumber">
@@ -99,6 +63,10 @@
<InputCheckbox bind:value={checked} />
</Section>
<Section title="Color" value={colorValue}>
<InputColor bind:value={colorValue} />
</Section>
<Section title="Shape">
{#snippet header()}
<label class="flex gap-2">

View File

@@ -0,0 +1,80 @@
<script lang="ts">
import { InputColor } from '$lib';
interface Props {
theme?: string;
}
let { theme }: Props = $props();
const colors = [
'layer-0',
'layer-1',
'layer-2',
'layer-3',
'active',
'selected',
'outline',
'connection',
'text'
];
let customColors = $state<CustomColors>({
text: [205, 214, 244],
outline: [62, 62, 79],
'layer-0': [6, 6, 27],
'layer-1': [23, 23, 46],
'layer-2': [49, 50, 68],
'layer-3': [168, 170, 200],
active: [0, 0, 0],
selected: [38, 139, 210],
connection: [131, 148, 150]
});
const themeCss = $derived.by(() => {
return `<style>html.theme-custom{
${
Object.keys(customColors)
.map((v) => {
return `--color-${v}: rgb(${customColors[v].join(',')});`;
})
.join('\n')
}
</style>`;
});
</script>
<svelte:head>
{@html themeCss}
</svelte:head>
<table>
<thead>
<tr>
<th>Color</th>
<th>Name</th>
<th>Custom</th>
</tr>
</thead>
<tbody>
{#each colors as color (color)}
<tr>
<td>
<div class="w-6 h-6 mr-2 my-1 rounded-sm outline-1 bg-{color}"></div>
</td>
<td>{color}</td>
<td>
<InputColor bind:value={customColors[color]} />
</td>
</tr>
{/each}
</tbody>
</table>
<style>
table {
border-spacing: 5px;
border-collapse: separate;
text-align: left;
margin-left: 5px;
}

View File

@@ -0,0 +1,27 @@
<script lang="ts">
import { InputSelect } from '$lib';
const themes = [
'dark',
'light',
'solarized',
'catppuccin',
'high-contrast',
'nord',
'dracula',
'custom'
];
let { theme = $bindable() } = $props();
let themeIndex = $state(0);
$effect(() => {
theme = themes[themeIndex];
const classList = document.documentElement.classList;
for (const c of classList) {
if (c.startsWith('theme-')) document.documentElement.classList.remove(c);
}
document.documentElement.classList.add(`theme-${themes[themeIndex]}`);
});
</script>
<InputSelect bind:value={themeIndex} options={themes}></InputSelect>