refactor: migrate to svelte 5

This commit is contained in:
Tomáš Mládek 2025-09-26 00:01:59 +02:00
parent f37019c3ef
commit 2d5c5f4130
23 changed files with 3529 additions and 126 deletions

View file

@ -31,7 +31,7 @@
"prettier": "^3.5.0",
"prettier-plugin-svelte": "^3.3.3",
"puppeteer": "^22.15.0",
"svelte-check": "^3.8.6",
"svelte-check": "^4.0.0",
"wait-on": "^7.2.0"
},
"type": "module",
@ -41,13 +41,13 @@
"@sveltejs/adapter-auto": "^3.3.1",
"@sveltejs/adapter-static": "^3.0.8",
"@sveltejs/kit": "^2.17.1",
"@sveltejs/vite-plugin-svelte": "^3.1.2",
"@sveltejs/vite-plugin-svelte": "^4.0.0",
"@tabler/icons-webfont": "^2.47.0",
"debug": "^4.4.0",
"i18next": "^23.16.8",
"lodash": "^4.17.21",
"normalize.css": "^8.0.1",
"svelte": "^4.2.19",
"svelte": "^5.0.0",
"svelte-i18next": "^2.2.2",
"tslib": "^2.8.1",
"typescript": "^5.7.3",

3333
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,8 +1,8 @@
<script>
import { onMount } from 'svelte';
let heightOdd = false;
let widthOdd = false;
let heightOdd = $state(false);
let widthOdd = $state(false);
function updateOdd() {
heightOdd = window.innerHeight % 2 === 1;

View file

@ -7,17 +7,21 @@
const MAX_COUNT = 33;
const MARGIN_SIZE = 16;
let horizontalCount = START_COUNT;
let verticalCount = START_COUNT;
let blockSize = 64;
let cornerBlocks = 2;
let horizontalCount = $state(START_COUNT);
let verticalCount = $state(START_COUNT);
let blockSize = $state(64);
let cornerBlocks = $state(2);
let horizontalMargin = MARGIN_SIZE;
let verticalMargin = MARGIN_SIZE;
let unloaded = true;
let horizontalMargin = $state(MARGIN_SIZE);
let verticalMargin = $state(MARGIN_SIZE);
let unloaded = $state(true);
export let transparent = false;
export let subdued = false;
interface Props {
transparent?: boolean;
subdued?: boolean;
}
let { transparent = false, subdued = false }: Props = $props();
function updateCounts() {
const gridWidth = window.innerWidth - MARGIN_SIZE;

View file

@ -1,7 +1,7 @@
<script lang="ts">
import { onMount } from 'svelte';
let time = new Date();
let time = $state(new Date());
onMount(() => {
setInterval(() => {

View file

@ -3,9 +3,9 @@
import { fade } from 'svelte/transition';
import { i18n } from '$lib/i18n';
let screenResolution = '... x ...';
let windowResolution = '';
let dpr = "1";
let screenResolution = $state('... x ...');
let windowResolution = $state('');
let dpr = $state("1");
function updateResolution() {
const realWidth = Math.round(screen.width) * window.devicePixelRatio;

View file

@ -8,25 +8,29 @@
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher<{ focus: void }>();
export let bg = false;
interface Props {
bg?: boolean;
}
let sizes = {
let { bg = false }: Props = $props();
let sizes = $state({
blockSize: 64,
horizontalCount: 16,
verticalCount: 16,
horizontalMargin: 0,
verticalMargin: 0
};
});
$: columnWidth = sizes.horizontalCount % 2 === 0 ? 3 : 4;
$: columnHeight = 2 * Math.floor((sizes.verticalCount * 0.75) / 2) + (sizes.verticalCount % 2);
$: leftColumn = sizes.horizontalCount / 4 - columnWidth / 2;
$: circleBlocks =
2 * Math.floor((Math.min(sizes.horizontalCount, sizes.verticalCount) * 0.66) / 2) +
(sizes.horizontalCount % 2);
let columnWidth = $derived(sizes.horizontalCount % 2 === 0 ? 3 : 4);
let columnHeight = $derived(2 * Math.floor((sizes.verticalCount * 0.75) / 2) + (sizes.verticalCount % 2));
let leftColumn = $derived(sizes.horizontalCount / 4 - columnWidth / 2);
let circleBlocks =
$derived(2 * Math.floor((Math.min(sizes.horizontalCount, sizes.verticalCount) * 0.66) / 2) +
(sizes.horizontalCount % 2));
</script>
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class="test-card"
class:bg
@ -37,7 +41,7 @@
--column-width: {columnWidth};
--column-height: {columnHeight};
--left-column: {leftColumn};"
on:dblclick={() => dispatch('focus') && document.body.requestFullscreen()}
ondblclick={() => dispatch('focus') && document.body.requestFullscreen()}
>
<BackgroundGrid on:change={(ev) => (sizes = ev.detail)} subdued={bg} />

View file

@ -1,9 +1,14 @@
<script>
<script lang="ts">
import { i18n } from '$lib/i18n';
interface Props {
children?: import('svelte').Snippet;
}
let { children }: Props = $props();
</script>
<a href=".." class="hide-idle"><i class="ti ti-arrow-back"></i> {$i18n.t('Back')}</a>
<slot />
{@render children?.()}
<style>
a {

View file

@ -1,10 +1,10 @@
<script lang="ts">
import TestCard from '$lib/TestCard.svelte';
let x = -1;
let y = -1;
let leftButton = false;
let rightButton = false;
let x = $state(-1);
let y = $state(-1);
let leftButton = $state(false);
let rightButton = $state(false);
function onMouseMove(ev: MouseEvent) {
x = ev.x;
@ -31,10 +31,10 @@
</script>
<svelte:body
on:mousemove={onMouseMove}
on:mousedown={onMouseDown}
on:mouseup={onMouseUp}
on:contextmenu={(ev) => {
onmousemove={onMouseMove}
onmousedown={onMouseDown}
onmouseup={onMouseUp}
oncontextmenu={(ev) => {
ev.preventDefault();
return false;
}}

View file

@ -1,14 +1,19 @@
<script lang="ts">
import TestCard from '$lib/TestCard.svelte';
import { page } from '$app/stores';
import { page } from '$app/state';
import { goto } from '$app/navigation';
import { i18n } from '$lib/i18n';
interface Props {
children?: import('svelte').Snippet;
}
let { children }: Props = $props();
</script>
<TestCard bg on:focus={() => goto('/card')} />
<main class:sub={!$page.data.root}>
<a href=".." class="button button-back"><i class="ti ti-arrow-back" />{$i18n.t('Back')}</a>
<slot />
<main class:sub={!page.data.root}>
<a href=".." class="button button-back"><i class="ti ti-arrow-back"></i>{$i18n.t('Back')}</a>
{@render children?.()}
</main>
<style>

View file

@ -1,10 +1,12 @@
<script lang="ts">
import { run } from 'svelte/legacy';
import { version } from '../../../package.json';
import { i18n } from '$lib/i18n';
import type { Snapshot } from '@sveltejs/kit';
const buildDate = import.meta.env.VITE_BUILD_DATE || "???";
let search = '';
let search = $state('');
type Entry = {
id: string;
@ -126,8 +128,8 @@
}
];
let filteredTests: Test[] = tests;
let filteredCategories: Category[] = [];
let filteredTests: Test[] = $state(tests);
let filteredCategories: Category[] = $state([]);
function doSearch(search: string) {
filteredTests = tests.filter((test) => {
@ -141,7 +143,9 @@
);
});
}
$: doSearch(search);
run(() => {
doSearch(search);
});
function setFilter(category: Category) {
if (filteredCategories.includes(category)) {
@ -151,13 +155,13 @@
}
}
$: nonEmptyCategories = categories.filter((category) => {
let nonEmptyCategories = $derived(categories.filter((category) => {
const categoryTests = filteredTests.filter((test) => test.categories.includes(category.id));
return categoryTests.some(
(test) =>
!filteredCategories.length || filteredCategories.every((f) => test.categories.includes(f))
);
});
}));
export const snapshot: Snapshot<string> = {
capture: () => JSON.stringify({ filtered: filteredCategories, search }),
@ -172,13 +176,13 @@
<h1>Total Tech Test</h1>
<nav>
<!-- svelte-ignore a11y-autofocus -->
<!-- svelte-ignore a11y_autofocus -->
<input type="search" placeholder={$i18n.t('Search')} bind:value={search} autofocus />
<div class="options">
{#each superCategories as category}
<button
on:click={() => setFilter(category.id)}
onclick={() => setFilter(category.id)}
class:active={!filteredCategories.length || filteredCategories.includes(category.id)}
class="super"
>
@ -189,7 +193,7 @@
<div class="separator"></div>
{#each categories as category}
<button
on:click={() => setFilter(category.id)}
onclick={() => setFilter(category.id)}
class:active={!filteredCategories.length || filteredCategories.includes(category.id)}
>
<i class="ti {category.icon}"></i>

View file

@ -1,11 +1,16 @@
<script lang="ts">
import CycleButton from './cycle-button.svelte';
interface Props {
children?: import('svelte').Snippet;
}
let channelsEl: HTMLDivElement;
let { children }: Props = $props();
let channelsEl: HTMLDivElement = $state();
</script>
<div class="channels" bind:this={channelsEl}>
<slot />
{@render children?.()}
</div>
<div class="controls">
<CycleButton element={channelsEl} />

View file

@ -2,9 +2,13 @@
import { onDestroy } from 'svelte';
import { i18n } from '$lib/i18n';
export let element: HTMLElement;
interface Props {
element: HTMLElement;
}
let cycling = false;
let { element }: Props = $props();
let cycling = $state(false);
let currentChannel: HTMLAudioElement | undefined;
async function cycleChannels() {
cycling = true;
@ -44,7 +48,7 @@
});
</script>
<button on:click={onClick}>
<button onclick={onClick}>
<i class="ti ti-refresh"></i>
{#if cycling}
{$i18n.t('Stop Cycling')}

View file

@ -1,13 +1,26 @@
<script lang="ts">
export let src: string;
export let left = false;
export let center = false;
export let right = false;
export let lfe = false;
export let inline = false;
interface Props {
src: string;
left?: boolean;
center?: boolean;
right?: boolean;
lfe?: boolean;
inline?: boolean;
children?: import('svelte').Snippet;
}
let currentTime = 0;
let paused = true;
let {
src,
left = false,
center = false,
right = false,
lfe = false,
inline = false,
children
}: Props = $props();
let currentTime = $state(0);
let paused = $state(true);
function play() {
currentTime = 0;
paused = false;
@ -22,14 +35,14 @@
class:lfe
class:inline
class:playing={!paused}
on:click={play}
onclick={play}
>
{#if !lfe}
<i class="ti ti-volume"></i>
{:else}
<i class="ti ti-wave-sine"></i>
{/if}
<span><slot /></span>
<span>{@render children?.()}</span>
<audio bind:currentTime bind:paused {src}></audio>
</button>

View file

@ -6,7 +6,7 @@
import CycleButton from './cycle-button.svelte';
import { i18n } from '$lib/i18n';
let speakersEl: HTMLElement;
let speakersEl: HTMLElement = $state();
</script>
<div class="test">

View file

@ -1,6 +1,11 @@
<script lang="ts">
import { i18n } from '$lib/i18n';
interface Props {
children?: import('svelte').Snippet;
}
let { children }: Props = $props();
</script>
<h2><i class="ti ti-volume"></i> {$i18n.t('Audio test')}</h2>
<slot />
{@render children?.()}

View file

@ -2,8 +2,8 @@
import { onMount } from 'svelte';
import { i18n } from '$lib/i18n';
let frequency = 60;
let playing = false;
let frequency = $state(60);
let playing = $state(false);
let audioCtx: AudioContext | undefined;
let oscillatorL: OscillatorNode | undefined;
@ -58,9 +58,9 @@
<input type="number" bind:value={frequency} min="20" max="20000" disabled={playing} />Hz
</label>
<div class="controls">
<button on:click={() => start('inPhase')}>{$i18n.t('In Phase')}</button>
<button on:click={() => start('outOfPhase')}>{$i18n.t('Out of Phase')}</button>
<button class="stop" on:click={stop} disabled={!playing}>{$i18n.t('Stop')}</button>
<button onclick={() => start('inPhase')}>{$i18n.t('In Phase')}</button>
<button onclick={() => start('outOfPhase')}>{$i18n.t('Out of Phase')}</button>
<button class="stop" onclick={stop} disabled={!playing}>{$i18n.t('Stop')}</button>
</div>
</div>

View file

@ -1,18 +1,18 @@
<script lang="ts">
import videoUrl from '@assets/avsync.webm';
import { i18n } from '$lib/i18n';
let paused = true;
let paused = $state(true);
</script>
<h2><i class="ti ti-time-duration-off"></i> {$i18n.t('Audio/Video Synchronization')}</h2>
<!-- svelte-ignore a11y-media-has-caption -->
<!-- svelte-ignore a11y_media_has_caption -->
<video
class:playing={!paused}
autoplay
loop
bind:paused
src={videoUrl}
on:click={() => (paused = false)}
onclick={() => (paused = false)}
></video>
<style>

View file

@ -1,25 +1,31 @@
<script lang="ts">
import { run } from 'svelte/legacy';
import { onDestroy, onMount } from 'svelte';
import { browser } from '$app/environment';
import debug from 'debug';
import { i18n } from '$lib/i18n';
const dbg = debug('app:camera');
let video: HTMLVideoElement;
let devices: MediaDeviceInfo[] = [];
let currentDevice: string | undefined;
let video: HTMLVideoElement = $state();
let devices: MediaDeviceInfo[] = $state([]);
let currentDevice: string | undefined = $state();
let requestResolution: [number, number] | 'auto' = 'auto';
let requestFramerate: number | 'auto' = 'auto';
let requestResolution: [number, number] | 'auto' = $state('auto');
let requestFramerate: number | 'auto' = $state('auto');
let deviceInfo: {
resolution?: string;
frameRate?: number;
} = {};
let snapshot: string | undefined;
let flipped = false;
} = $state({});
let snapshot: string | undefined = $state();
let flipped = $state(false);
$: dbg('devices %O', devices);
$: dbg('currentDevice %s', currentDevice);
run(() => {
dbg('devices %O', devices);
});
run(() => {
dbg('currentDevice %s', currentDevice);
});
onMount(() => {
refreshDevices();
@ -48,21 +54,23 @@
}
}
$: if (currentDevice) {
navigator.mediaDevices
.getUserMedia({
video: {
deviceId: currentDevice,
width: requestResolution === 'auto' ? undefined : requestResolution[0],
height: requestResolution === 'auto' ? undefined : requestResolution[1],
frameRate: requestFramerate === 'auto' ? undefined : requestFramerate
}
})
.then((stream) => {
video.srcObject = stream;
refreshDevices();
});
}
run(() => {
if (currentDevice) {
navigator.mediaDevices
.getUserMedia({
video: {
deviceId: currentDevice,
width: requestResolution === 'auto' ? undefined : requestResolution[0],
height: requestResolution === 'auto' ? undefined : requestResolution[1],
frameRate: requestFramerate === 'auto' ? undefined : requestFramerate
}
})
.then((stream) => {
video.srcObject = stream;
refreshDevices();
});
}
});
async function takeSnapshot() {
const canvas = document.createElement('canvas');
@ -92,7 +100,7 @@
{/each}
</select>
</label>
<button on:click={refreshDevices}>
<button onclick={refreshDevices}>
<i class="ti ti-refresh"></i>
{$i18n.t('Refresh')}
</button>
@ -124,13 +132,13 @@
</div>
<div class="display" class:snapshot={Boolean(snapshot)}>
<!-- svelte-ignore a11y-media-has-caption -->
<!-- svelte-ignore a11y_media_has_caption -->
<video class:flipped bind:this={video} autoplay class:unloaded={!currentDevice}></video>
{#if snapshot}
<!-- svelte-ignore a11y-missing-attribute -->
<!-- svelte-ignore a11y_missing_attribute -->
<!--suppress HtmlRequiredAltAttribute -->
<img src={snapshot} />
<button on:click={() => (snapshot = undefined)}><i class="ti ti-x"></i></button>
<button onclick={() => (snapshot = undefined)}><i class="ti ti-x"></i></button>
{/if}
</div>
@ -149,11 +157,11 @@
{/key}
</ul>
<div class="controls">
<button on:click={takeSnapshot}>
<button onclick={takeSnapshot}>
<i class="ti ti-camera"></i>
{$i18n.t('Take picture')}
</button>
<button on:click={() => (flipped = !flipped)}>
<button onclick={() => (flipped = !flipped)}>
<i class="ti ti-flip-vertical"></i>
{#if flipped}
{$i18n.t('Unflip image')}

View file

@ -1,4 +1,6 @@
<script lang="ts">
import { run } from 'svelte/legacy';
import { onMount } from 'svelte';
import { browser } from '$app/environment';
import debug from 'debug';
@ -6,10 +8,10 @@
const dbg = debug('app:camera');
let gamepads: Gamepad[] = [];
let currentGamepad: Gamepad | undefined;
let buttons: GamepadButton[] = [];
let axes: number[] = [];
let gamepads: Gamepad[] = $state([]);
let currentGamepad: Gamepad | undefined = $state();
let buttons: GamepadButton[] = $state([]);
let axes: number[] = $state([]);
const axisHistory: number[][] = [];
const sizes: [number, number][] = [];
@ -58,17 +60,23 @@
requestAnimationFrame(update);
}
$: {
run(() => {
if (currentGamepad) {
update();
}
}
});
$: dbg('Gamepads %O', gamepads);
$: dbg('Current gamepad %s', currentGamepad);
run(() => {
dbg('Gamepads %O', gamepads);
});
run(() => {
dbg('Current gamepad %s', currentGamepad);
});
$: currentGamepad?.vibrationActuator?.playEffect('dual-rumble', {
duration: 1000
run(() => {
currentGamepad?.vibrationActuator?.playEffect('dual-rumble', {
duration: 1000
});
});
onMount(() => {
@ -105,7 +113,7 @@
{/each}
</select>
</label>
<button on:click={refreshGamepads}>
<button onclick={refreshGamepads}>
<i class="ti ti-refresh"></i>
{$i18n.t('Refresh')}
</button>

View file

@ -2,9 +2,9 @@
import { onMount } from 'svelte';
import { i18n } from '$lib/i18n';
let key: string;
let code: string;
let pressedKeys: string[] = [];
let key: string = $state();
let code: string = $state();
let pressedKeys: string[] = $state([]);
onMount(() => {
document.addEventListener('keydown', (event) => {
key = event.key;

View file

@ -2,10 +2,10 @@
import { onMount } from 'svelte';
import { i18n } from '$lib/i18n';
let time = 0;
let time = $state(0);
let fps = 0;
let start = 0;
let displayFps = '?';
let displayFps = $state('?');
let fpsInterval: NodeJS.Timeout | undefined;
const times: number[] = [];
@ -41,7 +41,7 @@
<div class="time">{time}</div>
<div class="fps">{displayFps} {$i18n.t('FPS')}</div>
</div>
<button on:click={restart}>{$i18n.t('Restart')}</button>
<button onclick={restart}>{$i18n.t('Restart')}</button>
<style>
div,

View file

@ -7,6 +7,11 @@
import '@tabler/icons-webfont/tabler-icons.css';
import '../index.css';
import { onMount } from 'svelte';
interface Props {
children?: import('svelte').Snippet;
}
let { children }: Props = $props();
let idleTimeout: NodeJS.Timeout | undefined;
onMount(() => {
@ -20,7 +25,7 @@
});
</script>
<slot />
{@render children?.()}
<style>
:global(.hide-idle) {