refactor: use tailwind custom colors for themes

Use tailwind v4 @theme block so we can use bg-layer-0 instead of
bg-[--layer-0] for theme colors.
This commit is contained in:
Max Richter
2026-02-03 12:16:12 +01:00
parent 89e4cf8364
commit b19da950a6
35 changed files with 379 additions and 375 deletions

View File

@@ -4,3 +4,7 @@
prefix: "i"; prefix: "i";
icon-sets: from-folder(custom, "./src/lib/icons"); icon-sets: from-folder(custom, "./src/lib/icons");
} }
body * {
color: var(--color-text);
}

View File

@@ -149,7 +149,7 @@
} }
input { input {
background: var(--layer-0); background: var(--color-layer-0);
font-family: var(--font-family); font-family: var(--font-family);
border: none; border: none;
border-radius: 5px; border-radius: 5px;
@@ -168,10 +168,10 @@
.add-menu-wrapper { .add-menu-wrapper {
position: absolute; position: absolute;
background: var(--layer-1); background: var(--color-layer-1);
border-radius: 7px; border-radius: 7px;
overflow: hidden; overflow: hidden;
border: solid 2px var(--layer-2); border: solid 2px var(--color-layer-2);
width: 150px; width: 150px;
} }
.content { .content {
@@ -184,14 +184,14 @@
.result { .result {
padding: 1em 0.9em; padding: 1em 0.9em;
border-bottom: solid 1px var(--layer-2); border-bottom: solid 1px var(--color-layer-2);
opacity: 0.7; opacity: 0.7;
font-size: 0.9em; font-size: 0.9em;
cursor: pointer; cursor: pointer;
} }
.result[aria-selected="true"] { .result[aria-selected="true"] {
background: var(--layer-2); background: var(--color-layer-2);
opacity: 1; opacity: 1;
} }
</style> </style>

View File

@@ -32,7 +32,7 @@
.box-selection { .box-selection {
width: 40px; width: 40px;
height: 20px; height: 20px;
border: solid 2px var(--outline); border: solid 2px var(--color-outline);
border-style: dashed; border-style: dashed;
border-radius: 2px; border-radius: 2px;
} }

View File

@@ -88,12 +88,12 @@
position: fixed; position: fixed;
pointer-events: none; pointer-events: none;
transform: translate(var(--mx), var(--my)); transform: translate(var(--mx), var(--my));
background: var(--layer-1); background: var(--color-layer-1);
border-radius: 5px; border-radius: 5px;
top: 10px; top: 10px;
left: 10px; left: 10px;
max-width: 250px; max-width: 250px;
border: 1px solid var(--outline); border: 1px solid var(--color-outline);
z-index: 10000; z-index: 10000;
display: none; display: none;
} }

View File

@@ -244,7 +244,7 @@
z-index: 1; z-index: 1;
width: 100%; width: 100%;
height: 100%; height: 100%;
background: var(--layer-2); background: var(--color-layer-2);
opacity: 0; opacity: 0;
} }
input:disabled { input:disabled {
@@ -264,8 +264,8 @@
border-radius: 5px; border-radius: 5px;
width: calc(100% - 20px); width: calc(100% - 20px);
height: calc(100% - 25px); height: calc(100% - 25px);
border: dashed 4px var(--layer-2); border: dashed 4px var(--color-layer-2);
background: var(--layer-1); background: var(--color-layer-1);
opacity: 0.5; opacity: 0.5;
} }
</style> </style>

View File

@@ -14,7 +14,7 @@ const variables = [
function getColor(variable: (typeof variables)[number]) { function getColor(variable: (typeof variables)[number]) {
const style = getComputedStyle(document.body.parentElement!); const style = getComputedStyle(document.body.parentElement!);
const color = style.getPropertyValue(`--${variable}`); const color = style.getPropertyValue(`--color-${variable}`);
return new Color().setStyle(color, LinearSRGBColorSpace); return new Color().setStyle(color, LinearSRGBColorSpace);
} }
@@ -27,7 +27,7 @@ $effect.root(() => {
if (!appSettings.value.theme || !('getComputedStyle' in globalThis)) return; if (!appSettings.value.theme || !('getComputedStyle' in globalThis)) return;
const style = getComputedStyle(document.body.parentElement!); const style = getComputedStyle(document.body.parentElement!);
for (const v of variables) { for (const v of variables) {
const hex = style.getPropertyValue(`--${v}`); const hex = style.getPropertyValue(`--color-${v}`);
colors[v].setStyle(hex, LinearSRGBColorSpace); colors[v].setStyle(hex, LinearSRGBColorSpace);
} }
}); });

View File

@@ -71,22 +71,22 @@
user-select: none !important; user-select: none !important;
cursor: pointer; cursor: pointer;
width: 200px; width: 200px;
color: var(--text-color); color: var(--color-text);
transform: translate3d(var(--nx), var(--ny), 0); transform: translate3d(var(--nx), var(--ny), 0);
z-index: 1; z-index: 1;
opacity: calc((var(--cz) - 2.5) / 3.5); opacity: calc((var(--cz) - 2.5) / 3.5);
font-weight: 300; font-weight: 300;
--stroke: var(--outline); --stroke: var(--color-outline);
--stroke-width: 2px; --stroke-width: 2px;
} }
.node.active { .node.active {
--stroke: var(--active); --stroke: var(--color-active);
--stroke-width: 2px; --stroke-width: 2px;
} }
.node.selected { .node.selected {
--stroke: var(--selected); --stroke: var(--color-selected);
--stroke-width: 2px; --stroke-width: 2px;
} }
</style> </style>

