feat(ui): migrate to svelte-5 and storybook

This commit is contained in:
max_richter 2024-11-02 15:15:09 +01:00
parent 9ba26374db
commit a87add30ff
31 changed files with 5926 additions and 1148 deletions

View File

@ -27,6 +27,7 @@ Currently this visual programming language is used to develop https://nodes.max-
- [Node.js](https://nodejs.org/en/download) - [Node.js](https://nodejs.org/en/download)
- [pnpm](https://pnpm.io/installation) - [pnpm](https://pnpm.io/installation)
- [rust](https://www.rust-lang.org/tools/install) - [rust](https://www.rust-lang.org/tools/install)
- wasm-pack
### Install dependencies ### Install dependencies

View File

@ -4,7 +4,7 @@
"build:app": "BASE_PATH=/ui pnpm -r --filter 'ui' build && pnpm -r --filter 'app' build", "build:app": "BASE_PATH=/ui pnpm -r --filter 'ui' build && pnpm -r --filter 'app' build",
"build:nodes": "pnpm -r --filter './nodes/**' build", "build:nodes": "pnpm -r --filter './nodes/**' build",
"dev:nodes": "pnpm -r --parallel --filter './nodes/**' dev", "dev:nodes": "pnpm -r --parallel --filter './nodes/**' dev",
"build:deploy": "pnpm build && cp -r ./packages/ui/build ./app/build/ui", "build:deploy": "pnpm build && cp -r ./packages/ui/storybook-static ./app/build/ui",
"dev": "pnpm -r --filter 'app' --filter './packages/node-registry' dev" "dev": "pnpm -r --filter 'app' --filter './packages/node-registry' dev"
} }
} }

View File

@ -2,11 +2,12 @@
module.exports = { module.exports = {
root: true, root: true,
extends: [ extends: [
'eslint:recommended', "eslint:recommended",
'plugin:@typescript-eslint/recommended', "plugin:@typescript-eslint/recommended",
'plugin:svelte/recommended', "plugin:svelte/recommended",
'prettier' "prettier",
], "plugin:storybook/recommended"
],
parser: '@typescript-eslint/parser', parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'], plugins: ['@typescript-eslint'],
parserOptions: { parserOptions: {

View File

@ -2,6 +2,7 @@
node_modules node_modules
/build /build
/dist /dist
/storybook-static
/.svelte-kit /.svelte-kit
/package /package
.env .env
@ -9,3 +10,5 @@ node_modules
!.env.example !.env.example
vite.config.js.timestamp-* vite.config.js.timestamp-*
vite.config.ts.timestamp-* vite.config.ts.timestamp-*
*storybook.log

View File

@ -0,0 +1,21 @@
import type { StorybookConfig } from '@storybook/sveltekit';
const config: StorybookConfig = {
"stories": [
"../src/**/*.stories.@(js|ts|svelte)"
],
"addons": [
"@storybook/addon-svelte-csf",
"@storybook/addon-essentials",
"@storybook/addon-themes",
],
"framework": {
"name": "@storybook/sveltekit",
"options": {}
},
docs: {}
};
export default config;

View File

@ -0,0 +1,12 @@
<style>
.sidebar-header {
display: none !important;
}
#downshift-0-label {
display: none !important;
}
#downshift-0-label ~ div {
margin-top: 0 !important;
}
</style>

View File

@ -0,0 +1,29 @@
import { withThemeByClassName } from "@storybook/addon-themes";
import type { Preview } from '@storybook/svelte';
import "../src/lib/app.css";
const preview: Preview = {
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
},
decorators: [withThemeByClassName({
themes: {
dark: 'theme-dark',
light: 'theme-light',
catppuccin: 'theme-catppuccin',
solarized: 'theme-solarized',
high: 'theme-high-contrast',
nord: 'theme-nord',
dracula: 'theme-dracula',
},
defaultTheme: 'light',
})],
};
export default preview;

View File

@ -1,12 +0,0 @@
import { defineConfig } from 'histoire'
import { HstSvelte } from '@histoire/plugin-svelte'
export default defineConfig({
setupFile: '/src/histoire.setup.ts',
storyMatch: [
'./src/lib/**/*.story.svelte',
],
plugins: [
HstSvelte(),
],
})

View File

@ -11,9 +11,8 @@
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"test": "vitest", "test": "vitest",
"lint": "eslint .", "lint": "eslint .",
"story:dev": "histoire dev", "story:dev": "storybook dev -p 6006",
"story:build": "histoire build", "story:build": "storybook build"
"story:preview": "histoire preview"
}, },
"exports": { "exports": {
".": { ".": {
@ -31,24 +30,28 @@
"svelte": "^4.0.0" "svelte": "^4.0.0"
}, },
"devDependencies": { "devDependencies": {
"@histoire/plugin-svelte": "^0.17.17", "@storybook/addon-essentials": "^8.4.1",
"@sveltejs/adapter-auto": "^3.2.0", "@storybook/addon-svelte-csf": "^5.0.0-next.10",
"@storybook/addon-themes": "^8.4.1",
"@storybook/svelte": "^8.4.1",
"@storybook/sveltekit": "^8.4.1",
"@sveltejs/adapter-static": "^3.0.1", "@sveltejs/adapter-static": "^3.0.1",
"@sveltejs/kit": "^2.5.7", "@sveltejs/kit": "^2.5.27",
"@sveltejs/package": "^2.3.1", "@sveltejs/package": "^2.3.1",
"@sveltejs/vite-plugin-svelte": "^3.1.0", "@sveltejs/vite-plugin-svelte": "^4.0.0",
"@types/eslint": "^8.56.10", "@types/eslint": "^8.56.10",
"@typescript-eslint/eslint-plugin": "^7.7.1", "@typescript-eslint/eslint-plugin": "^7.7.1",
"@typescript-eslint/parser": "^7.7.1", "@typescript-eslint/parser": "^7.7.1",
"eslint": "^9.1.1", "eslint": "^9.1.1",
"eslint-plugin-svelte": "^2.38.0", "eslint-plugin-storybook": "^0.10.1",
"histoire": "^0.17.17", "eslint-plugin-svelte": "^2.45.1",
"publint": "^0.2.7", "publint": "^0.2.7",
"svelte": "^4.2.15", "storybook": "^8.4.1",
"svelte-check": "^3.7.0", "svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"tslib": "^2.6.2", "tslib": "^2.6.2",
"typescript": "^5.4.5", "typescript": "^5.5.0",
"vite": "^5.2.10", "vite": "^5.4.4",
"vitest": "^1.5.2" "vitest": "^1.5.2"
}, },
"svelte": "./dist/index.js", "svelte": "./dist/index.js",

View File

@ -1 +0,0 @@
import "./lib/app.css"

View File

@ -1,12 +1,17 @@
<script lang="ts"> <script lang="ts">
export let title = "Details"; interface Props {
export let transparent = false; title?: string;
transparent?: boolean;
children?: import('svelte').Snippet;
}
let { title = "Details", transparent = false, children }: Props = $props();
</script> </script>
<details class:transparent> <details class:transparent>
<summary>{title}</summary> <summary>{title}</summary>
<div class="content"> <div class="content">
<slot /> {@render children?.()}
</div> </div>
</details> </details>

View File

@ -7,9 +7,13 @@
import type { NodeInput } from '@nodes/types'; import type { NodeInput } from '@nodes/types';
import Vec3 from './elements/Vec3.svelte'; import Vec3 from './elements/Vec3.svelte';
export let input: NodeInput; interface Props {
export let value: any; input: NodeInput;
export let id: string; value: any;
id: string;
}
let { input, value = $bindable(), id }: Props = $props();
</script> </script>
{#if input.type === 'float'} {#if input.type === 'float'}

View File

@ -1,8 +1,17 @@
<script lang="ts"> <script lang="ts">
export let ctrl = false; interface Props {
export let shift = false; ctrl?: boolean;
export let alt = false; shift?: boolean;
export let key: string; alt?: boolean;
key: string;
}
let {
ctrl = false,
shift = false,
alt = false,
key
}: Props = $props();
</script> </script>
<div class="command"> <div class="command">

View File

@ -38,9 +38,7 @@
} }
body { html {
overflow: hidden;
--neutral-100: #E7E7E7; --neutral-100: #E7E7E7;
--neutral-200: #CECECE; --neutral-200: #CECECE;
--neutral-300: #7C7C7C; --neutral-300: #7C7C7C;
@ -65,6 +63,11 @@ body {
--edge: var(--connection, var(--outline)); --edge: var(--connection, var(--outline));
--text-color: var(--neutral-200); --text-color: var(--neutral-200);
}
body {
overflow: hidden;
color: var(--text-color); color: var(--text-color);
background-color: var(--layer-0); background-color: var(--layer-0);
@ -74,7 +77,7 @@ body * {
color: var(--text-color); color: var(--text-color);
} }
body.theme-light { html.theme-light {
--text-color: var(--neutral-800); --text-color: var(--neutral-800);
--outline: var(--neutral-300); --outline: var(--neutral-300);
--layer-0: var(--neutral-100); --layer-0: var(--neutral-100);
@ -86,7 +89,7 @@ body.theme-light {
--connection: #888; --connection: #888;
} }
body.theme-solarized { html.theme-solarized {
--text-color: #425055; --text-color: #425055;
--outline: #93a1a1; --outline: #93a1a1;
--layer-0: #fdf6e3; --layer-0: #fdf6e3;
@ -98,7 +101,7 @@ body.theme-solarized {
--connection: #839496; --connection: #839496;
} }
body.theme-catppuccin { html.theme-catppuccin {
--text-color: #CDD6F4; --text-color: #CDD6F4;
--outline: #3e3e4f; --outline: #3e3e4f;
--layer-3: #a8aac8; --layer-3: #a8aac8;
@ -108,7 +111,7 @@ body.theme-catppuccin {
--connection: #444459; --connection: #444459;
} }
body.theme-high-contrast { html.theme-high-contrast {
--text-color: #FFFFFF; --text-color: #FFFFFF;
--outline: white; --outline: white;
--layer-0: #000000; --layer-0: #000000;
@ -118,7 +121,7 @@ body.theme-high-contrast {
--connection: #FFF; --connection: #FFF;
} }
body.theme-nord { html.theme-nord {
--text-color: #D8DEE9; --text-color: #D8DEE9;
--outline: #4C566A; --outline: #4C566A;
--layer-0: #2E3440; --layer-0: #2E3440;
@ -130,7 +133,7 @@ body.theme-nord {
--connection: #4C566A; --connection: #4C566A;
} }
body.theme-dracula { html.theme-dracula {
--text-color: #F8F8F2; --text-color: #F8F8F2;
--outline: #6272A4; --outline: #6272A4;
--layer-0: #282A36; --layer-0: #282A36;

View File

@ -1,13 +1,21 @@
<script lang="ts"> <script lang="ts">
export let value: boolean; import { run } from 'svelte/legacy';
$: if (typeof value === 'string') {
value = value === 'true';
} else if (typeof value === 'number') { interface Props {
value = value === 1; value: boolean;
id?: string;
} }
export let id = ''; let { value = $bindable(), id = '' }: Props = $props();
run(() => {
if (typeof value === 'string') {
value = value === 'true';
} else if (typeof value === 'number') {
value = value === 1;
}
});
</script> </script>
<input {id} type="checkbox" bind:checked={value} /> <input {id} type="checkbox" bind:checked={value} />

View File

@ -0,0 +1,11 @@
<script module>
import { defineMeta } from '@storybook/addon-svelte-csf';
import FloatComp from './Float.svelte';
const { Story } = defineMeta({
title: 'Inputs/Float',
component: FloatComp
});
</script>
<Story name="Float" />

View File

@ -1,17 +0,0 @@
<script lang="ts">
import type { Hst } from '@histoire/plugin-svelte';
export let Hst: Hst;
import Float from './Float.svelte';
import StoryContent from '$lib/helpers/StoryContent.svelte';
import StorySettings from '$lib/helpers/StorySettings.svelte';
let theme = 'dark';
</script>
<Hst.Story>
<StoryContent {theme}>
<Float value={0} min={0} max={6.9} />
</StoryContent>
<svelte:fragment slot="controls">
<StorySettings bind:theme />
</svelte:fragment>
</Hst.Story>

View File

@ -1,9 +1,19 @@
<script lang="ts"> <script lang="ts">
export let value = 0.5; interface Props {
export let step = 0.01; value?: number;
export let min = 0; step?: number;
export let max = 1; min?: number;
export let id = ''; max?: number;
id?: string;
}
let {
value = $bindable(0.5),
step = 0.01,
min = $bindable(0),
max = $bindable(1),
id = ''
}: Props = $props();
if (min > max) { if (min > max) {
[min, max] = [max, min]; [min, max] = [max, min];
@ -16,23 +26,15 @@
return +parseFloat(input + '').toPrecision(2); return +parseFloat(input + '').toPrecision(2);
} }
let inputEl: HTMLInputElement; let inputEl: HTMLInputElement | undefined = $state();
$: if ((value || 0).toString().length > 5) {
value = strip(value || 0);
}
$: value !== undefined && handleChange();
let oldValue: number; let oldValue: number;
function handleChange() { function handleChange() {
if (value === oldValue) return; if (value === oldValue) return;
oldValue = value; oldValue = value;
} }
$: width = Number.isFinite(value) let isMouseDown = $state(false);
? Math.max((value?.toString().length ?? 1) * 8, 50) + 'px'
: '20px';
let isMouseDown = false;
let downV = 0; let downV = 0;
let vx = 0; let vx = 0;
let rect: DOMRect; let rect: DOMRect;
@ -40,6 +42,8 @@
function handleMouseDown(ev: MouseEvent) { function handleMouseDown(ev: MouseEvent) {
ev.preventDefault(); ev.preventDefault();
if (!inputEl) return;
inputEl.focus(); inputEl.focus();
isMouseDown = true; isMouseDown = true;
@ -57,7 +61,7 @@
isMouseDown = false; isMouseDown = false;
if (downV === value) { if (downV === value) {
inputEl.focus(); inputEl?.focus();
} }
if (value > max) { if (value > max) {
@ -76,7 +80,7 @@
function handleKeyDown(ev: KeyboardEvent) { function handleKeyDown(ev: KeyboardEvent) {
if (ev.key === 'Escape' || ev.key === 'Enter') { if (ev.key === 'Escape' || ev.key === 'Enter') {
handleMouseUp(); handleMouseUp();
inputEl.blur(); inputEl?.blur();
} }
} }
@ -90,6 +94,15 @@
value = Math.max(Math.min(min + (max - min) * vx, max), min); value = Math.max(Math.min(min + (max - min) * vx, max), min);
} }
} }
$effect(() => {
if ((value || 0).toString().length > 5) {
value = strip(value || 0);
}
value !== undefined && handleChange();
});
let width = $derived(
Number.isFinite(value) ? Math.max((value?.toString().length ?? 1) * 8, 50) + 'px' : '20px'
);
</script> </script>
<div class="component-wrapper" class:is-down={isMouseDown}> <div class="component-wrapper" class:is-down={isMouseDown}>
@ -101,9 +114,9 @@
{step} {step}
{max} {max}
{min} {min}
on:keydown={handleKeyDown} onkeydown={handleKeyDown}
on:mousedown={handleMouseDown} onmousedown={handleMouseDown}
on:mouseup={handleMouseUp} onmouseup={handleMouseUp}
type="number" type="number"
style={`width:${width};`} style={`width:${width};`}
/> />

View File

@ -0,0 +1,11 @@
<script module>
import { defineMeta } from '@storybook/addon-svelte-csf';
import IntegerComp from './Integer.svelte';
const { Story } = defineMeta({
title: 'Inputs/Integer',
component: IntegerComp
});
</script>
<Story name="Integer" />

View File

@ -1,18 +0,0 @@
<script lang="ts">
import type { Hst } from '@histoire/plugin-svelte';
export let Hst: Hst;
import Integer from './Integer.svelte';
import StorySettings from '../helpers/StorySettings.svelte';
import StoryContent from '$lib/helpers/StoryContent.svelte';
let theme = 'dark';
</script>
<Hst.Story>
<StoryContent {theme}>
<Integer value={5} min={0} max={42} />
</StoryContent>
<svelte:fragment slot="controls">
<StorySettings bind:theme />
</svelte:fragment>
</Hst.Story>

View File

@ -2,20 +2,28 @@
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
// Styling interface Props {
export let min: number | undefined = undefined; min?: number | undefined;
export let max: number | undefined = undefined; max?: number | undefined;
export let step = 1; step?: number;
export let value = 0; value?: number;
export let id = ''; id?: string;
}
let {
min = undefined,
max = undefined,
step = 1,
value = $bindable(0),
id = ''
}: Props = $props();
if (!value) { if (!value) {
value = 0; value = 0;
} }
let inputEl: HTMLInputElement; let inputEl: HTMLInputElement | undefined = $state();
let wrapper: HTMLDivElement; let wrapper: HTMLDivElement | undefined = $state();
$: value !== undefined && update();
let prev = -1; let prev = -1;
function update() { function update() {
@ -24,10 +32,6 @@
dispatch('change', parseFloat(value + '')); dispatch('change', parseFloat(value + ''));
} }
$: width = Number.isFinite(value)
? Math.max((value?.toString().length ?? 1) * 8, 30) + 'px'
: '20px';
function handleChange(change: number) { function handleChange(change: number) {
value = Math.max(min ?? -Infinity, Math.min(+value + change, max ?? Infinity)); value = Math.max(min ?? -Infinity, Math.min(+value + change, max ?? Infinity));
} }
@ -70,6 +74,13 @@
value = downV + Math.round(vx / 10); value = downV + Math.round(vx / 10);
} }
} }
$effect(() => {
value !== undefined && update();
});
let width = $derived(
Number.isFinite(value) ? Math.max((value?.toString().length ?? 1) * 8, 30) + 'px' : '20px'
);
</script> </script>
<div <div
@ -78,13 +89,14 @@
role="slider" role="slider"
tabindex="0" tabindex="0"
aria-valuenow={value} aria-valuenow={value}
on:mousedown={handleMouseDown} onmousedown={handleMouseDown}
on:mouseup={handleMouseUp} onmouseup={handleMouseUp}
> >
{#if typeof min !== 'undefined' && typeof max !== 'undefined'} {#if typeof min !== 'undefined' && typeof max !== 'undefined'}
<span class="overlay" style={`width: ${Math.min((value - min) / (max - min), 1) * 100}%`} /> <span class="overlay" style={`width: ${Math.min((value - min) / (max - min), 1) * 100}%`}
></span>
{/if} {/if}
<button on:click={() => handleChange(-step)}>-</button> <button onclick={() => handleChange(-step)}>-</button>
<input <input
bind:value bind:value
bind:this={inputEl} bind:this={inputEl}
@ -96,7 +108,7 @@
style={`width:${width};`} style={`width:${width};`}
/> />
<button on:click={() => handleChange(+step)}>+</button> <button onclick={() => handleChange(+step)}>+</button>
</div> </div>
<style> <style>
@ -159,4 +171,3 @@
border-style: none; border-style: none;
} }
</style> </style>

View File

@ -0,0 +1,23 @@
<script module>
import { defineMeta } from '@storybook/addon-svelte-csf';
import SelectComp from '$lib/elements/Select.svelte';
const { Story } = defineMeta({
title: 'Inputs/Select',
component: SelectComp,
argTypes: {
options: {
control: {
type: 'select'
}
}
},
parameters: {
actions: {
handles: ['change']
}
}
});
</script>
<Story name="Select" args={{ options: ['strawberry', 'raspberry', 'chickpeas'] }} />

View File

@ -1,18 +0,0 @@
<script lang="ts">
import type { Hst } from '@histoire/plugin-svelte';
export let Hst: Hst;
import Select from './Select.svelte';
import StoryContent from '$lib/helpers/StoryContent.svelte';
import StorySettings from '$lib/helpers/StorySettings.svelte';
let theme = 'dark';
</script>
<Hst.Story>
<StoryContent {theme}>
<Select id="" options={['strawberry', 'apple', 'banana']} />
</StoryContent>
<svelte:fragment slot="controls">
<StorySettings bind:theme />
</svelte:fragment>
</Hst.Story>

View File

@ -1,7 +1,11 @@
<script lang="ts"> <script lang="ts">
export let options: string[] = []; interface Props {
export let value: number = 0; options?: string[];
export let id = ''; value?: number;
id?: string;
}
let { options = [], value = $bindable(0), id = '' }: Props = $props();
</script> </script>
<select {id} bind:value> <select {id} bind:value>

View File

@ -0,0 +1,10 @@
<script module>
import { defineMeta } from '@storybook/addon-svelte-csf';
import Vec3Comp from './Vec3.svelte';
const { Story } = defineMeta({
title: 'Inputs/Vec3',
component: Vec3Comp
});
</script>
<Story name="Vec3" />

View File

@ -1,18 +0,0 @@
<script lang="ts">
import type { Hst } from '@histoire/plugin-svelte';
export let Hst: Hst;
import Vec3 from './Vec3.svelte';
import StoryContent from '$lib/helpers/StoryContent.svelte';
import StorySettings from '$lib/helpers/StorySettings.svelte';
let theme = 'dark';
</script>
<Hst.Story>
<StoryContent {theme}>
<Vec3 value={[0.2, 0.4, 0.6]} />
</StoryContent>
<svelte:fragment slot="controls">
<StorySettings bind:theme />
</svelte:fragment>
</Hst.Story>

View File

@ -1,8 +1,12 @@
<script lang="ts"> <script lang="ts">
import Float from './Float.svelte'; import Float from './Float.svelte';
export let value = [0, 0, 0]; interface Props {
export let id = ''; value?: any;
id?: string;
}
let { value = $bindable([0, 0, 0]), id = '' }: Props = $props();
</script> </script>
<div> <div>

View File

@ -1,24 +0,0 @@
<script lang="ts">
export let theme = 'dark';
$: if (theme !== undefined) {
const classes = document.body.classList;
const newClassName = `theme-${theme}`;
for (const className of classes) {
if (className.startsWith('theme-') && className !== newClassName) {
classes.remove(className);
}
}
document.body.classList.add(newClassName);
}
</script>
<div>
<slot />
</div>
<style>
div {
padding: 1em;
}
</style>

View File

@ -1,23 +0,0 @@
<script lang="ts">
import { Select } from '$lib/index.js';
const themes = ['dark', 'light', 'catppuccin', 'solarized', 'high-contrast', 'nord', 'dracula'];
let value = 0;
export let theme = themes[value];
$: theme = themes[value];
</script>
<div>
<label for="theme-select"> Select Theme </label>
<Select id="" bind:value options={themes} />
</div>
<style>
div {
display: flex;
flex-direction: column;
gap: 1em;
padding: 1em;
}
</style>

View File

@ -4,9 +4,9 @@
import Integer from '$lib/elements/Integer.svelte'; import Integer from '$lib/elements/Integer.svelte';
import Vec3 from '$lib/elements/Vec3.svelte'; import Vec3 from '$lib/elements/Vec3.svelte';
let intValue = 0; let intValue = $state(0);
let floatValue = 0.2; let floatValue = $state(0.2);
let vecValue = [0.2, 0.3, 0.4]; let vecValue = $state([0.2, 0.3, 0.4]);
</script> </script>
<main> <main>

File diff suppressed because it is too large Load Diff