View File

@@ -109,7 +109,7 @@
svg path { svg path {
stroke-width: 0.2px; stroke-width: 0.2px;
transition: d 0.3s ease, fill 0.3s ease; transition: d 0.3s ease, fill 0.3s ease;
fill: var(--layer-2); fill: var(--color-layer-2);
stroke: var(--stroke); stroke: var(--stroke);
stroke-width: var(--stroke-width); stroke-width: var(--stroke-width);
d: var(--path); d: var(--path);

View File

@@ -175,7 +175,7 @@
svg path { svg path {
transition: d 0.3s ease, fill 0.3s ease; transition: d 0.3s ease, fill 0.3s ease;
fill: var(--layer-1); fill: var(--color-layer-1);
stroke: var(--stroke); stroke: var(--stroke);
stroke-width: var(--stroke-width); stroke-width: var(--stroke-width);
d: var(--path); d: var(--path);

View File

@@ -62,7 +62,7 @@
cursor: ew-resize; cursor: ew-resize;
height: 100%; height: 100%;
width: 1px; width: 1px;
background: var(--outline); background: var(--color-outline);
} }
.seperator::before { .seperator::before {
content: ""; content: "";

View File

@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { Select } from '@nodarium/ui'; import { InputSelect } from '@nodarium/ui';
let activeStore = $state(0); let activeStore = $state(0);
let { activeId }: { activeId: string } = $props(); let { activeId }: { activeId: string } = $props();
@@ -10,7 +10,7 @@
<div class="breadcrumbs"> <div class="breadcrumbs">
{#if activeUser} {#if activeUser}
<Select id="root" options={['root']} bind:value={activeStore}></Select> <InputSelect id="root" options={['root']} bind:value={activeStore}></InputSelect>
{#if activeCollection} {#if activeCollection}
<button <button
onclick={() => { onclick={() => {
@@ -35,7 +35,7 @@
<span>{activeUser}</span> <span>{activeUser}</span>
{/if} {/if}
{:else} {:else}
<Select id="root" options={['root']} bind:value={activeStore}></Select> <InputSelect id="root" options={['root']} bind:value={activeStore}></InputSelect>
{/if} {/if}
</div> </div>
@@ -47,7 +47,7 @@
gap: 0.8em; gap: 0.8em;
height: 35px; height: 35px;
box-sizing: border-box; box-sizing: border-box;
border-bottom: solid thin var(--outline); border-bottom: solid thin var(--color-outline);
} }
.breadcrumbs > button { .breadcrumbs > button {
position: relative; position: relative;

View File

@@ -66,7 +66,7 @@
} }
.dragging { .dragging {
border: dashed 2px var(--outline); border: dashed 2px var(--color-outline);
} }
.node-wrapper > div { .node-wrapper > div {
opacity: 1; opacity: 1;

View File

@@ -74,7 +74,7 @@
.wrapper { .wrapper {
position: relative; position: relative;
border-bottom: solid thin var(--outline); border-bottom: solid thin var(--color-outline);
display: flex; display: flex;
} }
p { p {
@@ -88,13 +88,13 @@
svg { svg {
height: 124px; height: 124px;
margin: 24px 0px; margin: 24px 0px;
border-top: solid thin var(--outline); border-top: solid thin var(--color-outline);
border-bottom: solid thin var(--outline); border-bottom: solid thin var(--color-outline);
width: 100%; width: 100%;
} }
polyline { polyline {
fill: none; fill: none;
stroke: var(--layer-3); stroke: var(--color-layer-3);
opacity: 0.5; opacity: 0.5;
stroke-width: 1; stroke-width: 1;
} }

View File

@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { humanizeNumber } from '$lib/helpers'; import { humanizeNumber } from '$lib/helpers';
import { Checkbox } from '@nodarium/ui'; import { InputCheckbox } from '@nodarium/ui';
import type { PerformanceData } from '@nodarium/utils'; import type { PerformanceData } from '@nodarium/utils';
import BarSplit from './BarSplit.svelte'; import BarSplit from './BarSplit.svelte';
import Monitor from './Monitor.svelte'; import Monitor from './Monitor.svelte';
@@ -195,7 +195,7 @@
<div class="p-4 performance-tabler"> <div class="p-4 performance-tabler">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<Checkbox id="show-total" bind:value={showAverage} /> <InputCheckbox id="show-total" bind:value={showAverage} />
<label for="show-total">Show Average</label> <label for="show-total">Show Average</label>
</div> </div>

View File

@@ -25,7 +25,7 @@
} }
polyline { polyline {
fill: none; fill: none;
stroke: var(--layer-3); stroke: var(--color-layer-3);
opacity: 1; opacity: 1;
stroke-width: 1; stroke-width: 1;
} }

View File

@@ -74,14 +74,14 @@
top: 10px; top: 10px;
left: 10px; left: 10px;
z-index: 2; z-index: 2;
background: var(--layer-0); background: var(--color-layer-0);
border: solid thin var(--outline); border: solid thin var(--color-outline);
border-collapse: collapse; border-collapse: collapse;
} }
td { td {
padding: 4px; padding: 4px;
padding-inline: 8px; padding-inline: 8px;
font-size: 0.8em; font-size: 0.8em;
border: solid thin var(--outline); border: solid thin var(--color-outline);
} }
</style> </style>

View File

@@ -31,10 +31,10 @@
} }
</script> </script>
<header class="flex justify-between px-4 h-[70px] border-b-1 border-[var(--outline)] items-center"> <header class="flex justify-between px-4 h-[70px] border-b-1 border-outline items-center">
<h3>Project</h3> <h3>Project</h3>
<button <button
class="px-3 py-1 bg-[var(--layer-0)] rounded" class="px-3 py-1 bg-layer-0 rounded"
onclick={() => (showNewProject = !showNewProject)} onclick={() => (showNewProject = !showNewProject)}
> >
New New
@@ -42,7 +42,7 @@
</header> </header>
{#if showNewProject} {#if showNewProject}
<div class="flex flex-col px-4 py-3 border-b-1 border-[var(--outline)] gap-2"> <div class="flex flex-col px-4 py-3 border-b-1 border-outline gap-2">
<input <input
type="text" type="text"
bind:value={newProjectName} bind:value={newProjectName}

View File

@@ -223,6 +223,6 @@
left: 0; left: 0;
right: 0; right: 0;
border: none; border: none;
border-bottom: solid thin var(--outline); border-bottom: solid thin var(--color-outline);
} }
</style> </style>

View File

@@ -43,7 +43,7 @@
<style> <style>
header { header {
border-bottom: solid thin var(--outline); border-bottom: solid thin var(--color-outline);
height: 70px; height: 70px;
display: flex; display: flex;
align-items: center; align-items: center;

View File

@@ -8,7 +8,6 @@
<div class="wrapper" class:visible={state.activePanel.value}> <div class="wrapper" class:visible={state.activePanel.value}>
<div class="tabs"> <div class="tabs">
<button aria-label="Close" onclick={() => state.toggleOpen()}> <button aria-label="Close" onclick={() => state.toggleOpen()}>
<span class="icon-[tabler--settings]"></span>
<span class="absolute i-[tabler--chevron-left] w-6 h-6 block"></span> <span class="absolute i-[tabler--chevron-left] w-6 h-6 block"></span>
</button> </button>
{#each state.keys as panelId (panelId)} {#each state.keys as panelId (panelId)}
@@ -45,7 +44,7 @@
} }
.content { .content {
background: var(--layer-1); background: var(--color-layer-1);
z-index: 10; z-index: 10;
position: relative; position: relative;
max-height: 100vh; max-height: 100vh;
@@ -55,7 +54,7 @@
.tabs { .tabs {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
border-right: solid thin var(--outline); border-right: solid thin var(--color-outline);
} }
.tabs > button { .tabs > button {
@@ -66,9 +65,9 @@
border: none; border: none;
display: flex; display: flex;
align-items: center; align-items: center;
border-bottom: solid thin var(--outline); border-bottom: solid thin var(--color-outline);
border-left: solid thin var(--outline); border-left: solid thin var(--color-outline);
background: var(--layer-1); background: var(--color-layer-1);
} }
.tabs > button > span { .tabs > button > span {
@@ -76,7 +75,7 @@
} }
.tabs > button.active { .tabs > button.active {
background: var(--layer-2); background: var(--color-layer-2);
} }
.tabs > button.active span { .tabs > button.active span {

View File

@@ -8,7 +8,7 @@
import { humanizeDuration } from '$lib/helpers'; import { humanizeDuration } from '$lib/helpers';
import { localState } from '$lib/helpers/localState.svelte'; import { localState } from '$lib/helpers/localState.svelte';
import Monitor from '$lib/performance/Monitor.svelte'; import Monitor from '$lib/performance/Monitor.svelte';
import { Float } from '@nodarium/ui'; import { InputNumber } from '@nodarium/ui';
import { writable } from 'svelte/store'; import { writable } from 'svelte/store';
function calculateStandardDeviation(array: number[]) { function calculateStandardDeviation(array: number[]) {
@@ -125,7 +125,7 @@
</progress> </progress>
{:else} {:else}
<label for="bench-samples">Samples</label> <label for="bench-samples">Samples</label>
<Float id="bench-sample" bind:value={amount.value} max={1000} /> <InputNumber id="bench-sample" bind:value={amount.value} max={1000} step={1} />
<button onclick={benchmark} disabled={isRunning}>start</button> <button onclick={benchmark} disabled={isRunning}>start</button>
{/if} {/if}
</div> </div>
@@ -138,7 +138,7 @@
gap: 1em; gap: 1em;
} }
.monitor-wrapper { .monitor-wrapper {
border: solid thin var(--outline); border: solid thin var(--color-outline);
border-bottom: none; border-bottom: none;
} }
i { i {

View File

@@ -262,7 +262,7 @@
<style> <style>
header { header {
background-color: var(--layer-1); background-color: var(--color-layer-1);
} }
.wrapper { .wrapper {

View File

@@ -1,16 +1,16 @@
<script lang="ts"> <script lang="ts">
import type { Snippet } from 'svelte';
interface Props { interface Props {
title?: string; title?: string;
transparent?: boolean; transparent?: boolean;
children?: import('svelte').Snippet; children?: Snippet;
open?: boolean; open?: boolean;
} }
let { title = 'Details', transparent = false, children, open = $bindable(false) }: Props = let { title = 'Details', transparent = false, children, open = $bindable(false) }: Props =
$props(); $props();
</script> </script>
<details class:transparent bind:open> <details class:transparent bind:open class="text-text outline-1 outline-outline bg-layer-1">
<summary>{title}</summary> <summary>{title}</summary>
<div class="content"> <div class="content">
{@render children?.()} {@render children?.()}
@@ -20,10 +20,7 @@
<style> <style>
details { details {
padding: 1em; padding: 1em;
color: var(--text-color);
padding-left: 20px; padding-left: 20px;
background-color: #202020;
outline: solid 0.1px white;
border-radius: 2px; border-radius: 2px;
font-weight: 300; font-weight: 300;
font-size: 0.9em; font-size: 0.9em;

View File

@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import type { NodeInput } from '@nodarium/types'; import type { NodeInput } from '@nodarium/types';
import { Checkbox, Float, Select, Vec3 } from './index.js'; import { InputCheckbox, InputNumber, InputSelect, InputVec3 } from './index';
interface Props { interface Props {
input: NodeInput; input: NodeInput;
@@ -13,18 +13,18 @@
</script> </script>
{#if input.type === 'float'} {#if input.type === 'float'}
<Float <InputNumber
bind:value={value as number} bind:value={value as number}
min={input?.min} min={input?.min}
max={input?.max} max={input?.max}
step={input?.step} step={input?.step}
/> />
{:else if input.type === 'integer'} {:else if input.type === 'integer'}
<Float bind:value={value as number} min={input?.min} max={input?.max} /> <InputNumber bind:value={value as number} min={input?.min} max={input?.max} step={1} />
{:else if input.type === 'boolean'} {:else if input.type === 'boolean'}
<Checkbox bind:value={value as boolean} {id} /> <InputCheckbox bind:value={value as boolean} {id} />
{:else if input.type === 'select'} {:else if input.type === 'select'}
<Select bind:value={value as number} options={input.options} {id} /> <InputSelect bind:value={value as number} options={input.options} {id} />
{:else if input.type === 'vec3'} {:else if input.type === 'vec3'}
<Vec3 bind:value={value as [number, number, number]} {id} /> <InputVec3 bind:value={value as [number, number, number]} {id} />
{/if} {/if}

View File

@@ -9,7 +9,7 @@
let { ctrl = false, shift = false, alt = false, key }: Props = $props(); let { ctrl = false, shift = false, alt = false, key }: Props = $props();
</script> </script>
<div class="command"> <div class="command bg-layer-2">
{#if ctrl} {#if ctrl}
<span>Ctrl</span> <span>Ctrl</span>
{/if} {/if}
@@ -24,8 +24,8 @@
<style> <style>
.command { .command {
background: var(--layer-2);
padding: 0.4em; padding: 0.4em;
padding-inline: 0.8em;
font-size: 0.8em; font-size: 0.8em;
border-radius: 0.3em; border-radius: 0.3em;
white-space: nowrap; white-space: nowrap;

View File

@@ -1,4 +1,18 @@
@import "tailwindcss"; @import "tailwindcss";
@plugin "@iconify/tailwind4" {
prefix: "i";
}
@source inline('{hover:,}{bg-,outline-,text-,}layer-0');
@source inline('{hover:,}{bg-,outline-,text-,}layer-1');
@source inline('{hover:,}{bg-,outline-,text-,}layer-2');
@source inline('{hover:,}{bg-,outline-,text-,}layer-3');
@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-,}connection');
@source inline('{hover:,}{bg-,outline-,text-,}edge');
@source inline('{hover:,}{bg-,outline-,text-,}text');
/* fira-code-300 - latin */ /* fira-code-300 - latin */
@font-face { @font-face {
@@ -58,9 +72,9 @@
--color-outline: var(--neutral-400); --color-outline: var(--neutral-400);
--color-connection: #333333; --color-connection: #333333;
--color-edge: var(--connection, var(--outline)); --color-edge: var(--connection, var(--color-outline));
--color-text-color: var(--neutral-200); --color-text: var(--neutral-200);
} }
html { html {
@@ -72,100 +86,89 @@ html {
--neutral-800: #111111; --neutral-800: #111111;
--neutral-900: #060606; --neutral-900: #060606;
--layer-0: var(--neutral-900); --color-layer-0: var(--neutral-900);
--layer-1: var(--neutral-500); --color-layer-1: var(--neutral-500);
--layer-2: var(--neutral-400); --color-layer-2: var(--neutral-400);
--layer-3: var(--neutral-200); --color-layer-3: var(--neutral-200);
--active: #ffffff; --color-active: #ffffff;
--selected: #c65a19; --color-selected: #c65a19;
--outline: var(--neutral-400); --color-outline: var(--neutral-400);
--connection: #333333; --color-connection: #333333;
--edge: var(--connection, var(--outline)); --color-edge: var(--connection, var(--color-outline));
--text-color: var(--neutral-200); --color-text-color: var(--neutral-200);
} }
body { body {
color: var(--text-color); color: var(--color-text);
background-color: var(--layer-0); background-color: var(--color-layer-0);
margin: 0; margin: 0;
} }
body * {
color: var(--text-color);
}
html.theme-light { html.theme-light {
--text-color: var(--neutral-800); --color-text: var(--neutral-800);
--outline: var(--neutral-300); --color-outline: var(--neutral-300);
--layer-0: var(--neutral-100); --color-layer-0: var(--neutral-100);
--layer-1: var(--neutral-100); --color-layer-1: var(--neutral-100);
--layer-2: var(--neutral-200); --color-layer-2: var(--neutral-200);
--layer-3: var(--neutral-500); --color-layer-3: var(--neutral-500);
--active: #000000; --color-active: #000000;
--selected: #c65a19; --color-selected: #c65a19;
--connection: #888; --color-connection: #888;
} }
html.theme-solarized { html.theme-solarized {
--text-color: #425055; --color-text: #425055;
--outline: #93a1a1; --color-outline: #93a1a1;
--layer-0: #fdf6e3; --color-layer-0: #fdf6e3;
--layer-1: #eee8d5; --color-layer-1: #eee8d5;
--layer-2: #c4c0b4; --color-layer-2: #c4c0b4;
--layer-3: #586e75; --color-layer-3: #586e75;
--active: #000000; --color-active: #000000;
--selected: #268bd2; --color-selected: #268bd2;
--connection: #839496; --color-connection: #839496;
} }
html.theme-catppuccin { html.theme-catppuccin {
--text-color: #cdd6f4; --color-text: #cdd6f4;
--outline: #3e3e4f; --color-outline: #3e3e4f;
--layer-3: #a8aac8; --color-layer-3: #a8aac8;
--layer-2: #313244; --color-layer-2: #313244;
--layer-1: #1e1e2e; --color-layer-1: #1e1e2e;
--layer-0: #11111b; --color-layer-0: #11111b;
--connection: #444459; --color-connection: #444459;
} }
html.theme-high-contrast { html.theme-high-contrast {
--text-color: #ffffff; --color-text: #ffffff;
--outline: white; --color-outline: white;
--layer-0: #000000; --color-layer-0: #000000;
--layer-1: black; --color-layer-1: black;
--layer-2: #222222; --color-layer-2: #222222;
--layer-3: #ffffff; --color-layer-3: #ffffff;
--connection: #fff; --color-connection: #fff;
} }
html.theme-nord { html.theme-nord {
--text-color: #d8dee9; --color-text: #d8dee9;
--outline: #4c566a; --color-outline: #4c566a;
--layer-0: #2e3440; --color-layer-0: #2e3440;
--layer-1: #3b4252; --color-layer-1: #3b4252;
--layer-2: #434c5e; --color-layer-2: #434c5e;
--layer-3: #5e81ac; --color-layer-3: #5e81ac;
--active: #8999bd; --color-active: #8999bd;
--selected: #b76c3f; --color-selected: #b76c3f;
--connection: #4c566a; --color-connection: #4c566a;
} }
html.theme-dracula { html.theme-dracula {
--text-color: #f8f8f2; --color-text: #f8f8f2;
--outline: #6272a4; --color-outline: #6272a4;
--layer-0: #282a36; --color-layer-0: #282a36;
--layer-1: #44475a; --color-layer-1: #44475a;
--layer-2: #32374d; --color-layer-2: #32374d;
--layer-3: #bd93f9; --color-layer-3: #bd93f9;
--connection: #6272a4; --color-connection: #6272a4;
}
button {
background-color: var(--layer-2);
border: 1px solid var(--outline);
padding: 8px 9px;
border-radius: 4px;
} }

View File

@@ -1,8 +1,8 @@
export { default as Input } from './Input.svelte'; export { default as Input } from './Input.svelte';
export { default as Checkbox } from './inputs/Checkbox.svelte'; export { default as InputCheckbox } from './inputs/InputCheckbox.svelte';
export { default as Float } from './inputs/Float.svelte'; export { default as InputNumber } from './inputs/InputNumber.svelte';
export { default as Select } from './inputs/Select.svelte'; export { default as InputSelect } from './inputs/InputSelect.svelte';
export { default as Vec3 } from './inputs/Vec3.svelte'; export { default as InputVec3 } from './inputs/InputVec3.svelte';
export { default as Details } from './Details.svelte'; export { default as Details } from './Details.svelte';
export { default as ShortCut } from './ShortCut.svelte'; export { default as ShortCut } from './ShortCut.svelte';

View File

@@ -1,199 +0,0 @@
<script lang="ts">
interface Props {
value?: number;
step?: number;
min?: number;
max?: number;
id?: string;
}
let {
value = $bindable(0.5),
step,
min = $bindable(0),
max = $bindable(1),
id
}: Props = $props();
if (min > max) {
[min, max] = [max, min];
}
if (value > max) {
max = value;
}
const precision = $derived(
((step || value).toString().split('.')[1] || '').length
);
function strip(input: number) {
return +parseFloat(input + '').toFixed(precision);
}
let inputEl: HTMLInputElement | undefined = $state();
let oldValue: number;
function handleChange() {
if (value === oldValue) return;
oldValue = value;
}
let isMouseDown = $state(false);
let downV = 0;
let vx = 0;
let rect: DOMRect;
function handleMouseDown(ev: MouseEvent) {
ev.preventDefault();
if (!inputEl) return;
inputEl.focus();
isMouseDown = true;
downV = value;
rect = inputEl.getBoundingClientRect();
window.removeEventListener('mousemove', handleMouseMove);
window.addEventListener('mousemove', handleMouseMove);
window.addEventListener('mouseup', handleMouseUp);
document.body.style.cursor = 'ew-resize';
(ev.target as HTMLElement)?.blur();
}
function handleMouseUp() {
isMouseDown = false;
if (downV === value) {
inputEl?.focus();
}
if (value > max) {
max = value;
}
if (value < min) {
min = value;
}
document.body.style.cursor = 'unset';
window.removeEventListener('mouseup', handleMouseUp);
window.removeEventListener('mousemove', handleMouseMove);
}
function handleKeyDown(ev: KeyboardEvent) {
if (ev.key === 'Escape' || ev.key === 'Enter') {
handleMouseUp();
inputEl?.blur();
}
}
function handleMouseMove(ev: MouseEvent) {
vx = (ev.clientX - rect.left) / rect.width;
if (ev.ctrlKey) {
let v = min + (max - min) * vx;
value = v;
} else {
value = Math.max(Math.min(min + (max - min) * vx, max), min);
}
value = strip(value);
// With ctrl + outside of input ev.target becomes HTMLDocument
if (ev.target instanceof HTMLElement) {
ev.target?.blur();
}
}
$effect(() => {
if (value.toString().length > 5) {
value = strip(value);
}
if (value !== undefined) {
handleChange();
}
});
let width = $derived(
Number.isFinite(value)
? Math.max((value?.toString().length ?? 1) * 8, 50) + 'px'
: '20px'
);
</script>
<div class="component-wrapper" class:is-down={isMouseDown}>
<span class="overlay" style={`width: ${((value - min) / (max - min)) * 100}%`}></span>
<input
bind:value
bind:this={inputEl}
{id}
{step}
{max}
{min}
onkeydown={handleKeyDown}
onmousedown={handleMouseDown}
onmouseup={handleMouseUp}
type="number"
style={`width:${width};`}
/>
</div>
<style>
.component-wrapper {
position: relative;
background-color: var(--layer-2, #4b4b4b);
border-radius: 4px;
user-select: none;
transition: box-shadow 0.3s ease;
border: solid 1px var(--outline);
box-sizing: border-box;
overflow: hidden;
border-radius: var(--border-radius, 2px);
}
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
-webkit-appearance: none;
}
input[type="number"] {
box-sizing: border-box;
-webkit-appearance: textfield;
-moz-appearance: textfield;
appearance: textfield;
font-family: var(--font-family);
font-variant-numeric: tabular-nums;
cursor: pointer;
color: var(--text-color);
background-color: transparent;
padding: var(--padding, 6px);
font-size: 1em;
padding-inline: 10px;
text-align: center;
border: none;
border-style: none;
min-width: 100%;
}
.is-down > input {
cursor: ew-resize !important;
}
.overlay {
position: absolute;
top: 0px;
left: 0px;
height: 100%;
max-width: 100%;
background-color: var(--text-color);
opacity: 0.3;
pointer-events: none;
transition: width 0.3s ease;
}
.is-down > .overlay {
transition: none !important;
}
</style>

View File

@@ -18,7 +18,7 @@
</script> </script>
<label <label
class="relative inline-flex h-[22px] w-[22px] cursor-pointer items-center justify-center bg-[var(--layer-2)] rounded-[5px]" class="relative inline-flex h-5.5 w-5.5 cursor-pointer items-center justify-center bg-layer-2 rounded-[5px]"
> >
<input <input
type="checkbox" type="checkbox"
@@ -33,7 +33,7 @@
viewBox="0 0 19 14" viewBox="0 0 19 14"
fill="none" fill="none"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="h-[10px] w-[12px] text-[var(--text-color)]" class="h-2.5 w-3 text-text"
> >
<path <path
d="M2 7L7 12L17 2" d="M2 7L7 12L17 2"

View File

@@ -0,0 +1,174 @@
<script lang="ts">
import { onMount } from 'svelte';
interface Props {
value?: number;
step?: number;
min?: number;
max?: number;
id?: string;
}
let {
value = $bindable(0.5),
step,
min = $bindable(0),
max = $bindable(1),
id
}: Props = $props();
// normalize bounds
if (min > max) [min, max] = [max, min];
if (value > max) max = value;
let inputEl: HTMLInputElement | undefined = $state();
function clamp(v: number) {
return Math.min(max, Math.max(min, v));
}
function snap(v: number) {
if (step) v = Math.round(v / step) * step;
return +v.toFixed(3);
}
let dragging = $state(false);
let startValue = 0;
let rect: DOMRect;
function onMouseDown(e: MouseEvent) {
if (!inputEl) return;
e.preventDefault();
dragging = true;
startValue = value;
rect = inputEl.getBoundingClientRect();
document.body.style.cursor = 'ew-resize';
window.addEventListener('mousemove', onMouseMove);
window.addEventListener('mouseup', onMouseUp);
}
function onMouseMove(e: MouseEvent) {
const ratio = (e.clientX - rect.left) / rect.width;
let delta = (min + (max - min) * ratio) - startValue;
if (e.shiftKey) delta /= 5;
value = snap(
e.ctrlKey
? startValue + delta
: clamp(startValue + delta)
);
}
function onMouseUp() {
dragging = false;
document.body.style.cursor = '';
window.removeEventListener('mousemove', onMouseMove);
window.removeEventListener('mouseup', onMouseUp);
if (startValue === value) {
inputEl?.focus();
}
min = Math.min(min, value);
max = Math.max(max, value);
}
function onKeyDown(e: KeyboardEvent) {
if (e.key === 'Escape' || e.key === 'Enter') {
onMouseUp();
inputEl?.blur();
}
}
function windowKeyDown(e: KeyboardEvent) {
if (!dragging) return;
if (e.shiftKey) {
startValue = value;
}
}
const width = $derived(
Number.isFinite(value)
? `${Math.max(value.toString().length * 8, 50)}px`
: '20px'
);
function stepUp(e: MouseEvent) {
value = snap(value + (e.shiftKey ? step! * 10 : step!));
}
function stepDown(e: MouseEvent) {
value = snap(value - (e.shiftKey ? step! * 10 : step!));
}
onMount(() => {
value = snap(clamp(value));
});
</script>
<svelte:window onkeydown={windowKeyDown} />
<div
class="component-wrapper relative flex items-stretch overflow-hidden rounded-sm border border-outline bg-layer-2 select-none transition-shadow"
class:cursor-ew-resize={dragging}
>
{#if step}
<button
aria-label="step down"
onmousedown={stepDown}
class="cursor-pointer w-4 bg-layer-3 opacity-30 hover:opacity-50"
>
<span class="i-[tabler--chevron-compact-left] block h-full w-full text-outline!"></span>
</button>
<div class="w-px bg-outline"></div>
{/if}
<div class="relative grow">
<div
class="absolute inset-y-0 left-0 bg-layer-3 opacity-30 pointer-events-none transition-[width]"
class:transition-none={dragging}
style={`width: ${Math.min((value - min) / (max - min), 1) * 100}%`}
>
</div>
<input
bind:this={inputEl}
bind:value
{id}
{step}
{min}
{max}
type="number"
onkeydown={onKeyDown}
onmousedown={onMouseDown}
class="w-full min-w-full cursor-pointer bg-transparent px-2 py-1 text-center font-tabular text-text outline-none appearance-none"
style:width={width}
/>
</div>
{#if step}
<div class="w-px bg-outline"></div>
<button
aria-label="step up"
onmousedown={stepUp}
class="cursor-pointer w-4 bg-layer-3 opacity-30 hover:opacity-50"
>
<span class="i-[tabler--chevron-compact-right] block h-full w-full text-outline!"></span>
</button>
{/if}
</div>
<style>
input[type="number"] {
-webkit-appearance: textfield;
-moz-appearance: textfield;
appearance: textfield;
}
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
-webkit-appearance: none;
}
</style>

View File

@@ -8,7 +8,7 @@
let { options = [], value = $bindable(0), id = '' }: Props = $props(); let { options = [], value = $bindable(0), id = '' }: Props = $props();
</script> </script>
<select {id} bind:value> <select {id} bind:value class="bg-layer-2 text-text">
{#each options as label, i (label)} {#each options as label, i (label)}
<option value={i}>{label}</option> <option value={i}>{label}</option>
{/each} {/each}
@@ -16,10 +16,8 @@
<style> <style>
select { select {
background: var(--layer-2);
color: var(--text-color);
font-family: var(--font-family); font-family: var(--font-family);
outline: solid 1px var(--outline); outline: solid 1px var(--color-outline);
padding: 0.8em 1em; padding: 0.8em 1em;
border-radius: 5px; border-radius: 5px;
border: none; border: none;

View File

@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import Float from './Float.svelte'; import InputNumber from './InputNumber.svelte';
interface Props { interface Props {
value?: number[]; value?: number[];
@@ -10,9 +10,9 @@
</script> </script>
<div> <div>
<Float id={`${id}-x`} bind:value={value[0]} step={0.01} /> <InputNumber id={`${id}-x`} bind:value={value[0]} step={0.01} />
<Float id={`${id}-y`} bind:value={value[1]} step={0.01} /> <InputNumber id={`${id}-y`} bind:value={value[1]} step={0.01} />
<Float id={`${id}-z`} bind:value={value[2]} step={0.01} /> <InputNumber id={`${id}-z`} bind:value={value[2]} step={0.01} />
</div> </div>
<style> <style>
@@ -23,9 +23,9 @@
div > :global(.component-wrapper:nth-child(2)) { div > :global(.component-wrapper:nth-child(2)) {
border-radius: 0px !important; border-radius: 0px !important;
outline: none; outline: none;
border: solid thin var(--outline); border: solid thin var(--color-outline);
border-top: solid thin color-mix(in srgb, var(--outline) 50%, transparent); border-top: solid 1px var(--color-outline);
border-bottom: solid thin color-mix(in srgb, var(--outline) 50%, transparent); border-bottom: solid 1px var(--color-outline);
} }
div > :global(.component-wrapper:nth-child(3)) { div > :global(.component-wrapper:nth-child(3)) {
border-top: none !important; border-top: none !important;

View File

@@ -1,11 +1,10 @@
<script lang="ts"> <script lang="ts">
import '$lib/app.css'; import '$lib/app.css';
import { Checkbox, Details, Float, Select, ShortCut, Vec3 } from '$lib/index.js'; import { Details, InputCheckbox, InputNumber, InputSelect, InputVec3, ShortCut } from '$lib';
import Section from './Section.svelte'; import Section from './Section.svelte';
let intValue = $state(0); let intValue = $state(0);
let floatValue = $state(0.2); let floatValue = $state(0.2);
let float2Value = $state(0.02);
let float3Value = $state(1); let float3Value = $state(1);
let vecValue = $state([0.2, 0.3, 0.4]); let vecValue = $state([0.2, 0.3, 0.4]);
const options = ['strawberry', 'raspberry', 'chickpeas']; const options = ['strawberry', 'raspberry', 'chickpeas'];
@@ -16,6 +15,7 @@
let detailsOpen = $state(false); let detailsOpen = $state(false);
const themes = [ const themes = [
'dark',
'light', 'light',
'solarized', 'solarized',
'catppuccin', 'catppuccin',
@@ -31,40 +31,63 @@
} }
document.documentElement.classList.add(`theme-${themes[themeIndex]}`); document.documentElement.classList.add(`theme-${themes[themeIndex]}`);
}); });
const colors = [
'layer-0',
'layer-1',
'layer-2',
'layer-3',
'active',
'selected',
'outline',
'connection',
'edge',
'text'
];
</script> </script>
<main class="flex flex-col gap-8 py-8"> <main class="flex flex-col gap-8 py-8">
<div class="flex gap-4"> <div class="flex gap-4">
<h1 class="text-4xl">@nodarium/ui</h1> <h1 class="text-4xl">@nodarium/ui</h1>
<Select bind:value={themeIndex} options={themes}></Select> <InputSelect bind:value={themeIndex} options={themes}></InputSelect>
</div> </div>
<Section title="Integer (step inherit)" value={intValue}> <Section title="Colors">
<Float bind:value={intValue} max={2} /> <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> </Section>
<Section title="Float (step inherit)" value={floatValue}> <Section title="InputNumber">
<Float bind:value={floatValue} /> <div class="flex flex-col gap-2">
</Section> <p>value={floatValue}</p>
<InputNumber bind:value={floatValue} />
<Section title="Float 2 (step inherit)" value={intValue}> <p>min=0 max=10 step=1 value={intValue}</p>
<Float bind:value={float2Value} /> <InputNumber bind:value={intValue} min={0} max={10} step={1} />
</Section> <p>min=0 max=3 step=0.01 value={float3Value}</p>
<InputNumber bind:value={float3Value} step={0.01} max={3} />
<Section title="Float (0.01 step)" value={floatValue}> </div>
<Float bind:value={float3Value} step={0.01} max={3} />
</Section> </Section>
<Section title="Vec3" value={JSON.stringify(vecValue)}> <Section title="Vec3" value={JSON.stringify(vecValue)}>
<Vec3 bind:value={vecValue} /> <InputVec3 bind:value={vecValue} />
</Section> </Section>
<Section title="Select" value={d}> <Section title="Select" value={d}>
<Select bind:value={selectValue} {options} /> <InputSelect bind:value={selectValue} {options} />
</Section> </Section>
<Section title="Checkbox" value={checked}> <Section title="Checkbox" value={checked}>
<Checkbox bind:value={checked} /> <InputCheckbox bind:value={checked} />
</Section> </Section>
<Section title="Details" value={detailsOpen}> <Section title="Details" value={detailsOpen}>
@@ -74,7 +97,11 @@
</Section> </Section>
<Section title="Shortcut"> <Section title="Shortcut">
<ShortCut ctrl key="S" /> <div class="flex gap-4">
<ShortCut ctrl key="S" />
<ShortCut alt key="F4" />
<ShortCut alt ctrl key="delete" />
</div>
</Section> </Section>
</main> </main>

View File

@@ -1,13 +1,14 @@
<script lang="ts"> <script lang="ts">
import { type Snippet } from 'svelte'; import { type Snippet } from 'svelte';
let { title, value, children } = $props<{ let { title, value, children, class: _class } = $props<{
title?: string; title?: string;
value?: unknown; value?: unknown;
children?: Snippet; children?: Snippet;
class?: string;
}>(); }>();
</script> </script>
<section class="border border-1/2 mb-4 p-4 flex flex-col gap-4"> <section class="border-outline border-1/2 bg-layer-1 rounded border mb-4 p-4 flex flex-col gap-4 {_class}">
<h3 class="flex gap-2 font-bold"> <h3 class="flex gap-2 font-bold">
{title} {title}
<p class="font-normal! opacity-50!">{value}</p> <p class="font-normal! opacity-50!">{value}</p>