Compare commits
	
		
			No commits in common. "main" and "develop" have entirely different histories.
		
	
	
		
	
		
					 81 changed files with 3533 additions and 5606 deletions
				
			
		| 
						 | 
				
			
			@ -1,6 +1,5 @@
 | 
			
		|||
/** @type { import("eslint").Linter.Config } */
 | 
			
		||||
module.exports = {
 | 
			
		||||
	ignorePatterns: ['av-sync/**', '**/*.js'],
 | 
			
		||||
	root: true,
 | 
			
		||||
	extends: [
 | 
			
		||||
		'eslint:recommended',
 | 
			
		||||
| 
						 | 
				
			
			@ -13,8 +12,7 @@ module.exports = {
 | 
			
		|||
	parserOptions: {
 | 
			
		||||
		sourceType: 'module',
 | 
			
		||||
		ecmaVersion: 2020,
 | 
			
		||||
		extraFileExtensions: ['.svelte'],
 | 
			
		||||
		project: './tsconfig.json'
 | 
			
		||||
		extraFileExtensions: ['.svelte']
 | 
			
		||||
	},
 | 
			
		||||
	env: {
 | 
			
		||||
		browser: true,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										1
									
								
								.gitattributes
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitattributes
									
										
									
									
										vendored
									
									
								
							| 
						 | 
				
			
			@ -1,2 +1 @@
 | 
			
		|||
**/*.wav filter=lfs diff=lfs merge=lfs -text
 | 
			
		||||
tests/output/* filter=lfs diff=lfs merge=lfs -text
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										6
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							| 
						 | 
				
			
			@ -11,9 +11,3 @@ node_modules
 | 
			
		|||
!.env.example
 | 
			
		||||
vite.config.js.timestamp-*
 | 
			
		||||
vite.config.ts.timestamp-*
 | 
			
		||||
 | 
			
		||||
# Paraglide
 | 
			
		||||
src/lib/paraglide
 | 
			
		||||
 | 
			
		||||
tests/output/*-current.png
 | 
			
		||||
tests/output/*-diff.png
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										12
									
								
								.idea/runConfigurations/dev.xml
									
										
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								.idea/runConfigurations/dev.xml
									
										
									
										generated
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,12 @@
 | 
			
		|||
<component name="ProjectRunConfigurationManager">
 | 
			
		||||
  <configuration default="false" name="dev" type="js.build_tools.npm" nameIsGenerated="true">
 | 
			
		||||
    <package-json value="$PROJECT_DIR$/package.json" />
 | 
			
		||||
    <command value="run" />
 | 
			
		||||
    <scripts>
 | 
			
		||||
      <script value="dev" />
 | 
			
		||||
    </scripts>
 | 
			
		||||
    <node-interpreter value="project" />
 | 
			
		||||
    <envs />
 | 
			
		||||
    <method v="2" />
 | 
			
		||||
  </configuration>
 | 
			
		||||
</component>
 | 
			
		||||
| 
						 | 
				
			
			@ -1,28 +1,22 @@
 | 
			
		|||
when:
 | 
			
		||||
  - branch: main
 | 
			
		||||
    event: push
 | 
			
		||||
 | 
			
		||||
steps:
 | 
			
		||||
pipeline:
 | 
			
		||||
  update:
 | 
			
		||||
    image: earthly/earthly:v0.8.1
 | 
			
		||||
    volumes:
 | 
			
		||||
      - /var/run/docker.sock:/var/run/docker.sock
 | 
			
		||||
    environment:
 | 
			
		||||
      FORCE_COLOR: 1
 | 
			
		||||
      EARTHLY_EXEC_CMD: '/bin/sh'
 | 
			
		||||
      EARTHLY_CONFIGURATION:
 | 
			
		||||
        from_secret: EARTHLY_CONFIGURATION
 | 
			
		||||
      SSH_CONFIG:
 | 
			
		||||
        from_secret: THM_WEB_SSH_CONFIG
 | 
			
		||||
      SSH_UPLOAD_KEY:
 | 
			
		||||
        from_secret: THM_WEB_DEPLOY_KEY
 | 
			
		||||
      SSH_KNOWN_HOSTS:
 | 
			
		||||
        from_secret: THM_WEB_KNOWN_HOSTS
 | 
			
		||||
      SSH_TARGET:
 | 
			
		||||
        from_secret: SSH_TARGET
 | 
			
		||||
      - FORCE_COLOR=1
 | 
			
		||||
      - EARTHLY_EXEC_CMD="/bin/sh"
 | 
			
		||||
    secrets:
 | 
			
		||||
      [
 | 
			
		||||
        EARTHLY_CONFIGURATION,
 | 
			
		||||
        SSH_CONFIG,
 | 
			
		||||
        SSH_UPLOAD_KEY,
 | 
			
		||||
        SSH_KNOWN_HOSTS,
 | 
			
		||||
        SSH_TARGET,
 | 
			
		||||
      ]
 | 
			
		||||
    commands:
 | 
			
		||||
      - mkdir ~/.earthly && echo "$EARTHLY_CONFIGURATION" > ~/.earthly/config.yaml
 | 
			
		||||
      - earthly bootstrap
 | 
			
		||||
      - earthly --secret SSH_CONFIG --secret SSH_UPLOAD_KEY --secret SSH_KNOWN_HOSTS --secret SSH_TARGET --push +deploy
 | 
			
		||||
    when:
 | 
			
		||||
      branch: ['main']
 | 
			
		||||
      branch: ["main"]
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										23
									
								
								Earthfile
									
										
									
									
									
								
							
							
						
						
									
										23
									
								
								Earthfile
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -2,13 +2,14 @@ VERSION 0.7
 | 
			
		|||
FROM node:lts
 | 
			
		||||
 | 
			
		||||
site:
 | 
			
		||||
    RUN npm install -g bun
 | 
			
		||||
    COPY package.json bun.lock /site
 | 
			
		||||
    RUN npm install -g pnpm
 | 
			
		||||
    COPY package.json pnpm-lock.yaml /site
 | 
			
		||||
    WORKDIR /site
 | 
			
		||||
    RUN bun install --frozen-lockfile
 | 
			
		||||
    COPY --dir src static vite.config.ts tsconfig.json svelte.config.js /site
 | 
			
		||||
    CACHE --id=pnpm $HOME/.local/share/pnpm
 | 
			
		||||
    RUN pnpm install --frozen-lockfile --prod
 | 
			
		||||
    COPY . /site
 | 
			
		||||
    COPY +assets-generated/ /site/assets/generated
 | 
			
		||||
    RUN export VITE_BUILD_DATE=$(date -Iminutes -u | sed 's/+00:00//') && bun x svelte-kit sync && bun run build
 | 
			
		||||
    RUN pnpm build
 | 
			
		||||
    SAVE ARTIFACT build AS LOCAL build
 | 
			
		||||
 | 
			
		||||
deploy:
 | 
			
		||||
| 
						 | 
				
			
			@ -25,22 +26,22 @@ deploy:
 | 
			
		|||
 | 
			
		||||
 | 
			
		||||
avsync-video-components:
 | 
			
		||||
    FROM --platform=linux/amd64 node:lts
 | 
			
		||||
    # https://pptr.dev/troubleshooting
 | 
			
		||||
    RUN apt-get update && apt-get -y install libgtk-3-dev libnotify-dev libgconf-2-4 libnss3 libxss1 libasound2 && rm -rf /var/lib/apt/lists/*
 | 
			
		||||
    RUN npm install -g bun
 | 
			
		||||
    RUN npm install -g pnpm
 | 
			
		||||
    RUN groupadd -r pptruser && useradd -r -g pptruser -G audio,video pptruser && mkdir /home/pptruser && chown -R pptruser:pptruser /home/pptruser
 | 
			
		||||
    USER pptruser
 | 
			
		||||
    COPY package.json bun.lock /site
 | 
			
		||||
    COPY package.json pnpm-lock.yaml /site
 | 
			
		||||
    WORKDIR /site
 | 
			
		||||
    RUN bun install --frozen-lockfile
 | 
			
		||||
    CACHE --id=pnpm /home/pptruser/.local/share/pnpm
 | 
			
		||||
    RUN pnpm install --frozen-lockfile
 | 
			
		||||
    COPY av-sync av-sync
 | 
			
		||||
    ARG FPS=60
 | 
			
		||||
    ARG CYCLES=16
 | 
			
		||||
    ARG SIZE=1200
 | 
			
		||||
    RUN bun av:render:video --fps $FPS --cycles 1 --size $SIZE --output /var/tmp/frames
 | 
			
		||||
    RUN pnpm av:render:video --fps $FPS --cycles 1 --size $SIZE --output /var/tmp/frames
 | 
			
		||||
    SAVE ARTIFACT /var/tmp/frames
 | 
			
		||||
    RUN bun av:render:audio -i beep.wav -o /var/tmp/track.wav --repeats $CYCLES
 | 
			
		||||
    RUN pnpm av:render:audio -i beep.wav -o /var/tmp/track.wav --repeats $CYCLES
 | 
			
		||||
    SAVE ARTIFACT /var/tmp/track.wav
 | 
			
		||||
 | 
			
		||||
aux-media:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,11 +1,11 @@
 | 
			
		|||
<!doctype html>
 | 
			
		||||
<html lang="en">
 | 
			
		||||
	<head>
 | 
			
		||||
		<meta charset="UTF-8" />
 | 
			
		||||
		<title>AV SYNC</title>
 | 
			
		||||
	</head>
 | 
			
		||||
	<body>
 | 
			
		||||
		<div id="app"></div>
 | 
			
		||||
		<script type="module" src="/src/main.ts"></script>
 | 
			
		||||
	</body>
 | 
			
		||||
  <head>
 | 
			
		||||
    <meta charset="UTF-8" />
 | 
			
		||||
    <title>AV SYNC</title>
 | 
			
		||||
  </head>
 | 
			
		||||
  <body>
 | 
			
		||||
    <div id="app"></div>
 | 
			
		||||
    <script type="module" src="/src/main.ts"></script>
 | 
			
		||||
  </body>
 | 
			
		||||
</html>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,6 @@
 | 
			
		|||
<script lang="ts">
 | 
			
		||||
	import '@fontsource/atkinson-hyperlegible';
 | 
			
		||||
	import '@fontsource/atkinson-hyperlegible/700.css';
 | 
			
		||||
	import '@fontsource/b612';
 | 
			
		||||
	import '@fontsource/b612/700.css';
 | 
			
		||||
	import 'normalize.css/normalize.css';
 | 
			
		||||
 | 
			
		||||
	import { onMount, tick } from 'svelte';
 | 
			
		||||
| 
						 | 
				
			
			@ -80,8 +80,7 @@
 | 
			
		|||
		justify-content: space-evenly;
 | 
			
		||||
		align-items: center;
 | 
			
		||||
 | 
			
		||||
		font-family: 'Atkinson Hyperlegible', 'IBM Plex Sans', 'Helvetica Neue', Arial, sans-serif;
 | 
			
		||||
		font-variant-numeric: tabular-nums;
 | 
			
		||||
		font-family: 'B612', 'IBM Plex Sans', 'Helvetica Neue', Arial, sans-serif;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.circular {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,3 @@
 | 
			
		|||
html,
 | 
			
		||||
body {
 | 
			
		||||
	margin: 0;
 | 
			
		||||
}
 | 
			
		||||
html, body {
 | 
			
		||||
  margin: 0;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,7 +1,7 @@
 | 
			
		|||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
 | 
			
		||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
	// Consult https://svelte.dev/docs#compile-time-svelte-preprocess
 | 
			
		||||
	// for more information about preprocessors
 | 
			
		||||
	preprocess: vitePreprocess()
 | 
			
		||||
};
 | 
			
		||||
  // Consult https://svelte.dev/docs#compile-time-svelte-preprocess
 | 
			
		||||
  // for more information about preprocessors
 | 
			
		||||
  preprocess: vitePreprocess(),
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,20 +1,20 @@
 | 
			
		|||
{
 | 
			
		||||
	"extends": "@tsconfig/svelte/tsconfig.json",
 | 
			
		||||
	"compilerOptions": {
 | 
			
		||||
		"target": "ESNext",
 | 
			
		||||
		"useDefineForClassFields": true,
 | 
			
		||||
		"module": "ESNext",
 | 
			
		||||
		"resolveJsonModule": true,
 | 
			
		||||
		/**
 | 
			
		||||
		 * Typecheck JS in `.svelte` and `.js` files by default.
 | 
			
		||||
		 * Disable checkJs if you'd like to use dynamic types in JS.
 | 
			
		||||
		 * Note that setting allowJs false does not prevent the use
 | 
			
		||||
		 * of JS in `.svelte` files.
 | 
			
		||||
		 */
 | 
			
		||||
		"allowJs": true,
 | 
			
		||||
		"checkJs": true,
 | 
			
		||||
		"isolatedModules": true
 | 
			
		||||
	},
 | 
			
		||||
	"include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte"],
 | 
			
		||||
	"references": [{ "path": "./tsconfig.node.json" }]
 | 
			
		||||
  "extends": "@tsconfig/svelte/tsconfig.json",
 | 
			
		||||
  "compilerOptions": {
 | 
			
		||||
    "target": "ESNext",
 | 
			
		||||
    "useDefineForClassFields": true,
 | 
			
		||||
    "module": "ESNext",
 | 
			
		||||
    "resolveJsonModule": true,
 | 
			
		||||
    /**
 | 
			
		||||
     * Typecheck JS in `.svelte` and `.js` files by default.
 | 
			
		||||
     * Disable checkJs if you'd like to use dynamic types in JS.
 | 
			
		||||
     * Note that setting allowJs false does not prevent the use
 | 
			
		||||
     * of JS in `.svelte` files.
 | 
			
		||||
     */
 | 
			
		||||
    "allowJs": true,
 | 
			
		||||
    "checkJs": true,
 | 
			
		||||
    "isolatedModules": true
 | 
			
		||||
  },
 | 
			
		||||
  "include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte"],
 | 
			
		||||
  "references": [{ "path": "./tsconfig.node.json" }]
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,10 +1,10 @@
 | 
			
		|||
{
 | 
			
		||||
	"compilerOptions": {
 | 
			
		||||
		"composite": true,
 | 
			
		||||
		"skipLibCheck": true,
 | 
			
		||||
		"module": "ESNext",
 | 
			
		||||
		"moduleResolution": "bundler",
 | 
			
		||||
		"strict": true
 | 
			
		||||
	},
 | 
			
		||||
	"include": ["vite.config.ts"]
 | 
			
		||||
  "compilerOptions": {
 | 
			
		||||
    "composite": true,
 | 
			
		||||
    "skipLibCheck": true,
 | 
			
		||||
    "module": "ESNext",
 | 
			
		||||
    "moduleResolution": "bundler",
 | 
			
		||||
    "strict": true
 | 
			
		||||
  },
 | 
			
		||||
  "include": ["vite.config.ts"]
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,7 +1,7 @@
 | 
			
		|||
import { defineConfig } from 'vite';
 | 
			
		||||
import { svelte } from '@sveltejs/vite-plugin-svelte';
 | 
			
		||||
import { defineConfig } from 'vite'
 | 
			
		||||
import { svelte } from '@sveltejs/vite-plugin-svelte'
 | 
			
		||||
 | 
			
		||||
// https://vitejs.dev/config/
 | 
			
		||||
export default defineConfig({
 | 
			
		||||
	plugins: [svelte()]
 | 
			
		||||
});
 | 
			
		||||
  plugins: [svelte()],
 | 
			
		||||
})
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										936
									
								
								bun.lock
									
										
									
									
									
								
							
							
						
						
									
										936
									
								
								bun.lock
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -1,936 +0,0 @@
 | 
			
		|||
{
 | 
			
		||||
  "lockfileVersion": 1,
 | 
			
		||||
  "workspaces": {
 | 
			
		||||
    "": {
 | 
			
		||||
      "name": "testcard",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@fontsource/atkinson-hyperlegible": "^5.1.1",
 | 
			
		||||
        "@fontsource/b612": "^5.1.1",
 | 
			
		||||
        "@sveltejs/adapter-auto": "^3.3.1",
 | 
			
		||||
        "@sveltejs/adapter-static": "^3.0.8",
 | 
			
		||||
        "@sveltejs/kit": "^2.17.1",
 | 
			
		||||
        "@sveltejs/vite-plugin-svelte": "^4.0.0",
 | 
			
		||||
        "@tabler/icons-webfont": "^2.47.0",
 | 
			
		||||
        "debug": "^4.4.0",
 | 
			
		||||
        "lodash": "^4.17.21",
 | 
			
		||||
        "normalize.css": "^8.0.1",
 | 
			
		||||
        "svelte": "^5.0.0",
 | 
			
		||||
        "tslib": "^2.8.1",
 | 
			
		||||
        "typescript": "^5.7.3",
 | 
			
		||||
        "vite": "^5.4.14",
 | 
			
		||||
      },
 | 
			
		||||
      "devDependencies": {
 | 
			
		||||
        "@inlang/paraglide-js": "^2.0.0",
 | 
			
		||||
        "@inlang/plugin-m-function-matcher": "^2.1.0",
 | 
			
		||||
        "@inlang/plugin-message-format": "^4.0.0",
 | 
			
		||||
        "@tsconfig/svelte": "^5.0.4",
 | 
			
		||||
        "@types/debug": "^4.1.12",
 | 
			
		||||
        "@types/eslint": "8.56.0",
 | 
			
		||||
        "@types/lodash": "^4.17.15",
 | 
			
		||||
        "@types/pngjs": "^6.0.5",
 | 
			
		||||
        "@types/wait-on": "^5.3.4",
 | 
			
		||||
        "@typescript-eslint/eslint-plugin": "^6.21.0",
 | 
			
		||||
        "@typescript-eslint/parser": "^6.21.0",
 | 
			
		||||
        "commander": "^12.1.0",
 | 
			
		||||
        "concurrently": "^8.2.2",
 | 
			
		||||
        "eslint": "^8.57.1",
 | 
			
		||||
        "eslint-config-prettier": "^9.1.0",
 | 
			
		||||
        "eslint-plugin-svelte": "^2.46.1",
 | 
			
		||||
        "node-wav": "^0.0.2",
 | 
			
		||||
        "pixelmatch": "^7.1.0",
 | 
			
		||||
        "pngjs": "^7.0.0",
 | 
			
		||||
        "prettier": "^3.5.0",
 | 
			
		||||
        "prettier-plugin-svelte": "^3.3.3",
 | 
			
		||||
        "puppeteer": "^22.15.0",
 | 
			
		||||
        "svelte-check": "^4.0.0",
 | 
			
		||||
        "vitest": "^3.2.4",
 | 
			
		||||
        "wait-on": "^9.0.1",
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  "trustedDependencies": [
 | 
			
		||||
    "esbuild",
 | 
			
		||||
    "puppeteer",
 | 
			
		||||
  ],
 | 
			
		||||
  "packages": {
 | 
			
		||||
    "@babel/code-frame": ["@babel/code-frame@7.26.2", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.25.9", "js-tokens": "^4.0.0", "picocolors": "^1.0.0" } }, "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ=="],
 | 
			
		||||
 | 
			
		||||
    "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.25.9", "", {}, "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ=="],
 | 
			
		||||
 | 
			
		||||
    "@babel/runtime": ["@babel/runtime@7.26.7", "", { "dependencies": { "regenerator-runtime": "^0.14.0" } }, "sha512-AOPI3D+a8dXnja+iwsUqGRjr1BbZIe771sXdapOtYI531gSqpi92vXivKcq2asu/DFpdl1ceFAKZyRzK2PCVcQ=="],
 | 
			
		||||
 | 
			
		||||
    "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="],
 | 
			
		||||
 | 
			
		||||
    "@esbuild/android-arm": ["@esbuild/android-arm@0.21.5", "", { "os": "android", "cpu": "arm" }, "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="],
 | 
			
		||||
 | 
			
		||||
    "@esbuild/android-arm64": ["@esbuild/android-arm64@0.21.5", "", { "os": "android", "cpu": "arm64" }, "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A=="],
 | 
			
		||||
 | 
			
		||||
    "@esbuild/android-x64": ["@esbuild/android-x64@0.21.5", "", { "os": "android", "cpu": "x64" }, "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA=="],
 | 
			
		||||
 | 
			
		||||
    "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.21.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ=="],
 | 
			
		||||
 | 
			
		||||
    "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.21.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw=="],
 | 
			
		||||
 | 
			
		||||
    "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.21.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g=="],
 | 
			
		||||
 | 
			
		||||
    "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.21.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ=="],
 | 
			
		||||
 | 
			
		||||
    "@esbuild/linux-arm": ["@esbuild/linux-arm@0.21.5", "", { "os": "linux", "cpu": "arm" }, "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA=="],
 | 
			
		||||
 | 
			
		||||
    "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.21.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q=="],
 | 
			
		||||
 | 
			
		||||
    "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.21.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg=="],
 | 
			
		||||
 | 
			
		||||
    "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg=="],
 | 
			
		||||
 | 
			
		||||
    "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg=="],
 | 
			
		||||
 | 
			
		||||
    "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.21.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w=="],
 | 
			
		||||
 | 
			
		||||
    "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA=="],
 | 
			
		||||
 | 
			
		||||
    "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.21.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A=="],
 | 
			
		||||
 | 
			
		||||
    "@esbuild/linux-x64": ["@esbuild/linux-x64@0.21.5", "", { "os": "linux", "cpu": "x64" }, "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ=="],
 | 
			
		||||
 | 
			
		||||
    "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.21.5", "", { "os": "none", "cpu": "x64" }, "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg=="],
 | 
			
		||||
 | 
			
		||||
    "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.21.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow=="],
 | 
			
		||||
 | 
			
		||||
    "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.21.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg=="],
 | 
			
		||||
 | 
			
		||||
    "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.21.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A=="],
 | 
			
		||||
 | 
			
		||||
    "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.21.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA=="],
 | 
			
		||||
 | 
			
		||||
    "@esbuild/win32-x64": ["@esbuild/win32-x64@0.21.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw=="],
 | 
			
		||||
 | 
			
		||||
    "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.4.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA=="],
 | 
			
		||||
 | 
			
		||||
    "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.1", "", {}, "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ=="],
 | 
			
		||||
 | 
			
		||||
    "@eslint/eslintrc": ["@eslint/eslintrc@2.1.4", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^9.6.0", "globals": "^13.19.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ=="],
 | 
			
		||||
 | 
			
		||||
    "@eslint/js": ["@eslint/js@8.57.1", "", {}, "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q=="],
 | 
			
		||||
 | 
			
		||||
    "@fontsource/atkinson-hyperlegible": ["@fontsource/atkinson-hyperlegible@5.1.1", "", {}, "sha512-AxjSa0Ld2CrHqz3GV0gHVY7R5ySBw5c1B7FXLz59aUeFK9rJNm5ivc14qzYKaDi0lYCjFQtfpqb9pklie91PeA=="],
 | 
			
		||||
 | 
			
		||||
    "@fontsource/b612": ["@fontsource/b612@5.1.1", "", {}, "sha512-Ti0e8lF+dr9taC36uvgJqWSUUsPFrMQ5jDL/a6PoB/4fFIm9L2xfYYPQQC5zVoKiFVN9wVGR7wcZY9WIaufL8w=="],
 | 
			
		||||
 | 
			
		||||
    "@hapi/address": ["@hapi/address@5.1.1", "", { "dependencies": { "@hapi/hoek": "^11.0.2" } }, "sha512-A+po2d/dVoY7cYajycYI43ZbYMXukuopIsqCjh5QzsBCipDtdofHntljDlpccMjIfTy6UOkg+5KPriwYch2bXA=="],
 | 
			
		||||
 | 
			
		||||
    "@hapi/formula": ["@hapi/formula@3.0.2", "", {}, "sha512-hY5YPNXzw1He7s0iqkRQi+uMGh383CGdyyIGYtB+W5N3KHPXoqychklvHhKCC9M3Xtv0OCs/IHw+r4dcHtBYWw=="],
 | 
			
		||||
 | 
			
		||||
    "@hapi/hoek": ["@hapi/hoek@11.0.7", "", {}, "sha512-HV5undWkKzcB4RZUusqOpcgxOaq6VOAH7zhhIr2g3G8NF/MlFO75SjOr2NfuSx0Mh40+1FqCkagKLJRykUWoFQ=="],
 | 
			
		||||
 | 
			
		||||
    "@hapi/pinpoint": ["@hapi/pinpoint@2.0.1", "", {}, "sha512-EKQmr16tM8s16vTT3cA5L0kZZcTMU5DUOZTuvpnY738m+jyP3JIUj+Mm1xc1rsLkGBQ/gVnfKYPwOmPg1tUR4Q=="],
 | 
			
		||||
 | 
			
		||||
    "@hapi/tlds": ["@hapi/tlds@1.1.3", "", {}, "sha512-QIvUMB5VZ8HMLZF9A2oWr3AFM430QC8oGd0L35y2jHpuW6bIIca6x/xL7zUf4J7L9WJ3qjz+iJII8ncaeMbpSg=="],
 | 
			
		||||
 | 
			
		||||
    "@hapi/topo": ["@hapi/topo@6.0.2", "", { "dependencies": { "@hapi/hoek": "^11.0.2" } }, "sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg=="],
 | 
			
		||||
 | 
			
		||||
    "@humanwhocodes/config-array": ["@humanwhocodes/config-array@0.13.0", "", { "dependencies": { "@humanwhocodes/object-schema": "^2.0.3", "debug": "^4.3.1", "minimatch": "^3.0.5" } }, "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw=="],
 | 
			
		||||
 | 
			
		||||
    "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="],
 | 
			
		||||
 | 
			
		||||
    "@humanwhocodes/object-schema": ["@humanwhocodes/object-schema@2.0.3", "", {}, "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA=="],
 | 
			
		||||
 | 
			
		||||
    "@inlang/paraglide-js": ["@inlang/paraglide-js@2.4.0", "", { "dependencies": { "@inlang/recommend-sherlock": "0.2.1", "@inlang/sdk": "2.4.9", "commander": "11.1.0", "consola": "3.4.0", "json5": "2.2.3", "unplugin": "^2.1.2", "urlpattern-polyfill": "^10.0.0" }, "bin": { "paraglide-js": "bin/run.js" } }, "sha512-T/m9uoev574/1JrhCnPcgK1xnAwkVMgaDev4LFthnmID8ubX2xjboSGO3IztwXWwO0aJoT1UJr89JCwjbwgnJQ=="],
 | 
			
		||||
 | 
			
		||||
    "@inlang/plugin-m-function-matcher": ["@inlang/plugin-m-function-matcher@2.1.0", "", { "dependencies": { "@inlang/sdk": "2.4.9" } }, "sha512-IAbG7rOl+rlTiZY7qj92we6lmII693lVthPtY9bFDkZ/Ig7FPSpae/TfLzqjf2KcR1nDdx1zRpSo6roDPeM85g=="],
 | 
			
		||||
 | 
			
		||||
    "@inlang/plugin-message-format": ["@inlang/plugin-message-format@4.0.0", "", { "dependencies": { "flat": "^6.0.1" } }, "sha512-zNpLxLTt+bDd3JLXj1ONzo+Q6AOzz2MfcgGo8XB6/bweGhFIndK3GU/q0iU4o7VI4KS1+OHNLpKwFcrAifwERQ=="],
 | 
			
		||||
 | 
			
		||||
    "@inlang/recommend-sherlock": ["@inlang/recommend-sherlock@0.2.1", "", { "dependencies": { "comment-json": "^4.2.3" } }, "sha512-ckv8HvHy/iTqaVAEKrr+gnl+p3XFNwe5D2+6w6wJk2ORV2XkcRkKOJ/XsTUJbPSiyi4PI+p+T3bqbmNx/rDUlg=="],
 | 
			
		||||
 | 
			
		||||
    "@inlang/sdk": ["@inlang/sdk@2.4.9", "", { "dependencies": { "@lix-js/sdk": "0.4.7", "@sinclair/typebox": "^0.31.17", "kysely": "^0.27.4", "sqlite-wasm-kysely": "0.3.0", "uuid": "^10.0.0" } }, "sha512-cvz/C1rF5WBxzHbEoiBoI6Sz6q6M+TdxfWkEGBYTD77opY8i8WN01prUWXEM87GPF4SZcyIySez9U0Ccm12oFQ=="],
 | 
			
		||||
 | 
			
		||||
    "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.8", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA=="],
 | 
			
		||||
 | 
			
		||||
    "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
 | 
			
		||||
 | 
			
		||||
    "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
 | 
			
		||||
 | 
			
		||||
    "@jridgewell/set-array": ["@jridgewell/set-array@1.2.1", "", {}, "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A=="],
 | 
			
		||||
 | 
			
		||||
    "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="],
 | 
			
		||||
 | 
			
		||||
    "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
 | 
			
		||||
 | 
			
		||||
    "@lix-js/sdk": ["@lix-js/sdk@0.4.7", "", { "dependencies": { "@lix-js/server-protocol-schema": "0.1.1", "dedent": "1.5.1", "human-id": "^4.1.1", "js-sha256": "^0.11.0", "kysely": "^0.27.4", "sqlite-wasm-kysely": "0.3.0", "uuid": "^10.0.0" } }, "sha512-pRbW+joG12L0ULfMiWYosIW0plmW4AsUdiPCp+Z8rAsElJ+wJ6in58zhD3UwUcd4BNcpldEGjg6PdA7e0RgsDQ=="],
 | 
			
		||||
 | 
			
		||||
    "@lix-js/server-protocol-schema": ["@lix-js/server-protocol-schema@0.1.1", "", {}, "sha512-jBeALB6prAbtr5q4vTuxnRZZv1M2rKe8iNqRQhFJ4Tv7150unEa0vKyz0hs8Gl3fUGsWaNJBh3J8++fpbrpRBQ=="],
 | 
			
		||||
 | 
			
		||||
    "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
 | 
			
		||||
 | 
			
		||||
    "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="],
 | 
			
		||||
 | 
			
		||||
    "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
 | 
			
		||||
 | 
			
		||||
    "@polka/url": ["@polka/url@1.0.0-next.28", "", {}, "sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw=="],
 | 
			
		||||
 | 
			
		||||
    "@puppeteer/browsers": ["@puppeteer/browsers@2.3.0", "", { "dependencies": { "debug": "^4.3.5", "extract-zip": "^2.0.1", "progress": "^2.0.3", "proxy-agent": "^6.4.0", "semver": "^7.6.3", "tar-fs": "^3.0.6", "unbzip2-stream": "^1.4.3", "yargs": "^17.7.2" }, "bin": { "browsers": "lib/cjs/main-cli.js" } }, "sha512-ioXoq9gPxkss4MYhD+SFaU9p1IHFUX0ILAWFPyjGaBdjLsYAlZw6j1iLA0N/m12uVHLFDfSYNF7EQccjinIMDA=="],
 | 
			
		||||
 | 
			
		||||
    "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.34.0", "", { "os": "android", "cpu": "arm" }, "sha512-Eeao7ewDq79jVEsrtWIj5RNqB8p2knlm9fhR6uJ2gqP7UfbLrTrxevudVrEPDM7Wkpn/HpRC2QfazH7MXLz3vQ=="],
 | 
			
		||||
 | 
			
		||||
    "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.34.0", "", { "os": "android", "cpu": "arm64" }, "sha512-yVh0Kf1f0Fq4tWNf6mWcbQBCLDpDrDEl88lzPgKhrgTcDrTtlmun92ywEF9dCjmYO3EFiSuJeeo9cYRxl2FswA=="],
 | 
			
		||||
 | 
			
		||||
    "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.34.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gCs0ErAZ9s0Osejpc3qahTsqIPUDjSKIyxK/0BGKvL+Tn0n3Kwvj8BrCv7Y5sR1Ypz1K2qz9Ny0VvkVyoXBVUQ=="],
 | 
			
		||||
 | 
			
		||||
    "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.34.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-aIB5Anc8hngk15t3GUkiO4pv42ykXHfmpXGS+CzM9CTyiWyT8HIS5ygRAy7KcFb/wiw4Br+vh1byqcHRTfq2tQ=="],
 | 
			
		||||
 | 
			
		||||
    "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.34.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-kpdsUdMlVJMRMaOf/tIvxk8TQdzHhY47imwmASOuMajg/GXpw8GKNd8LNwIHE5Yd1onehNpcUB9jHY6wgw9nHQ=="],
 | 
			
		||||
 | 
			
		||||
    "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.34.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-D0RDyHygOBCQiqookcPevrvgEarN0CttBecG4chOeIYCNtlKHmf5oi5kAVpXV7qs0Xh/WO2RnxeicZPtT50V0g=="],
 | 
			
		||||
 | 
			
		||||
    "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.34.0", "", { "os": "linux", "cpu": "arm" }, "sha512-mCIw8j5LPDXmCOW8mfMZwT6F/Kza03EnSr4wGYEswrEfjTfVsFOxvgYfuRMxTuUF/XmRb9WSMD5GhCWDe2iNrg=="],
 | 
			
		||||
 | 
			
		||||
    "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.34.0", "", { "os": "linux", "cpu": "arm" }, "sha512-AwwldAu4aCJPob7zmjuDUMvvuatgs8B/QiVB0KwkUarAcPB3W+ToOT+18TQwY4z09Al7G0BvCcmLRop5zBLTag=="],
 | 
			
		||||
 | 
			
		||||
    "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.34.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-e7kDUGVP+xw05pV65ZKb0zulRploU3gTu6qH1qL58PrULDGxULIS0OSDQJLH7WiFnpd3ZKUU4VM3u/Z7Zw+e7Q=="],
 | 
			
		||||
 | 
			
		||||
    "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.34.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-SXYJw3zpwHgaBqTXeAZ31qfW/v50wq4HhNVvKFhRr5MnptRX2Af4KebLWR1wpxGJtLgfS2hEPuALRIY3LPAAcA=="],
 | 
			
		||||
 | 
			
		||||
    "@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.34.0", "", { "os": "linux", "cpu": "none" }, "sha512-e5XiCinINCI4RdyU3sFyBH4zzz7LiQRvHqDtRe9Dt8o/8hTBaYpdPimayF00eY2qy5j4PaaWK0azRgUench6WQ=="],
 | 
			
		||||
 | 
			
		||||
    "@rollup/rollup-linux-powerpc64le-gnu": ["@rollup/rollup-linux-powerpc64le-gnu@4.34.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-3SWN3e0bAsm9ToprLFBSro8nJe6YN+5xmB11N4FfNf92wvLye/+Rh5JGQtKOpwLKt6e61R1RBc9g+luLJsc23A=="],
 | 
			
		||||
 | 
			
		||||
    "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.34.0", "", { "os": "linux", "cpu": "none" }, "sha512-B1Oqt3GLh7qmhvfnc2WQla4NuHlcxAD5LyueUi5WtMc76ZWY+6qDtQYqnxARx9r+7mDGfamD+8kTJO0pKUJeJA=="],
 | 
			
		||||
 | 
			
		||||
    "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.34.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-UfUCo0h/uj48Jq2lnhX0AOhZPSTAq3Eostas+XZ+GGk22pI+Op1Y6cxQ1JkUuKYu2iU+mXj1QjPrZm9nNWV9rg=="],
 | 
			
		||||
 | 
			
		||||
    "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.34.0", "", { "os": "linux", "cpu": "x64" }, "sha512-chZLTUIPbgcpm+Z7ALmomXW8Zh+wE2icrG+K6nt/HenPLmtwCajhQC5flNSk1Xy5EDMt/QAOz2MhzfOfJOLSiA=="],
 | 
			
		||||
 | 
			
		||||
    "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.34.0", "", { "os": "linux", "cpu": "x64" }, "sha512-jo0UolK70O28BifvEsFD/8r25shFezl0aUk2t0VJzREWHkq19e+pcLu4kX5HiVXNz5qqkD+aAq04Ct8rkxgbyQ=="],
 | 
			
		||||
 | 
			
		||||
    "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.34.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-Vmg0NhAap2S54JojJchiu5An54qa6t/oKT7LmDaWggpIcaiL8WcWHEN6OQrfTdL6mQ2GFyH7j2T5/3YPEDOOGA=="],
 | 
			
		||||
 | 
			
		||||
    "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.34.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-CV2aqhDDOsABKHKhNcs1SZFryffQf8vK2XrxP6lxC99ELZAdvsDgPklIBfd65R8R+qvOm1SmLaZ/Fdq961+m7A=="],
 | 
			
		||||
 | 
			
		||||
    "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.34.0", "", { "os": "win32", "cpu": "x64" }, "sha512-g2ASy1QwHP88y5KWvblUolJz9rN+i4ZOsYzkEwcNfaNooxNUXG+ON6F5xFo0NIItpHqxcdAyls05VXpBnludGw=="],
 | 
			
		||||
 | 
			
		||||
    "@sinclair/typebox": ["@sinclair/typebox@0.31.28", "", {}, "sha512-/s55Jujywdw/Jpan+vsy6JZs1z2ZTGxTmbZTPiuSL2wz9mfzA2gN1zzaqmvfi4pq+uOt7Du85fkiwv5ymW84aQ=="],
 | 
			
		||||
 | 
			
		||||
    "@sqlite.org/sqlite-wasm": ["@sqlite.org/sqlite-wasm@3.48.0-build4", "", { "bin": { "sqlite-wasm": "bin/index.js" } }, "sha512-hI6twvUkzOmyGZhQMza1gpfqErZxXRw6JEsiVjUbo7tFanVD+8Oil0Ih3l2nGzHdxPI41zFmfUQG7GHqhciKZQ=="],
 | 
			
		||||
 | 
			
		||||
    "@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="],
 | 
			
		||||
 | 
			
		||||
    "@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.6", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-4awhxtMh4cx9blePWl10HRHj8Iivtqj+2QdDCSMDzxG+XKa9+VCNupQuCuvzEhYPzZSrX+0gC+0lHA/0fFKKQQ=="],
 | 
			
		||||
 | 
			
		||||
    "@sveltejs/adapter-auto": ["@sveltejs/adapter-auto@3.3.1", "", { "dependencies": { "import-meta-resolve": "^4.1.0" }, "peerDependencies": { "@sveltejs/kit": "^2.0.0" } }, "sha512-5Sc7WAxYdL6q9j/+D0jJKjGREGlfIevDyHSQ2eNETHcB1TKlQWHcAo8AS8H1QdjNvSXpvOwNjykDUHPEAyGgdQ=="],
 | 
			
		||||
 | 
			
		||||
    "@sveltejs/adapter-static": ["@sveltejs/adapter-static@3.0.8", "", { "peerDependencies": { "@sveltejs/kit": "^2.0.0" } }, "sha512-YaDrquRpZwfcXbnlDsSrBQNCChVOT9MGuSg+dMAyfsAa1SmiAhrA5jUYUiIMC59G92kIbY/AaQOWcBdq+lh+zg=="],
 | 
			
		||||
 | 
			
		||||
    "@sveltejs/kit": ["@sveltejs/kit@2.17.1", "", { "dependencies": { "@types/cookie": "^0.6.0", "cookie": "^0.6.0", "devalue": "^5.1.0", "esm-env": "^1.2.2", "import-meta-resolve": "^4.1.0", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", "sade": "^1.8.1", "set-cookie-parser": "^2.6.0", "sirv": "^3.0.0" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0", "svelte": "^4.0.0 || ^5.0.0-next.0", "vite": "^5.0.3 || ^6.0.0" }, "bin": { "svelte-kit": "svelte-kit.js" } }, "sha512-CpoGSLqE2MCmcQwA2CWJvOsZ9vW+p/1H3itrFykdgajUNAEyQPbsaSn7fZb6PLHQwe+07njxje9ss0fjZoCAyw=="],
 | 
			
		||||
 | 
			
		||||
    "@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@4.0.4", "", { "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^3.0.0-next.0||^3.0.0", "debug": "^4.3.7", "deepmerge": "^4.3.1", "kleur": "^4.1.5", "magic-string": "^0.30.12", "vitefu": "^1.0.3" }, "peerDependencies": { "svelte": "^5.0.0-next.96 || ^5.0.0", "vite": "^5.0.0" } }, "sha512-0ba1RQ/PHen5FGpdSrW7Y3fAMQjrXantECALeOiOdBdzR5+5vPP6HVZRLmZaQL+W8m++o+haIAKq5qT+MiZ7VA=="],
 | 
			
		||||
 | 
			
		||||
    "@sveltejs/vite-plugin-svelte-inspector": ["@sveltejs/vite-plugin-svelte-inspector@3.0.1", "", { "dependencies": { "debug": "^4.3.7" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^4.0.0-next.0||^4.0.0", "svelte": "^5.0.0-next.96 || ^5.0.0", "vite": "^5.0.0" } }, "sha512-2CKypmj1sM4GE7HjllT7UKmo4Q6L5xFRd7VMGEWhYnZ+wc6AUVU01IBd7yUi6WnFndEwWoMNOd6e8UjoN0nbvQ=="],
 | 
			
		||||
 | 
			
		||||
    "@tabler/icons": ["@tabler/icons@2.47.0", "", {}, "sha512-4w5evLh+7FUUiA1GucvGj2ReX2TvOjEr4ejXdwL/bsjoSkof6r1gQmzqI+VHrE2CpJpB3al7bCTulOkFa/RcyA=="],
 | 
			
		||||
 | 
			
		||||
    "@tabler/icons-webfont": ["@tabler/icons-webfont@2.47.0", "", { "dependencies": { "@tabler/icons": "2.47.0" } }, "sha512-yfV9zDal0iYDmyGz4BS9IlhaaMydtLdyOrY2UAZToP65sVWj7AFIi6symNzsoBaX867xAZWVHdKcocah0BfSog=="],
 | 
			
		||||
 | 
			
		||||
    "@tootallnate/quickjs-emscripten": ["@tootallnate/quickjs-emscripten@0.23.0", "", {}, "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA=="],
 | 
			
		||||
 | 
			
		||||
    "@tsconfig/svelte": ["@tsconfig/svelte@5.0.4", "", {}, "sha512-BV9NplVgLmSi4mwKzD8BD/NQ8erOY/nUE/GpgWe2ckx+wIQF5RyRirn/QsSSCPeulVpc3RA/iJt6DpfTIZps0Q=="],
 | 
			
		||||
 | 
			
		||||
    "@types/chai": ["@types/chai@5.2.2", "", { "dependencies": { "@types/deep-eql": "*" } }, "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg=="],
 | 
			
		||||
 | 
			
		||||
    "@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="],
 | 
			
		||||
 | 
			
		||||
    "@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="],
 | 
			
		||||
 | 
			
		||||
    "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="],
 | 
			
		||||
 | 
			
		||||
    "@types/eslint": ["@types/eslint@8.56.0", "", { "dependencies": { "@types/estree": "*", "@types/json-schema": "*" } }, "sha512-FlsN0p4FhuYRjIxpbdXovvHQhtlG05O1GG/RNWvdAxTboR438IOTwmrY/vLA+Xfgg06BTkP045M3vpFwTMv1dg=="],
 | 
			
		||||
 | 
			
		||||
    "@types/estree": ["@types/estree@1.0.6", "", {}, "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw=="],
 | 
			
		||||
 | 
			
		||||
    "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
 | 
			
		||||
 | 
			
		||||
    "@types/lodash": ["@types/lodash@4.17.15", "", {}, "sha512-w/P33JFeySuhN6JLkysYUK2gEmy9kHHFN7E8ro0tkfmlDOgxBDzWEZ/J8cWA+fHqFevpswDTFZnDx+R9lbL6xw=="],
 | 
			
		||||
 | 
			
		||||
    "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
 | 
			
		||||
 | 
			
		||||
    "@types/node": ["@types/node@22.13.0", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-ClIbNe36lawluuvq3+YYhnIN2CELi+6q8NpnM7PYp4hBn/TatfboPgVSm2rwKRfnV2M+Ty9GWDFI64KEe+kysA=="],
 | 
			
		||||
 | 
			
		||||
    "@types/pngjs": ["@types/pngjs@6.0.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-0k5eKfrA83JOZPppLtS2C7OUtyNAl2wKNxfyYl9Q5g9lPkgBl/9hNyAu6HuEH2J4XmIv2znEpkDd0SaZVxW6iQ=="],
 | 
			
		||||
 | 
			
		||||
    "@types/semver": ["@types/semver@7.5.8", "", {}, "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ=="],
 | 
			
		||||
 | 
			
		||||
    "@types/wait-on": ["@types/wait-on@5.3.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-EBsPjFMrFlMbbUFf9D1Fp+PAB2TwmUn7a3YtHyD9RLuTIk1jDd8SxXVAoez2Ciy+8Jsceo2MYEYZzJ/DvorOKw=="],
 | 
			
		||||
 | 
			
		||||
    "@types/yauzl": ["@types/yauzl@2.10.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q=="],
 | 
			
		||||
 | 
			
		||||
    "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@6.21.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.5.1", "@typescript-eslint/scope-manager": "6.21.0", "@typescript-eslint/type-utils": "6.21.0", "@typescript-eslint/utils": "6.21.0", "@typescript-eslint/visitor-keys": "6.21.0", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.2.4", "natural-compare": "^1.4.0", "semver": "^7.5.4", "ts-api-utils": "^1.0.1" }, "peerDependencies": { "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", "eslint": "^7.0.0 || ^8.0.0" } }, "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA=="],
 | 
			
		||||
 | 
			
		||||
    "@typescript-eslint/parser": ["@typescript-eslint/parser@6.21.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "6.21.0", "@typescript-eslint/types": "6.21.0", "@typescript-eslint/typescript-estree": "6.21.0", "@typescript-eslint/visitor-keys": "6.21.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^7.0.0 || ^8.0.0" } }, "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ=="],
 | 
			
		||||
 | 
			
		||||
    "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@6.21.0", "", { "dependencies": { "@typescript-eslint/types": "6.21.0", "@typescript-eslint/visitor-keys": "6.21.0" } }, "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg=="],
 | 
			
		||||
 | 
			
		||||
    "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@6.21.0", "", { "dependencies": { "@typescript-eslint/typescript-estree": "6.21.0", "@typescript-eslint/utils": "6.21.0", "debug": "^4.3.4", "ts-api-utils": "^1.0.1" }, "peerDependencies": { "eslint": "^7.0.0 || ^8.0.0" } }, "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag=="],
 | 
			
		||||
 | 
			
		||||
    "@typescript-eslint/types": ["@typescript-eslint/types@6.21.0", "", {}, "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg=="],
 | 
			
		||||
 | 
			
		||||
    "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@6.21.0", "", { "dependencies": { "@typescript-eslint/types": "6.21.0", "@typescript-eslint/visitor-keys": "6.21.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", "minimatch": "9.0.3", "semver": "^7.5.4", "ts-api-utils": "^1.0.1" } }, "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ=="],
 | 
			
		||||
 | 
			
		||||
    "@typescript-eslint/utils": ["@typescript-eslint/utils@6.21.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@types/json-schema": "^7.0.12", "@types/semver": "^7.5.0", "@typescript-eslint/scope-manager": "6.21.0", "@typescript-eslint/types": "6.21.0", "@typescript-eslint/typescript-estree": "6.21.0", "semver": "^7.5.4" }, "peerDependencies": { "eslint": "^7.0.0 || ^8.0.0" } }, "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ=="],
 | 
			
		||||
 | 
			
		||||
    "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@6.21.0", "", { "dependencies": { "@typescript-eslint/types": "6.21.0", "eslint-visitor-keys": "^3.4.1" } }, "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A=="],
 | 
			
		||||
 | 
			
		||||
    "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
 | 
			
		||||
 | 
			
		||||
    "@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="],
 | 
			
		||||
 | 
			
		||||
    "@vitest/mocker": ["@vitest/mocker@3.2.4", "", { "dependencies": { "@vitest/spy": "3.2.4", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ=="],
 | 
			
		||||
 | 
			
		||||
    "@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="],
 | 
			
		||||
 | 
			
		||||
    "@vitest/runner": ["@vitest/runner@3.2.4", "", { "dependencies": { "@vitest/utils": "3.2.4", "pathe": "^2.0.3", "strip-literal": "^3.0.0" } }, "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ=="],
 | 
			
		||||
 | 
			
		||||
    "@vitest/snapshot": ["@vitest/snapshot@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "magic-string": "^0.30.17", "pathe": "^2.0.3" } }, "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ=="],
 | 
			
		||||
 | 
			
		||||
    "@vitest/spy": ["@vitest/spy@3.2.4", "", { "dependencies": { "tinyspy": "^4.0.3" } }, "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw=="],
 | 
			
		||||
 | 
			
		||||
    "@vitest/utils": ["@vitest/utils@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="],
 | 
			
		||||
 | 
			
		||||
    "acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="],
 | 
			
		||||
 | 
			
		||||
    "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
 | 
			
		||||
 | 
			
		||||
    "agent-base": ["agent-base@7.1.3", "", {}, "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw=="],
 | 
			
		||||
 | 
			
		||||
    "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
 | 
			
		||||
 | 
			
		||||
    "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
 | 
			
		||||
 | 
			
		||||
    "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
 | 
			
		||||
 | 
			
		||||
    "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
 | 
			
		||||
 | 
			
		||||
    "aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="],
 | 
			
		||||
 | 
			
		||||
    "array-timsort": ["array-timsort@1.0.3", "", {}, "sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ=="],
 | 
			
		||||
 | 
			
		||||
    "array-union": ["array-union@2.1.0", "", {}, "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw=="],
 | 
			
		||||
 | 
			
		||||
    "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="],
 | 
			
		||||
 | 
			
		||||
    "ast-types": ["ast-types@0.13.4", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w=="],
 | 
			
		||||
 | 
			
		||||
    "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
 | 
			
		||||
 | 
			
		||||
    "axios": ["axios@1.12.2", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw=="],
 | 
			
		||||
 | 
			
		||||
    "axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
 | 
			
		||||
 | 
			
		||||
    "b4a": ["b4a@1.6.7", "", {}, "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg=="],
 | 
			
		||||
 | 
			
		||||
    "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
 | 
			
		||||
 | 
			
		||||
    "bare-events": ["bare-events@2.5.4", "", {}, "sha512-+gFfDkR8pj4/TrWCGUGWmJIkBwuxPS5F+a5yWjOHQt2hHvNZd5YLzadjmDUtFmMM4y429bnKLa8bYBMHcYdnQA=="],
 | 
			
		||||
 | 
			
		||||
    "bare-fs": ["bare-fs@4.0.1", "", { "dependencies": { "bare-events": "^2.0.0", "bare-path": "^3.0.0", "bare-stream": "^2.0.0" } }, "sha512-ilQs4fm/l9eMfWY2dY0WCIUplSUp7U0CT1vrqMg1MUdeZl4fypu5UP0XcDBK5WBQPJAKP1b7XEodISmekH/CEg=="],
 | 
			
		||||
 | 
			
		||||
    "bare-os": ["bare-os@3.4.0", "", {}, "sha512-9Ous7UlnKbe3fMi7Y+qh0DwAup6A1JkYgPnjvMDNOlmnxNRQvQ/7Nst+OnUQKzk0iAT0m9BisbDVp9gCv8+ETA=="],
 | 
			
		||||
 | 
			
		||||
    "bare-path": ["bare-path@3.0.0", "", { "dependencies": { "bare-os": "^3.0.1" } }, "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw=="],
 | 
			
		||||
 | 
			
		||||
    "bare-stream": ["bare-stream@2.6.4", "", { "dependencies": { "streamx": "^2.21.0" }, "peerDependencies": { "bare-buffer": "*", "bare-events": "*" }, "optionalPeers": ["bare-buffer", "bare-events"] }, "sha512-G6i3A74FjNq4nVrrSTUz5h3vgXzBJnjmWAVlBWaZETkgu+LgKd7AiyOml3EDJY1AHlIbBHKDXE+TUT53Ff8OaA=="],
 | 
			
		||||
 | 
			
		||||
    "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
 | 
			
		||||
 | 
			
		||||
    "basic-ftp": ["basic-ftp@5.0.5", "", {}, "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg=="],
 | 
			
		||||
 | 
			
		||||
    "brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="],
 | 
			
		||||
 | 
			
		||||
    "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
 | 
			
		||||
 | 
			
		||||
    "buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="],
 | 
			
		||||
 | 
			
		||||
    "buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="],
 | 
			
		||||
 | 
			
		||||
    "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="],
 | 
			
		||||
 | 
			
		||||
    "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
 | 
			
		||||
 | 
			
		||||
    "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
 | 
			
		||||
 | 
			
		||||
    "chai": ["chai@5.3.3", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="],
 | 
			
		||||
 | 
			
		||||
    "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
 | 
			
		||||
 | 
			
		||||
    "check-error": ["check-error@2.1.1", "", {}, "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw=="],
 | 
			
		||||
 | 
			
		||||
    "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
 | 
			
		||||
 | 
			
		||||
    "chromium-bidi": ["chromium-bidi@0.6.3", "", { "dependencies": { "mitt": "3.0.1", "urlpattern-polyfill": "10.0.0", "zod": "3.23.8" }, "peerDependencies": { "devtools-protocol": "*" } }, "sha512-qXlsCmpCZJAnoTYI83Iu6EdYQpMYdVkCfq08KDh2pmlVqK5t5IA9mGs4/LwCwp4fqisSOMXZxP3HIh8w8aRn0A=="],
 | 
			
		||||
 | 
			
		||||
    "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
 | 
			
		||||
 | 
			
		||||
    "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
 | 
			
		||||
 | 
			
		||||
    "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
 | 
			
		||||
 | 
			
		||||
    "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
 | 
			
		||||
 | 
			
		||||
    "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
 | 
			
		||||
 | 
			
		||||
    "commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="],
 | 
			
		||||
 | 
			
		||||
    "comment-json": ["comment-json@4.2.5", "", { "dependencies": { "array-timsort": "^1.0.3", "core-util-is": "^1.0.3", "esprima": "^4.0.1", "has-own-prop": "^2.0.0", "repeat-string": "^1.6.1" } }, "sha512-bKw/r35jR3HGt5PEPm1ljsQQGyCrR8sFGNiN5L+ykDHdpO8Smxkrkla9Yi6NkQyUrb8V54PGhfMs6NrIwtxtdw=="],
 | 
			
		||||
 | 
			
		||||
    "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
 | 
			
		||||
 | 
			
		||||
    "concurrently": ["concurrently@8.2.2", "", { "dependencies": { "chalk": "^4.1.2", "date-fns": "^2.30.0", "lodash": "^4.17.21", "rxjs": "^7.8.1", "shell-quote": "^1.8.1", "spawn-command": "0.0.2", "supports-color": "^8.1.1", "tree-kill": "^1.2.2", "yargs": "^17.7.2" }, "bin": { "conc": "dist/bin/concurrently.js", "concurrently": "dist/bin/concurrently.js" } }, "sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg=="],
 | 
			
		||||
 | 
			
		||||
    "consola": ["consola@3.4.0", "", {}, "sha512-EiPU8G6dQG0GFHNR8ljnZFki/8a+cQwEQ+7wpxdChl02Q8HXlwEZWD5lqAF8vC2sEC3Tehr8hy7vErz88LHyUA=="],
 | 
			
		||||
 | 
			
		||||
    "cookie": ["cookie@0.6.0", "", {}, "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="],
 | 
			
		||||
 | 
			
		||||
    "core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="],
 | 
			
		||||
 | 
			
		||||
    "cosmiconfig": ["cosmiconfig@9.0.0", "", { "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0" }, "peerDependencies": { "typescript": ">=4.9.5" }, "optionalPeers": ["typescript"] }, "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg=="],
 | 
			
		||||
 | 
			
		||||
    "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
 | 
			
		||||
 | 
			
		||||
    "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],
 | 
			
		||||
 | 
			
		||||
    "data-uri-to-buffer": ["data-uri-to-buffer@6.0.2", "", {}, "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw=="],
 | 
			
		||||
 | 
			
		||||
    "date-fns": ["date-fns@2.30.0", "", { "dependencies": { "@babel/runtime": "^7.21.0" } }, "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw=="],
 | 
			
		||||
 | 
			
		||||
    "debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="],
 | 
			
		||||
 | 
			
		||||
    "dedent": ["dedent@1.5.1", "", { "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg=="],
 | 
			
		||||
 | 
			
		||||
    "deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="],
 | 
			
		||||
 | 
			
		||||
    "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
 | 
			
		||||
 | 
			
		||||
    "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="],
 | 
			
		||||
 | 
			
		||||
    "degenerator": ["degenerator@5.0.1", "", { "dependencies": { "ast-types": "^0.13.4", "escodegen": "^2.1.0", "esprima": "^4.0.1" } }, "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ=="],
 | 
			
		||||
 | 
			
		||||
    "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
 | 
			
		||||
 | 
			
		||||
    "devalue": ["devalue@5.1.1", "", {}, "sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw=="],
 | 
			
		||||
 | 
			
		||||
    "devtools-protocol": ["devtools-protocol@0.0.1312386", "", {}, "sha512-DPnhUXvmvKT2dFA/j7B+riVLUt9Q6RKJlcppojL5CoRywJJKLDYnRlw0gTFKfgDPHP5E04UoB71SxoJlVZy8FA=="],
 | 
			
		||||
 | 
			
		||||
    "dir-glob": ["dir-glob@3.0.1", "", { "dependencies": { "path-type": "^4.0.0" } }, "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA=="],
 | 
			
		||||
 | 
			
		||||
    "doctrine": ["doctrine@3.0.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w=="],
 | 
			
		||||
 | 
			
		||||
    "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
 | 
			
		||||
 | 
			
		||||
    "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
 | 
			
		||||
 | 
			
		||||
    "end-of-stream": ["end-of-stream@1.4.4", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q=="],
 | 
			
		||||
 | 
			
		||||
    "env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="],
 | 
			
		||||
 | 
			
		||||
    "error-ex": ["error-ex@1.3.2", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g=="],
 | 
			
		||||
 | 
			
		||||
    "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
 | 
			
		||||
 | 
			
		||||
    "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
 | 
			
		||||
 | 
			
		||||
    "es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="],
 | 
			
		||||
 | 
			
		||||
    "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
 | 
			
		||||
 | 
			
		||||
    "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="],
 | 
			
		||||
 | 
			
		||||
    "esbuild": ["esbuild@0.21.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="],
 | 
			
		||||
 | 
			
		||||
    "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
 | 
			
		||||
 | 
			
		||||
    "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
 | 
			
		||||
 | 
			
		||||
    "escodegen": ["escodegen@2.1.0", "", { "dependencies": { "esprima": "^4.0.1", "estraverse": "^5.2.0", "esutils": "^2.0.2" }, "optionalDependencies": { "source-map": "~0.6.1" }, "bin": { "esgenerate": "bin/esgenerate.js", "escodegen": "bin/escodegen.js" } }, "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w=="],
 | 
			
		||||
 | 
			
		||||
    "eslint": ["eslint@8.57.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.4", "@eslint/js": "8.57.1", "@humanwhocodes/config-array": "^0.13.0", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", "eslint-scope": "^7.2.2", "eslint-visitor-keys": "^3.4.3", "espree": "^9.6.1", "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "globals": "^13.19.0", "graphemer": "^1.4.0", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3", "strip-ansi": "^6.0.1", "text-table": "^0.2.0" }, "bin": { "eslint": "bin/eslint.js" } }, "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA=="],
 | 
			
		||||
 | 
			
		||||
    "eslint-compat-utils": ["eslint-compat-utils@0.5.1", "", { "dependencies": { "semver": "^7.5.4" }, "peerDependencies": { "eslint": ">=6.0.0" } }, "sha512-3z3vFexKIEnjHE3zCMRo6fn/e44U7T1khUjg+Hp0ZQMCigh28rALD0nPFBcGZuiLC5rLZa2ubQHDRln09JfU2Q=="],
 | 
			
		||||
 | 
			
		||||
    "eslint-config-prettier": ["eslint-config-prettier@9.1.0", "", { "peerDependencies": { "eslint": ">=7.0.0" }, "bin": { "eslint-config-prettier": "bin/cli.js" } }, "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw=="],
 | 
			
		||||
 | 
			
		||||
    "eslint-plugin-svelte": ["eslint-plugin-svelte@2.46.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@jridgewell/sourcemap-codec": "^1.4.15", "eslint-compat-utils": "^0.5.1", "esutils": "^2.0.3", "known-css-properties": "^0.35.0", "postcss": "^8.4.38", "postcss-load-config": "^3.1.4", "postcss-safe-parser": "^6.0.0", "postcss-selector-parser": "^6.1.0", "semver": "^7.6.2", "svelte-eslint-parser": "^0.43.0" }, "peerDependencies": { "eslint": "^7.0.0 || ^8.0.0-0 || ^9.0.0-0", "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" }, "optionalPeers": ["svelte"] }, "sha512-7xYr2o4NID/f9OEYMqxsEQsCsj4KaMy4q5sANaKkAb6/QeCjYFxRmDm2S3YC3A3pl1kyPZ/syOx/i7LcWYSbIw=="],
 | 
			
		||||
 | 
			
		||||
    "eslint-scope": ["eslint-scope@7.2.2", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg=="],
 | 
			
		||||
 | 
			
		||||
    "eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
 | 
			
		||||
 | 
			
		||||
    "esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="],
 | 
			
		||||
 | 
			
		||||
    "espree": ["espree@9.6.1", "", { "dependencies": { "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^3.4.1" } }, "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ=="],
 | 
			
		||||
 | 
			
		||||
    "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="],
 | 
			
		||||
 | 
			
		||||
    "esquery": ["esquery@1.6.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg=="],
 | 
			
		||||
 | 
			
		||||
    "esrap": ["esrap@2.1.0", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" } }, "sha512-yzmPNpl7TBbMRC5Lj2JlJZNPml0tzqoqP5B1JXycNUwtqma9AKCO0M2wHrdgsHcy1WRW7S9rJknAMtByg3usgA=="],
 | 
			
		||||
 | 
			
		||||
    "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="],
 | 
			
		||||
 | 
			
		||||
    "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
 | 
			
		||||
 | 
			
		||||
    "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
 | 
			
		||||
 | 
			
		||||
    "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
 | 
			
		||||
 | 
			
		||||
    "expect-type": ["expect-type@1.2.2", "", {}, "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA=="],
 | 
			
		||||
 | 
			
		||||
    "extract-zip": ["extract-zip@2.0.1", "", { "dependencies": { "debug": "^4.1.1", "get-stream": "^5.1.0", "yauzl": "^2.10.0" }, "optionalDependencies": { "@types/yauzl": "^2.9.1" }, "bin": { "extract-zip": "cli.js" } }, "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg=="],
 | 
			
		||||
 | 
			
		||||
    "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
 | 
			
		||||
 | 
			
		||||
    "fast-fifo": ["fast-fifo@1.3.2", "", {}, "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ=="],
 | 
			
		||||
 | 
			
		||||
    "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
 | 
			
		||||
 | 
			
		||||
    "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
 | 
			
		||||
 | 
			
		||||
    "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
 | 
			
		||||
 | 
			
		||||
    "fastq": ["fastq@1.19.0", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-7SFSRCNjBQIZH/xZR3iy5iQYR8aGBE0h3VG6/cwlbrpdciNYBMotQav8c1XI3HjHH+NikUpP53nPdlZSdWmFzA=="],
 | 
			
		||||
 | 
			
		||||
    "fd-slicer": ["fd-slicer@1.1.0", "", { "dependencies": { "pend": "~1.2.0" } }, "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g=="],
 | 
			
		||||
 | 
			
		||||
    "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
 | 
			
		||||
 | 
			
		||||
    "file-entry-cache": ["file-entry-cache@6.0.1", "", { "dependencies": { "flat-cache": "^3.0.4" } }, "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg=="],
 | 
			
		||||
 | 
			
		||||
    "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
 | 
			
		||||
 | 
			
		||||
    "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
 | 
			
		||||
 | 
			
		||||
    "flat": ["flat@6.0.1", "", { "bin": { "flat": "cli.js" } }, "sha512-/3FfIa8mbrg3xE7+wAhWeV+bd7L2Mof+xtZb5dRDKZ+wDvYJK4WDYeIOuOhre5Yv5aQObZrlbRmk3RTSiuQBtw=="],
 | 
			
		||||
 | 
			
		||||
    "flat-cache": ["flat-cache@3.2.0", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.3", "rimraf": "^3.0.2" } }, "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw=="],
 | 
			
		||||
 | 
			
		||||
    "flatted": ["flatted@3.3.2", "", {}, "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA=="],
 | 
			
		||||
 | 
			
		||||
    "follow-redirects": ["follow-redirects@1.15.9", "", {}, "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ=="],
 | 
			
		||||
 | 
			
		||||
    "form-data": ["form-data@4.0.4", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow=="],
 | 
			
		||||
 | 
			
		||||
    "fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="],
 | 
			
		||||
 | 
			
		||||
    "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
 | 
			
		||||
 | 
			
		||||
    "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
 | 
			
		||||
 | 
			
		||||
    "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
 | 
			
		||||
 | 
			
		||||
    "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
 | 
			
		||||
 | 
			
		||||
    "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
 | 
			
		||||
 | 
			
		||||
    "get-stream": ["get-stream@5.2.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA=="],
 | 
			
		||||
 | 
			
		||||
    "get-uri": ["get-uri@6.0.4", "", { "dependencies": { "basic-ftp": "^5.0.2", "data-uri-to-buffer": "^6.0.2", "debug": "^4.3.4" } }, "sha512-E1b1lFFLvLgak2whF2xDBcOy6NLVGZBqqjJjsIhvopKfWWEi64pLVTWWehV8KlLerZkfNTA95sTe2OdJKm1OzQ=="],
 | 
			
		||||
 | 
			
		||||
    "glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
 | 
			
		||||
 | 
			
		||||
    "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
 | 
			
		||||
 | 
			
		||||
    "globals": ["globals@13.24.0", "", { "dependencies": { "type-fest": "^0.20.2" } }, "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ=="],
 | 
			
		||||
 | 
			
		||||
    "globby": ["globby@11.1.0", "", { "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", "fast-glob": "^3.2.9", "ignore": "^5.2.0", "merge2": "^1.4.1", "slash": "^3.0.0" } }, "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g=="],
 | 
			
		||||
 | 
			
		||||
    "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
 | 
			
		||||
 | 
			
		||||
    "graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="],
 | 
			
		||||
 | 
			
		||||
    "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
 | 
			
		||||
 | 
			
		||||
    "has-own-prop": ["has-own-prop@2.0.0", "", {}, "sha512-Pq0h+hvsVm6dDEa8x82GnLSYHOzNDt7f0ddFa3FqcQlgzEiptPqL+XrOJNavjOzSYiYWIrgeVYYgGlLmnxwilQ=="],
 | 
			
		||||
 | 
			
		||||
    "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
 | 
			
		||||
 | 
			
		||||
    "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
 | 
			
		||||
 | 
			
		||||
    "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
 | 
			
		||||
 | 
			
		||||
    "http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="],
 | 
			
		||||
 | 
			
		||||
    "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
 | 
			
		||||
 | 
			
		||||
    "human-id": ["human-id@4.1.1", "", { "bin": { "human-id": "dist/cli.js" } }, "sha512-3gKm/gCSUipeLsRYZbbdA1BD83lBoWUkZ7G9VFrhWPAU76KwYo5KR8V28bpoPm/ygy0x5/GCbpRQdY7VLYCoIg=="],
 | 
			
		||||
 | 
			
		||||
    "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
 | 
			
		||||
 | 
			
		||||
    "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
 | 
			
		||||
 | 
			
		||||
    "import-fresh": ["import-fresh@3.3.0", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw=="],
 | 
			
		||||
 | 
			
		||||
    "import-meta-resolve": ["import-meta-resolve@4.1.0", "", {}, "sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw=="],
 | 
			
		||||
 | 
			
		||||
    "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
 | 
			
		||||
 | 
			
		||||
    "inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="],
 | 
			
		||||
 | 
			
		||||
    "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
 | 
			
		||||
 | 
			
		||||
    "ip-address": ["ip-address@9.0.5", "", { "dependencies": { "jsbn": "1.1.0", "sprintf-js": "^1.1.3" } }, "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g=="],
 | 
			
		||||
 | 
			
		||||
    "is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="],
 | 
			
		||||
 | 
			
		||||
    "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
 | 
			
		||||
 | 
			
		||||
    "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
 | 
			
		||||
 | 
			
		||||
    "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
 | 
			
		||||
 | 
			
		||||
    "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
 | 
			
		||||
 | 
			
		||||
    "is-path-inside": ["is-path-inside@3.0.3", "", {}, "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ=="],
 | 
			
		||||
 | 
			
		||||
    "is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="],
 | 
			
		||||
 | 
			
		||||
    "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
 | 
			
		||||
 | 
			
		||||
    "joi": ["joi@18.0.1", "", { "dependencies": { "@hapi/address": "^5.1.1", "@hapi/formula": "^3.0.2", "@hapi/hoek": "^11.0.7", "@hapi/pinpoint": "^2.0.1", "@hapi/tlds": "^1.1.1", "@hapi/topo": "^6.0.2", "@standard-schema/spec": "^1.0.0" } }, "sha512-IiQpRyypSnLisQf3PwuN2eIHAsAIGZIrLZkd4zdvIar2bDyhM91ubRjy8a3eYablXsh9BeI/c7dmPYHca5qtoA=="],
 | 
			
		||||
 | 
			
		||||
    "js-sha256": ["js-sha256@0.11.1", "", {}, "sha512-o6WSo/LUvY2uC4j7mO50a2ms7E/EAdbP0swigLV+nzHKTTaYnaLIWJ02VdXrsJX0vGedDESQnLsOekr94ryfjg=="],
 | 
			
		||||
 | 
			
		||||
    "js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="],
 | 
			
		||||
 | 
			
		||||
    "js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="],
 | 
			
		||||
 | 
			
		||||
    "jsbn": ["jsbn@1.1.0", "", {}, "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A=="],
 | 
			
		||||
 | 
			
		||||
    "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
 | 
			
		||||
 | 
			
		||||
    "json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="],
 | 
			
		||||
 | 
			
		||||
    "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
 | 
			
		||||
 | 
			
		||||
    "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
 | 
			
		||||
 | 
			
		||||
    "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
 | 
			
		||||
 | 
			
		||||
    "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
 | 
			
		||||
 | 
			
		||||
    "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
 | 
			
		||||
 | 
			
		||||
    "known-css-properties": ["known-css-properties@0.35.0", "", {}, "sha512-a/RAk2BfKk+WFGhhOCAYqSiFLc34k8Mt/6NWRI4joER0EYUzXIcFivjjnoD3+XU1DggLn/tZc3DOAgke7l8a4A=="],
 | 
			
		||||
 | 
			
		||||
    "kysely": ["kysely@0.27.6", "", {}, "sha512-FIyV/64EkKhJmjgC0g2hygpBv5RNWVPyNCqSAD7eTCv6eFWNIi4PN1UvdSJGicN/o35bnevgis4Y0UDC0qi8jQ=="],
 | 
			
		||||
 | 
			
		||||
    "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
 | 
			
		||||
 | 
			
		||||
    "lilconfig": ["lilconfig@2.1.0", "", {}, "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ=="],
 | 
			
		||||
 | 
			
		||||
    "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="],
 | 
			
		||||
 | 
			
		||||
    "locate-character": ["locate-character@3.0.0", "", {}, "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="],
 | 
			
		||||
 | 
			
		||||
    "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
 | 
			
		||||
 | 
			
		||||
    "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
 | 
			
		||||
 | 
			
		||||
    "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
 | 
			
		||||
 | 
			
		||||
    "loupe": ["loupe@3.2.1", "", {}, "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ=="],
 | 
			
		||||
 | 
			
		||||
    "lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="],
 | 
			
		||||
 | 
			
		||||
    "magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="],
 | 
			
		||||
 | 
			
		||||
    "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
 | 
			
		||||
 | 
			
		||||
    "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
 | 
			
		||||
 | 
			
		||||
    "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
 | 
			
		||||
 | 
			
		||||
    "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
 | 
			
		||||
 | 
			
		||||
    "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
 | 
			
		||||
 | 
			
		||||
    "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
 | 
			
		||||
 | 
			
		||||
    "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
 | 
			
		||||
 | 
			
		||||
    "mitt": ["mitt@3.0.1", "", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="],
 | 
			
		||||
 | 
			
		||||
    "mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="],
 | 
			
		||||
 | 
			
		||||
    "mrmime": ["mrmime@2.0.0", "", {}, "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw=="],
 | 
			
		||||
 | 
			
		||||
    "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
 | 
			
		||||
 | 
			
		||||
    "nanoid": ["nanoid@3.3.8", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w=="],
 | 
			
		||||
 | 
			
		||||
    "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
 | 
			
		||||
 | 
			
		||||
    "netmask": ["netmask@2.0.2", "", {}, "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg=="],
 | 
			
		||||
 | 
			
		||||
    "node-wav": ["node-wav@0.0.2", "", {}, "sha512-M6Rm/bbG6De/gKGxOpeOobx/dnGuP0dz40adqx38boqHhlWssBJZgLCPBNtb9NkrmnKYiV04xELq+R6PFOnoLA=="],
 | 
			
		||||
 | 
			
		||||
    "normalize.css": ["normalize.css@8.0.1", "", {}, "sha512-qizSNPO93t1YUuUhP22btGOo3chcvDFqFaj2TRybP0DMxkHOCTYwp3n34fel4a31ORXy4m1Xq0Gyqpb5m33qIg=="],
 | 
			
		||||
 | 
			
		||||
    "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
 | 
			
		||||
 | 
			
		||||
    "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
 | 
			
		||||
 | 
			
		||||
    "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
 | 
			
		||||
 | 
			
		||||
    "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
 | 
			
		||||
 | 
			
		||||
    "pac-proxy-agent": ["pac-proxy-agent@7.1.0", "", { "dependencies": { "@tootallnate/quickjs-emscripten": "^0.23.0", "agent-base": "^7.1.2", "debug": "^4.3.4", "get-uri": "^6.0.1", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.6", "pac-resolver": "^7.0.1", "socks-proxy-agent": "^8.0.5" } }, "sha512-Z5FnLVVZSnX7WjBg0mhDtydeRZ1xMcATZThjySQUHqr+0ksP8kqaw23fNKkaaN/Z8gwLUs/W7xdl0I75eP2Xyw=="],
 | 
			
		||||
 | 
			
		||||
    "pac-resolver": ["pac-resolver@7.0.1", "", { "dependencies": { "degenerator": "^5.0.0", "netmask": "^2.0.2" } }, "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg=="],
 | 
			
		||||
 | 
			
		||||
    "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],
 | 
			
		||||
 | 
			
		||||
    "parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="],
 | 
			
		||||
 | 
			
		||||
    "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
 | 
			
		||||
 | 
			
		||||
    "path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="],
 | 
			
		||||
 | 
			
		||||
    "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
 | 
			
		||||
 | 
			
		||||
    "path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="],
 | 
			
		||||
 | 
			
		||||
    "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
 | 
			
		||||
 | 
			
		||||
    "pathval": ["pathval@2.0.1", "", {}, "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ=="],
 | 
			
		||||
 | 
			
		||||
    "pend": ["pend@1.2.0", "", {}, "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="],
 | 
			
		||||
 | 
			
		||||
    "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
 | 
			
		||||
 | 
			
		||||
    "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
 | 
			
		||||
 | 
			
		||||
    "pixelmatch": ["pixelmatch@7.1.0", "", { "dependencies": { "pngjs": "^7.0.0" }, "bin": { "pixelmatch": "bin/pixelmatch" } }, "sha512-1wrVzJ2STrpmONHKBy228LM1b84msXDUoAzVEl0R8Mz4Ce6EPr+IVtxm8+yvrqLYMHswREkjYFaMxnyGnaY3Ng=="],
 | 
			
		||||
 | 
			
		||||
    "pngjs": ["pngjs@7.0.0", "", {}, "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow=="],
 | 
			
		||||
 | 
			
		||||
    "postcss": ["postcss@8.5.1", "", { "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ=="],
 | 
			
		||||
 | 
			
		||||
    "postcss-load-config": ["postcss-load-config@3.1.4", "", { "dependencies": { "lilconfig": "^2.0.5", "yaml": "^1.10.2" }, "peerDependencies": { "postcss": ">=8.0.9", "ts-node": ">=9.0.0" }, "optionalPeers": ["postcss", "ts-node"] }, "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg=="],
 | 
			
		||||
 | 
			
		||||
    "postcss-safe-parser": ["postcss-safe-parser@6.0.0", "", { "peerDependencies": { "postcss": "^8.3.3" } }, "sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ=="],
 | 
			
		||||
 | 
			
		||||
    "postcss-scss": ["postcss-scss@4.0.9", "", { "peerDependencies": { "postcss": "^8.4.29" } }, "sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A=="],
 | 
			
		||||
 | 
			
		||||
    "postcss-selector-parser": ["postcss-selector-parser@6.1.2", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg=="],
 | 
			
		||||
 | 
			
		||||
    "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
 | 
			
		||||
 | 
			
		||||
    "prettier": ["prettier@3.5.0", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-quyMrVt6svPS7CjQ9gKb3GLEX/rl3BCL2oa/QkNcXv4YNVBC9olt3s+H7ukto06q7B1Qz46PbrKLO34PR6vXcA=="],
 | 
			
		||||
 | 
			
		||||
    "prettier-plugin-svelte": ["prettier-plugin-svelte@3.3.3", "", { "peerDependencies": { "prettier": "^3.0.0", "svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0" } }, "sha512-yViK9zqQ+H2qZD1w/bH7W8i+bVfKrD8GIFjkFe4Thl6kCT9SlAsXVNmt3jCvQOCsnOhcvYgsoVlRV/Eu6x5nNw=="],
 | 
			
		||||
 | 
			
		||||
    "progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="],
 | 
			
		||||
 | 
			
		||||
    "proxy-agent": ["proxy-agent@6.5.0", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "http-proxy-agent": "^7.0.1", "https-proxy-agent": "^7.0.6", "lru-cache": "^7.14.1", "pac-proxy-agent": "^7.1.0", "proxy-from-env": "^1.1.0", "socks-proxy-agent": "^8.0.5" } }, "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A=="],
 | 
			
		||||
 | 
			
		||||
    "proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="],
 | 
			
		||||
 | 
			
		||||
    "pump": ["pump@3.0.2", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw=="],
 | 
			
		||||
 | 
			
		||||
    "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
 | 
			
		||||
 | 
			
		||||
    "puppeteer": ["puppeteer@22.15.0", "", { "dependencies": { "@puppeteer/browsers": "2.3.0", "cosmiconfig": "^9.0.0", "devtools-protocol": "0.0.1312386", "puppeteer-core": "22.15.0" }, "bin": { "puppeteer": "lib/esm/puppeteer/node/cli.js" } }, "sha512-XjCY1SiSEi1T7iSYuxS82ft85kwDJUS7wj1Z0eGVXKdtr5g4xnVcbjwxhq5xBnpK/E7x1VZZoJDxpjAOasHT4Q=="],
 | 
			
		||||
 | 
			
		||||
    "puppeteer-core": ["puppeteer-core@22.15.0", "", { "dependencies": { "@puppeteer/browsers": "2.3.0", "chromium-bidi": "0.6.3", "debug": "^4.3.6", "devtools-protocol": "0.0.1312386", "ws": "^8.18.0" } }, "sha512-cHArnywCiAAVXa3t4GGL2vttNxh7GqXtIYGym99egkNJ3oG//wL9LkvO4WE8W1TJe95t1F1ocu9X4xWaGsOKOA=="],
 | 
			
		||||
 | 
			
		||||
    "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
 | 
			
		||||
 | 
			
		||||
    "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
 | 
			
		||||
 | 
			
		||||
    "regenerator-runtime": ["regenerator-runtime@0.14.1", "", {}, "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="],
 | 
			
		||||
 | 
			
		||||
    "repeat-string": ["repeat-string@1.6.1", "", {}, "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w=="],
 | 
			
		||||
 | 
			
		||||
    "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
 | 
			
		||||
 | 
			
		||||
    "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
 | 
			
		||||
 | 
			
		||||
    "reusify": ["reusify@1.0.4", "", {}, "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw=="],
 | 
			
		||||
 | 
			
		||||
    "rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="],
 | 
			
		||||
 | 
			
		||||
    "rollup": ["rollup@4.34.0", "", { "dependencies": { "@types/estree": "1.0.6" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.34.0", "@rollup/rollup-android-arm64": "4.34.0", "@rollup/rollup-darwin-arm64": "4.34.0", "@rollup/rollup-darwin-x64": "4.34.0", "@rollup/rollup-freebsd-arm64": "4.34.0", "@rollup/rollup-freebsd-x64": "4.34.0", "@rollup/rollup-linux-arm-gnueabihf": "4.34.0", "@rollup/rollup-linux-arm-musleabihf": "4.34.0", "@rollup/rollup-linux-arm64-gnu": "4.34.0", "@rollup/rollup-linux-arm64-musl": "4.34.0", "@rollup/rollup-linux-loongarch64-gnu": "4.34.0", "@rollup/rollup-linux-powerpc64le-gnu": "4.34.0", "@rollup/rollup-linux-riscv64-gnu": "4.34.0", "@rollup/rollup-linux-s390x-gnu": "4.34.0", "@rollup/rollup-linux-x64-gnu": "4.34.0", "@rollup/rollup-linux-x64-musl": "4.34.0", "@rollup/rollup-win32-arm64-msvc": "4.34.0", "@rollup/rollup-win32-ia32-msvc": "4.34.0", "@rollup/rollup-win32-x64-msvc": "4.34.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-+4C/cgJ9w6sudisA0nZz0+O7lTP9a3CzNLsoDwaRumM8QHwghUsu6tqHXiTmNUp/rqNiM14++7dkzHDyCRs0Jg=="],
 | 
			
		||||
 | 
			
		||||
    "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
 | 
			
		||||
 | 
			
		||||
    "rxjs": ["rxjs@7.8.1", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg=="],
 | 
			
		||||
 | 
			
		||||
    "sade": ["sade@1.8.1", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="],
 | 
			
		||||
 | 
			
		||||
    "semver": ["semver@7.7.0", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-DrfFnPzblFmNrIZzg5RzHegbiRWg7KMR7btwi2yjHwx06zsUbO5g613sVwEV7FTwmzJu+Io0lJe2GJ3LxqpvBQ=="],
 | 
			
		||||
 | 
			
		||||
    "set-cookie-parser": ["set-cookie-parser@2.7.1", "", {}, "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ=="],
 | 
			
		||||
 | 
			
		||||
    "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
 | 
			
		||||
 | 
			
		||||
    "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
 | 
			
		||||
 | 
			
		||||
    "shell-quote": ["shell-quote@1.8.2", "", {}, "sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA=="],
 | 
			
		||||
 | 
			
		||||
    "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="],
 | 
			
		||||
 | 
			
		||||
    "sirv": ["sirv@3.0.0", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-BPwJGUeDaDCHihkORDchNyyTvWFhcusy1XMmhEVTQTwGeybFbp8YEmB+njbPnth1FibULBSBVwCQni25XlCUDg=="],
 | 
			
		||||
 | 
			
		||||
    "slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="],
 | 
			
		||||
 | 
			
		||||
    "smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="],
 | 
			
		||||
 | 
			
		||||
    "socks": ["socks@2.8.3", "", { "dependencies": { "ip-address": "^9.0.5", "smart-buffer": "^4.2.0" } }, "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw=="],
 | 
			
		||||
 | 
			
		||||
    "socks-proxy-agent": ["socks-proxy-agent@8.0.5", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "socks": "^2.8.3" } }, "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw=="],
 | 
			
		||||
 | 
			
		||||
    "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
 | 
			
		||||
 | 
			
		||||
    "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
 | 
			
		||||
 | 
			
		||||
    "spawn-command": ["spawn-command@0.0.2", "", {}, "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ=="],
 | 
			
		||||
 | 
			
		||||
    "sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="],
 | 
			
		||||
 | 
			
		||||
    "sqlite-wasm-kysely": ["sqlite-wasm-kysely@0.3.0", "", { "dependencies": { "@sqlite.org/sqlite-wasm": "^3.48.0-build2" }, "peerDependencies": { "kysely": "*" } }, "sha512-TzjBNv7KwRw6E3pdKdlRyZiTmUIE0UttT/Sl56MVwVARl/u5gp978KepazCJZewFUnlWHz9i3NQd4kOtP/Afdg=="],
 | 
			
		||||
 | 
			
		||||
    "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="],
 | 
			
		||||
 | 
			
		||||
    "std-env": ["std-env@3.9.0", "", {}, "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw=="],
 | 
			
		||||
 | 
			
		||||
    "streamx": ["streamx@2.22.0", "", { "dependencies": { "fast-fifo": "^1.3.2", "text-decoder": "^1.1.0" }, "optionalDependencies": { "bare-events": "^2.2.0" } }, "sha512-sLh1evHOzBy/iWRiR6d1zRcLao4gGZr3C1kzNz4fopCOKJb6xD9ub8Mpi9Mr1R6id5o43S+d93fI48UC5uM9aw=="],
 | 
			
		||||
 | 
			
		||||
    "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
 | 
			
		||||
 | 
			
		||||
    "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
 | 
			
		||||
 | 
			
		||||
    "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
 | 
			
		||||
 | 
			
		||||
    "strip-literal": ["strip-literal@3.0.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA=="],
 | 
			
		||||
 | 
			
		||||
    "supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="],
 | 
			
		||||
 | 
			
		||||
    "svelte": ["svelte@5.39.6", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "acorn": "^8.12.1", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "esm-env": "^1.2.1", "esrap": "^2.1.0", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-bOJXmuwLNaoqPCTWO8mPu/fwxI5peGE5Efe7oo6Cakpz/G60vsnVF6mxbGODaxMUFUKEnjm6XOwHEqOht6cbvw=="],
 | 
			
		||||
 | 
			
		||||
    "svelte-check": ["svelte-check@4.3.2", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "chokidar": "^4.0.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", "sade": "^1.7.4" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": ">=5.0.0" }, "bin": { "svelte-check": "bin/svelte-check" } }, "sha512-71udP5w2kaSTcX8iV0hn3o2FWlabQHhJTJLIQrCqMsrcOeDUO2VhCQKKCA8AMVHSPwdxLEWkUWh9OKxns5PD9w=="],
 | 
			
		||||
 | 
			
		||||
    "svelte-eslint-parser": ["svelte-eslint-parser@0.43.0", "", { "dependencies": { "eslint-scope": "^7.2.2", "eslint-visitor-keys": "^3.4.3", "espree": "^9.6.1", "postcss": "^8.4.39", "postcss-scss": "^4.0.9" }, "peerDependencies": { "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" }, "optionalPeers": ["svelte"] }, "sha512-GpU52uPKKcVnh8tKN5P4UZpJ/fUDndmq7wfsvoVXsyP+aY0anol7Yqo01fyrlaWGMFfm4av5DyrjlaXdLRJvGA=="],
 | 
			
		||||
 | 
			
		||||
    "tar-fs": ["tar-fs@3.0.8", "", { "dependencies": { "pump": "^3.0.0", "tar-stream": "^3.1.5" }, "optionalDependencies": { "bare-fs": "^4.0.1", "bare-path": "^3.0.0" } }, "sha512-ZoROL70jptorGAlgAYiLoBLItEKw/fUxg9BSYK/dF/GAGYFJOJJJMvjPAKDJraCXFwadD456FCuvLWgfhMsPwg=="],
 | 
			
		||||
 | 
			
		||||
    "tar-stream": ["tar-stream@3.1.7", "", { "dependencies": { "b4a": "^1.6.4", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ=="],
 | 
			
		||||
 | 
			
		||||
    "text-decoder": ["text-decoder@1.2.3", "", { "dependencies": { "b4a": "^1.6.4" } }, "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA=="],
 | 
			
		||||
 | 
			
		||||
    "text-table": ["text-table@0.2.0", "", {}, "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw=="],
 | 
			
		||||
 | 
			
		||||
    "through": ["through@2.3.8", "", {}, "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg=="],
 | 
			
		||||
 | 
			
		||||
    "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="],
 | 
			
		||||
 | 
			
		||||
    "tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="],
 | 
			
		||||
 | 
			
		||||
    "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
 | 
			
		||||
 | 
			
		||||
    "tinypool": ["tinypool@1.1.1", "", {}, "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg=="],
 | 
			
		||||
 | 
			
		||||
    "tinyrainbow": ["tinyrainbow@2.0.0", "", {}, "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw=="],
 | 
			
		||||
 | 
			
		||||
    "tinyspy": ["tinyspy@4.0.4", "", {}, "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q=="],
 | 
			
		||||
 | 
			
		||||
    "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
 | 
			
		||||
 | 
			
		||||
    "totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="],
 | 
			
		||||
 | 
			
		||||
    "tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="],
 | 
			
		||||
 | 
			
		||||
    "ts-api-utils": ["ts-api-utils@1.4.3", "", { "peerDependencies": { "typescript": ">=4.2.0" } }, "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw=="],
 | 
			
		||||
 | 
			
		||||
    "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
 | 
			
		||||
 | 
			
		||||
    "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
 | 
			
		||||
 | 
			
		||||
    "type-fest": ["type-fest@0.20.2", "", {}, "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ=="],
 | 
			
		||||
 | 
			
		||||
    "typescript": ["typescript@5.7.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="],
 | 
			
		||||
 | 
			
		||||
    "unbzip2-stream": ["unbzip2-stream@1.4.3", "", { "dependencies": { "buffer": "^5.2.1", "through": "^2.3.8" } }, "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg=="],
 | 
			
		||||
 | 
			
		||||
    "undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
 | 
			
		||||
 | 
			
		||||
    "unplugin": ["unplugin@2.3.10", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "acorn": "^8.15.0", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-6NCPkv1ClwH+/BGE9QeoTIl09nuiAt0gS28nn1PvYXsGKRwM2TCbFA2QiilmehPDTXIe684k4rZI1yl3A1PCUw=="],
 | 
			
		||||
 | 
			
		||||
    "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
 | 
			
		||||
 | 
			
		||||
    "urlpattern-polyfill": ["urlpattern-polyfill@10.0.0", "", {}, "sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg=="],
 | 
			
		||||
 | 
			
		||||
    "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
 | 
			
		||||
 | 
			
		||||
    "uuid": ["uuid@10.0.0", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ=="],
 | 
			
		||||
 | 
			
		||||
    "vite": ["vite@5.4.14", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA=="],
 | 
			
		||||
 | 
			
		||||
    "vite-node": ["vite-node@3.2.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="],
 | 
			
		||||
 | 
			
		||||
    "vitefu": ["vitefu@1.1.1", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ=="],
 | 
			
		||||
 | 
			
		||||
    "vitest": ["vitest@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", "@vitest/mocker": "3.2.4", "@vitest/pretty-format": "^3.2.4", "@vitest/runner": "3.2.4", "@vitest/snapshot": "3.2.4", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "debug": "^4.4.1", "expect-type": "^1.2.1", "magic-string": "^0.30.17", "pathe": "^2.0.3", "picomatch": "^4.0.2", "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.14", "tinypool": "^1.1.1", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", "vite-node": "3.2.4", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "@vitest/browser": "3.2.4", "@vitest/ui": "3.2.4", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A=="],
 | 
			
		||||
 | 
			
		||||
    "wait-on": ["wait-on@9.0.1", "", { "dependencies": { "axios": "^1.12.2", "joi": "^18.0.1", "lodash": "^4.17.21", "minimist": "^1.2.8", "rxjs": "^7.8.2" }, "bin": { "wait-on": "bin/wait-on" } }, "sha512-noeCAI+XbqWMXY23sKril0BSURhuLYarkVXwJv1uUWwoojZJE7pmX3vJ7kh7SZaNgPGzfsCSQIZM/AGvu0Q9pA=="],
 | 
			
		||||
 | 
			
		||||
    "webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="],
 | 
			
		||||
 | 
			
		||||
    "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
 | 
			
		||||
 | 
			
		||||
    "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="],
 | 
			
		||||
 | 
			
		||||
    "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
 | 
			
		||||
 | 
			
		||||
    "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
 | 
			
		||||
 | 
			
		||||
    "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
 | 
			
		||||
 | 
			
		||||
    "ws": ["ws@8.18.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="],
 | 
			
		||||
 | 
			
		||||
    "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
 | 
			
		||||
 | 
			
		||||
    "yaml": ["yaml@1.10.2", "", {}, "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="],
 | 
			
		||||
 | 
			
		||||
    "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
 | 
			
		||||
 | 
			
		||||
    "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
 | 
			
		||||
 | 
			
		||||
    "yauzl": ["yauzl@2.10.0", "", { "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } }, "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g=="],
 | 
			
		||||
 | 
			
		||||
    "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
 | 
			
		||||
 | 
			
		||||
    "zimmerframe": ["zimmerframe@1.1.4", "", {}, "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ=="],
 | 
			
		||||
 | 
			
		||||
    "zod": ["zod@3.23.8", "", {}, "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g=="],
 | 
			
		||||
 | 
			
		||||
    "@babel/code-frame/js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
 | 
			
		||||
 | 
			
		||||
    "@inlang/paraglide-js/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="],
 | 
			
		||||
 | 
			
		||||
    "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.3", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg=="],
 | 
			
		||||
 | 
			
		||||
    "chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
 | 
			
		||||
 | 
			
		||||
    "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
 | 
			
		||||
 | 
			
		||||
    "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
 | 
			
		||||
 | 
			
		||||
    "unplugin/acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
 | 
			
		||||
 | 
			
		||||
    "vite-node/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
 | 
			
		||||
 | 
			
		||||
    "vitest/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
 | 
			
		||||
 | 
			
		||||
    "wait-on/rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="],
 | 
			
		||||
 | 
			
		||||
    "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										12
									
								
								index.html
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								index.html
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,12 @@
 | 
			
		|||
<!DOCTYPE html>
 | 
			
		||||
<html lang="en">
 | 
			
		||||
<head>
 | 
			
		||||
  <meta charset="UTF-8">
 | 
			
		||||
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
 | 
			
		||||
  <title>Expanded Modern Test Card</title>
 | 
			
		||||
  <link rel="stylesheet" href="style.css">
 | 
			
		||||
</head>
 | 
			
		||||
<body>
 | 
			
		||||
 | 
			
		||||
</body>
 | 
			
		||||
</html>
 | 
			
		||||
							
								
								
									
										170
									
								
								messages/cs.json
									
										
									
									
									
								
							
							
						
						
									
										170
									
								
								messages/cs.json
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -1,170 +0,0 @@
 | 
			
		|||
{
 | 
			
		||||
	"$schema": "https://inlang.com/schema/inlang-message-format",
 | 
			
		||||
	"search": "Hledat",
 | 
			
		||||
	"tests_audio_description": "Zkontrolujte své stereo kanály nebo prostorový zvukový výstup, ověřte, zda jsou reproduktory ve fázi.",
 | 
			
		||||
	"tests_audio_label": "Zvuk",
 | 
			
		||||
	"tests_av-sync_description": "Zkontrolujte, zda jsou zvuk a video synchronizovány, a změřte zpoždění.",
 | 
			
		||||
	"tests_av-sync_label": "Synchronizace zvuku a videa",
 | 
			
		||||
	"tests_card_description": "Testovací karta pro váš displej nebo projektor, zkontrolujte barvy, rozlišení a geometrii.",
 | 
			
		||||
	"tests_card_label": "Karta",
 | 
			
		||||
	"tests_camera_description": "Zkontrolujte, zda vaše webkamera nebo snímací zařízení funguje, jeho kvalitu obrazu, rozlišení a snímkovou frekvenci. Pořiďte snímek.",
 | 
			
		||||
	"tests_camera_label": "Kamera",
 | 
			
		||||
	"tests_gamepad_description": "Otestujte svůj gamepad, zkontrolujte, zda funguje, všechna tlačítka a joysticky, drift páček, mrtvé zóny a kalibraci.",
 | 
			
		||||
	"tests_gamepad_label": "Gamepad",
 | 
			
		||||
	"tests_keyboard_description": "Zkontrolujte, zda všechny klávesy fungují a jaké kódy kláves odesílají.",
 | 
			
		||||
	"tests_keyboard_label": "Klávesnice",
 | 
			
		||||
	"tests_microphone_description": "Zkontrolujte, zda váš mikrofon funguje, jeho kvalitu, hlasitost a šum.",
 | 
			
		||||
	"tests_microphone_label": "Mikrofon",
 | 
			
		||||
	"tests_mouse_description": "Zkontrolujte, zda vaše myš nebo dotykové zařízení funguje správně, zda existují mrtvé zóny nebo chvění.",
 | 
			
		||||
	"tests_mouse_label": "Myš",
 | 
			
		||||
	"tests_sensors_description": "Zobrazte výstup senzorů vašeho zařízení, např. GPS, akcelerometr, gyroskop, kompas atd.",
 | 
			
		||||
	"tests_sensors_label": "Senzory",
 | 
			
		||||
	"tests_internet_description": "Změřte rychlost a latenci internetu.",
 | 
			
		||||
	"tests_internet_label": "Rychlost internetu",
 | 
			
		||||
	"tests_timer_description": "Zkontrolujte, zda váš časovač s vysokým rozlišením funguje.",
 | 
			
		||||
	"tests_timer_label": "Časovač s vysokým rozlišením",
 | 
			
		||||
	"category_inputs": "Vstupy",
 | 
			
		||||
	"category_outputs": "Výstupy",
 | 
			
		||||
	"category_audio": "Zvuk",
 | 
			
		||||
	"category_video": "Video",
 | 
			
		||||
	"category_control": "Ovládání",
 | 
			
		||||
	"category_misc": "Různé",
 | 
			
		||||
	"noTestsFound": "Nebyly nalezeny žádné testy.",
 | 
			
		||||
	"camera_title": "Test kamery",
 | 
			
		||||
	"camera_device": "Zařízení",
 | 
			
		||||
	"camera_noCameraFound": "Nebyla nalezena žádná kamera",
 | 
			
		||||
	"camera_refresh": "Obnovit",
 | 
			
		||||
	"camera_resolution": "Rozlišení",
 | 
			
		||||
	"camera_frameRate": "Snímková frekvence",
 | 
			
		||||
	"camera_noCameraSelected": "Není vybrána žádná kamera",
 | 
			
		||||
	"camera_takePicture": "Vyfotit",
 | 
			
		||||
	"camera_unflipImage": "Převrátit obrázek zpět",
 | 
			
		||||
	"camera_flipImage": "Převrátit obrázek",
 | 
			
		||||
	"camera_closeSnapshot": "Zavřít snímek",
 | 
			
		||||
	"audio_channel_frontLeft": "Přední levý",
 | 
			
		||||
	"audio_channel_frontCenter": "Přední středový",
 | 
			
		||||
	"audio_channel_frontRight": "Přední pravý",
 | 
			
		||||
	"audio_channel_sideLeft": "Boční levý",
 | 
			
		||||
	"audio_channel_sideRight": "Boční pravý",
 | 
			
		||||
	"audio_channel_rearLeft": "Zadní levý",
 | 
			
		||||
	"audio_channel_rearRight": "Zadní pravý",
 | 
			
		||||
	"audio_channel_lfe": "LFE",
 | 
			
		||||
	"gamepad_title": "Testy gamepadu a joysticku",
 | 
			
		||||
	"gamepad_device": "Zařízení",
 | 
			
		||||
	"gamepad_noGamepadsDetected": "Nebyly detekovány žádné gamepady. (Zkuste stisknout tlačítko)",
 | 
			
		||||
	"gamepad_refresh": "Obnovit",
 | 
			
		||||
	"gamepad_buttons": "Tlačítka",
 | 
			
		||||
	"gamepad_axes": "Osy",
 | 
			
		||||
	"gamepad_history": "Historie",
 | 
			
		||||
	"audio_channelTests": "Testy kanálů",
 | 
			
		||||
	"audio_stereo": "Stereo",
 | 
			
		||||
	"audio_surroundAudio": "Prostorový zvuk",
 | 
			
		||||
	"audio_surround51": "5.1 Prostorový",
 | 
			
		||||
	"audio_surround71": "7.1 Prostorový",
 | 
			
		||||
	"audio_phaseTest": "Test fáze",
 | 
			
		||||
	"audio_frequency": "Frekvence",
 | 
			
		||||
	"audio_inPhase": "Ve fázi",
 | 
			
		||||
	"audio_outOfPhase": "Mimo fázi",
 | 
			
		||||
	"audio_stop": "Zastavit",
 | 
			
		||||
	"screenInfo_screenResolution": "Rozlišení obrazovky",
 | 
			
		||||
	"screenInfo_windowResolution": "Rozlišení okna",
 | 
			
		||||
	"screenInfo_devicePixelRatio": "Poměr pixelů zařízení",
 | 
			
		||||
	"audio_channel_left": "Levý",
 | 
			
		||||
	"audio_channel_center": "Střed",
 | 
			
		||||
	"audio_channel_right": "Pravý",
 | 
			
		||||
	"keyboard_title": "Test klávesnice",
 | 
			
		||||
	"keyboard_instruction": "Stiskněte klávesu na klávesnici pro zobrazení objektu události a kódu klávesy.",
 | 
			
		||||
	"keyboard_pressedKeys": "Stisknuté klávesy:",
 | 
			
		||||
	"timer_title": "Časovač s vysokým rozlišením",
 | 
			
		||||
	"timer_fps": "FPS",
 | 
			
		||||
	"timer_restart": "Restartovat",
 | 
			
		||||
	"audio_stopCycling": "Zastavit cyklování",
 | 
			
		||||
	"audio_cycleThrough": "Procházet",
 | 
			
		||||
	"common_back": "Zpět",
 | 
			
		||||
	"audio_title": "Test zvuku",
 | 
			
		||||
	"avSync_title": "Synchronizace zvuku a videa",
 | 
			
		||||
	"internet_title": "Rychlost internetu",
 | 
			
		||||
	"tests_signal-generator_description": "Generujte sinusové vlny, šum (bílý, růžový, hnědý) a frekvenční přechody. Zahrnuje osciloskop a spektrum.",
 | 
			
		||||
	"tests_signal-generator_label": "Generátor signálu",
 | 
			
		||||
	"signalGen_title": "Generátor signálu",
 | 
			
		||||
	"signalGen_type": "Typ",
 | 
			
		||||
	"signalGen_sine": "Sinus",
 | 
			
		||||
	"signalGen_sweep": "Přechod",
 | 
			
		||||
	"signalGen_noiseWhite": "Bílý šum",
 | 
			
		||||
	"signalGen_noisePink": "Růžový šum",
 | 
			
		||||
	"signalGen_noiseBrown": "Hnědý šum",
 | 
			
		||||
	"signalGen_frequency": "Frekvence",
 | 
			
		||||
	"signalGen_from": "Od",
 | 
			
		||||
	"signalGen_to": "Do",
 | 
			
		||||
	"signalGen_duration": "Doba trvání",
 | 
			
		||||
	"signalGen_gain": "Hlasitost",
 | 
			
		||||
	"signalGen_start": "Start",
 | 
			
		||||
	"signalGen_stop": "Stop",
 | 
			
		||||
	"signalGen_scope": "Osciloskop",
 | 
			
		||||
	"signalGen_spectrum": "Spektrum",
 | 
			
		||||
	"signalGen_loop": "Smyčka",
 | 
			
		||||
	"mic_title": "Test mikrofonu",
 | 
			
		||||
	"mic_startMicrophone": "Spustit mikrofon",
 | 
			
		||||
	"mic_stop": "Zastavit",
 | 
			
		||||
	"mic_monitoringOn": "Monitorování: ZAP",
 | 
			
		||||
	"mic_monitoringOff": "Monitorování: VYP",
 | 
			
		||||
	"mic_gain": "Zesílení",
 | 
			
		||||
	"mic_monitorDelay": "Zpoždění monitoru",
 | 
			
		||||
	"mic_sampleRate": "Vzorkovací frekvence",
 | 
			
		||||
	"mic_inputDevice": "Vstupní zařízení",
 | 
			
		||||
	"mic_volume": "Hlasitost",
 | 
			
		||||
	"mic_recording": "Nahrávání",
 | 
			
		||||
	"mic_startRecording": "Spustit nahrávání",
 | 
			
		||||
	"mic_stopRecording": "Zastavit nahrávání",
 | 
			
		||||
	"mic_downloadRecording": "Stáhnout nahrávku",
 | 
			
		||||
	"mic_device": "Zařízení",
 | 
			
		||||
	"mic_noMicFound": "Nenalezen žádný mikrofon",
 | 
			
		||||
	"mic_refresh": "Obnovit",
 | 
			
		||||
	"mic_clipping": "Ořezávání",
 | 
			
		||||
	"mic_constraints": "Omezení",
 | 
			
		||||
	"mic_echoCancellation": "Potlačení ozvěny",
 | 
			
		||||
	"mic_noiseSuppression": "Potlačení šumu",
 | 
			
		||||
	"mic_agc": "Automatické řízení zisku",
 | 
			
		||||
	"mic_applyConstraints": "Použít",
 | 
			
		||||
	"mic_channels": "Kanály",
 | 
			
		||||
	"mic_stereo": "Stereo",
 | 
			
		||||
	"mic_requested": "Požadováno",
 | 
			
		||||
	"mic_obtained": "Získáno",
 | 
			
		||||
	"mic_peakNow": "Špička",
 | 
			
		||||
	"mic_peakHold": "Držení špičky",
 | 
			
		||||
	"mic_resetPeaks": "Resetovat špičky",
 | 
			
		||||
	"mic_advanced": "Pokročilé",
 | 
			
		||||
	"mic_default": "Výchozí",
 | 
			
		||||
	"mic_on": "Zapnuto",
 | 
			
		||||
	"mic_off": "Vypnuto",
 | 
			
		||||
	"mic_mono": "Mono",
 | 
			
		||||
	"sensors_title": "Senzory",
 | 
			
		||||
	"sensors_geolocation": "Geolokace",
 | 
			
		||||
	"sensors_start": "Start",
 | 
			
		||||
	"sensors_stop": "Stop",
 | 
			
		||||
	"sensors_accuracy": "Přesnost (m)",
 | 
			
		||||
	"sensors_altitude": "Nadmořská výška (m)",
 | 
			
		||||
	"sensors_heading": "Směr (stupně)",
 | 
			
		||||
	"sensors_speed": "Rychlost (m/s)",
 | 
			
		||||
	"sensors_timestamp": "Časové razítko",
 | 
			
		||||
	"sensors_copy": "Kopírovat JSON",
 | 
			
		||||
	"sensors_copied": "Zkopírováno do schránky",
 | 
			
		||||
	"sensors_notSupported": "Není podporováno na tomto zařízení/prohlížeči",
 | 
			
		||||
	"sensors_deviceMotion": "Pohyb zařízení",
 | 
			
		||||
	"sensors_deviceOrientation": "Orientace zařízení",
 | 
			
		||||
	"sensors_accelerometer": "Akcelerometr",
 | 
			
		||||
	"sensors_gyroscope": "Gyroskop",
 | 
			
		||||
	"sensors_magnetometer": "Magnetometr",
 | 
			
		||||
	"sensors_ambientLight": "Okolní světlo",
 | 
			
		||||
	"sensors_illuminance": "Osvětlení (lux)",
 | 
			
		||||
	"sensors_barometer": "Barometr",
 | 
			
		||||
	"sensors_pressure": "Tlak (hPa)",
 | 
			
		||||
	"sensors_temperature": "Teplota (°C)",
 | 
			
		||||
	"sensors_permissions": "Oprávnění",
 | 
			
		||||
	"sensors_enableMotionOrientation": "Povolit pohyb/orientaci",
 | 
			
		||||
	"sensors_motion": "Pohyb",
 | 
			
		||||
	"sensors_orientation": "Orientace",
 | 
			
		||||
	"sensors_status_granted": "Uděleno",
 | 
			
		||||
	"sensors_status_denied": "Odepřeno",
 | 
			
		||||
	"sensors_status_unknown": "Neznámý"
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										170
									
								
								messages/de.json
									
										
									
									
									
								
							
							
						
						
									
										170
									
								
								messages/de.json
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -1,170 +0,0 @@
 | 
			
		|||
{
 | 
			
		||||
	"$schema": "https://inlang.com/schema/inlang-message-format",
 | 
			
		||||
	"search": "Suchen",
 | 
			
		||||
	"tests_audio_description": "Überprüfen Sie Ihre Stereokanäle oder den Surround-Audioausgang, überprüfen Sie, ob Ihre Lautsprecher in Phase sind.",
 | 
			
		||||
	"tests_audio_label": "Audio",
 | 
			
		||||
	"tests_av-sync_description": "Überprüfen Sie, ob Audio und Video synchron sind, und messen Sie die Verzögerung.",
 | 
			
		||||
	"tests_av-sync_label": "Audio/Video-Synchronisation",
 | 
			
		||||
	"tests_card_description": "Testkarte für Ihr Display oder Ihren Projektor, überprüfen Sie Farben, Auflösung und Geometrie.",
 | 
			
		||||
	"tests_card_label": "Karte",
 | 
			
		||||
	"tests_camera_description": "Überprüfen Sie, ob Ihre Webcam oder Ihr Aufnahmegerät funktioniert, die Bildqualität, Auflösung und Bildrate. Machen Sie einen Schnappschuss.",
 | 
			
		||||
	"tests_camera_label": "Kamera",
 | 
			
		||||
	"tests_gamepad_description": "Testen Sie Ihr Gamepad, überprüfen Sie, ob es funktioniert, alle Tasten und Joysticks, Stick-Drift, Totzonen und Kalibrierung.",
 | 
			
		||||
	"tests_gamepad_label": "Gamepad",
 | 
			
		||||
	"tests_keyboard_description": "Überprüfen Sie, ob alle Tasten funktionieren und welche Tastencodes sie senden.",
 | 
			
		||||
	"tests_keyboard_label": "Tastatur",
 | 
			
		||||
	"tests_microphone_description": "Überprüfen Sie, ob Ihr Mikrofon funktioniert, seine Qualität, Lautstärke und Rauschen.",
 | 
			
		||||
	"tests_microphone_label": "Mikrofon",
 | 
			
		||||
	"tests_mouse_description": "Überprüfen Sie, ob Ihre Maus oder Ihr Touch-Gerät ordnungsgemäß funktioniert, ob es tote Zonen oder Jitter gibt.",
 | 
			
		||||
	"tests_mouse_label": "Maus",
 | 
			
		||||
	"tests_sensors_description": "Sehen Sie sich die Ausgabe der Sensoren Ihres Geräts an, z. B. GPS, Beschleunigungsmesser, Gyroskop, Kompass usw.",
 | 
			
		||||
	"tests_sensors_label": "Sensoren",
 | 
			
		||||
	"tests_internet_description": "Messen Sie Ihre Internetgeschwindigkeit und Latenz.",
 | 
			
		||||
	"tests_internet_label": "Internetgeschwindigkeit",
 | 
			
		||||
	"tests_timer_description": "Überprüfen Sie, ob Ihr hochauflösender Timer funktioniert.",
 | 
			
		||||
	"tests_timer_label": "Hochauflösender Timer",
 | 
			
		||||
	"category_inputs": "Eingänge",
 | 
			
		||||
	"category_outputs": "Ausgänge",
 | 
			
		||||
	"category_audio": "Audio",
 | 
			
		||||
	"category_video": "Video",
 | 
			
		||||
	"category_control": "Steuerung",
 | 
			
		||||
	"category_misc": "Sonstiges",
 | 
			
		||||
	"noTestsFound": "Keine Tests gefunden.",
 | 
			
		||||
	"camera_title": "Kameratest",
 | 
			
		||||
	"camera_device": "Gerät",
 | 
			
		||||
	"camera_noCameraFound": "Keine Kamera gefunden",
 | 
			
		||||
	"camera_refresh": "Aktualisieren",
 | 
			
		||||
	"camera_resolution": "Auflösung",
 | 
			
		||||
	"camera_frameRate": "Bildrate",
 | 
			
		||||
	"camera_noCameraSelected": "Keine Kamera ausgewählt",
 | 
			
		||||
	"camera_takePicture": "Bild aufnehmen",
 | 
			
		||||
	"camera_unflipImage": "Bild zurückklappen",
 | 
			
		||||
	"camera_flipImage": "Bild spiegeln",
 | 
			
		||||
	"camera_closeSnapshot": "Schnappschuss schließen",
 | 
			
		||||
	"audio_channel_frontLeft": "Vorne links",
 | 
			
		||||
	"audio_channel_frontCenter": "Vorne Mitte",
 | 
			
		||||
	"audio_channel_frontRight": "Vorne rechts",
 | 
			
		||||
	"audio_channel_sideLeft": "Seite links",
 | 
			
		||||
	"audio_channel_sideRight": "Seite rechts",
 | 
			
		||||
	"audio_channel_rearLeft": "Hinten links",
 | 
			
		||||
	"audio_channel_rearRight": "Hinten rechts",
 | 
			
		||||
	"audio_channel_lfe": "LFE",
 | 
			
		||||
	"gamepad_title": "Gamepad- und Joystick-Tests",
 | 
			
		||||
	"gamepad_device": "Gerät",
 | 
			
		||||
	"gamepad_noGamepadsDetected": "Keine Gamepads erkannt. (Versuchen Sie, eine Taste zu drücken)",
 | 
			
		||||
	"gamepad_refresh": "Aktualisieren",
 | 
			
		||||
	"gamepad_buttons": "Tasten",
 | 
			
		||||
	"gamepad_axes": "Achsen",
 | 
			
		||||
	"gamepad_history": "Verlauf",
 | 
			
		||||
	"audio_channelTests": "Kanaltester",
 | 
			
		||||
	"audio_stereo": "Stereo",
 | 
			
		||||
	"audio_surroundAudio": "Surround-Audio",
 | 
			
		||||
	"audio_surround51": "5.1 Surround",
 | 
			
		||||
	"audio_surround71": "7.1 Surround",
 | 
			
		||||
	"audio_phaseTest": "Phasentest",
 | 
			
		||||
	"audio_frequency": "Frequenz",
 | 
			
		||||
	"audio_inPhase": "In Phase",
 | 
			
		||||
	"audio_outOfPhase": "Außer Phase",
 | 
			
		||||
	"audio_stop": "Stopp",
 | 
			
		||||
	"screenInfo_screenResolution": "Bildschirmauflösung",
 | 
			
		||||
	"screenInfo_windowResolution": "Fensterauflösung",
 | 
			
		||||
	"screenInfo_devicePixelRatio": "Gerätepixelverhältnis",
 | 
			
		||||
	"audio_channel_left": "Links",
 | 
			
		||||
	"audio_channel_center": "Mitte",
 | 
			
		||||
	"audio_channel_right": "Rechts",
 | 
			
		||||
	"keyboard_title": "Tastaturtest",
 | 
			
		||||
	"keyboard_instruction": "Drücken Sie eine Taste auf der Tastatur, um das Ereignisobjekt und den Tastencode anzuzeigen.",
 | 
			
		||||
	"keyboard_pressedKeys": "Gedrückte Tasten:",
 | 
			
		||||
	"timer_title": "Hochauflösender Timer",
 | 
			
		||||
	"timer_fps": "FPS",
 | 
			
		||||
	"timer_restart": "Neustart",
 | 
			
		||||
	"audio_stopCycling": "Zyklus stoppen",
 | 
			
		||||
	"audio_cycleThrough": "Durchlaufen",
 | 
			
		||||
	"common_back": "Zurück",
 | 
			
		||||
	"audio_title": "Audiotest",
 | 
			
		||||
	"avSync_title": "Audio/Video-Synchronisation",
 | 
			
		||||
	"internet_title": "Internetgeschwindigkeit",
 | 
			
		||||
	"tests_signal-generator_description": "Erzeugen Sie Sinuswellen, Rauschen (weiß, pink, braun) und Frequenz-Sweeps. Mit Oszilloskop und Spektrum.",
 | 
			
		||||
	"tests_signal-generator_label": "Signalgenerator",
 | 
			
		||||
	"signalGen_title": "Signalgenerator",
 | 
			
		||||
	"signalGen_type": "Typ",
 | 
			
		||||
	"signalGen_sine": "Sinus",
 | 
			
		||||
	"signalGen_sweep": "Sweep",
 | 
			
		||||
	"signalGen_noiseWhite": "Weißes Rauschen",
 | 
			
		||||
	"signalGen_noisePink": "Pinkes Rauschen",
 | 
			
		||||
	"signalGen_noiseBrown": "Braunes Rauschen",
 | 
			
		||||
	"signalGen_frequency": "Frequenz",
 | 
			
		||||
	"signalGen_from": "Von",
 | 
			
		||||
	"signalGen_to": "Bis",
 | 
			
		||||
	"signalGen_duration": "Dauer",
 | 
			
		||||
	"signalGen_gain": "Verstärkung",
 | 
			
		||||
	"signalGen_start": "Start",
 | 
			
		||||
	"signalGen_stop": "Stopp",
 | 
			
		||||
	"signalGen_scope": "Oszilloskop",
 | 
			
		||||
	"signalGen_spectrum": "Spektrum",
 | 
			
		||||
	"signalGen_loop": "Schleife",
 | 
			
		||||
	"mic_title": "Mikrofontest",
 | 
			
		||||
	"mic_startMicrophone": "Mikrofon starten",
 | 
			
		||||
	"mic_stop": "Stopp",
 | 
			
		||||
	"mic_monitoringOn": "Monitoring: EIN",
 | 
			
		||||
	"mic_monitoringOff": "Monitoring: AUS",
 | 
			
		||||
	"mic_gain": "Verstärkung",
 | 
			
		||||
	"mic_monitorDelay": "Monitor-Verzögerung",
 | 
			
		||||
	"mic_sampleRate": "Abtastrate",
 | 
			
		||||
	"mic_inputDevice": "Eingabegerät",
 | 
			
		||||
	"mic_volume": "Lautstärke",
 | 
			
		||||
	"mic_recording": "Aufnahme",
 | 
			
		||||
	"mic_startRecording": "Aufnahme starten",
 | 
			
		||||
	"mic_stopRecording": "Aufnahme stoppen",
 | 
			
		||||
	"mic_downloadRecording": "Aufnahme herunterladen",
 | 
			
		||||
	"mic_device": "Gerät",
 | 
			
		||||
	"mic_noMicFound": "Kein Mikrofon gefunden",
 | 
			
		||||
	"mic_refresh": "Aktualisieren",
 | 
			
		||||
	"mic_clipping": "Übersteuerung",
 | 
			
		||||
	"mic_constraints": "Einschränkungen",
 | 
			
		||||
	"mic_echoCancellation": "Echounterdrückung",
 | 
			
		||||
	"mic_noiseSuppression": "Rauschunterdrückung",
 | 
			
		||||
	"mic_agc": "Automatische Verstärkungsregelung",
 | 
			
		||||
	"mic_applyConstraints": "Anwenden",
 | 
			
		||||
	"mic_channels": "Kanäle",
 | 
			
		||||
	"mic_stereo": "Stereo",
 | 
			
		||||
	"mic_requested": "Angefordert",
 | 
			
		||||
	"mic_obtained": "Erhalten",
 | 
			
		||||
	"mic_peakNow": "Spitze",
 | 
			
		||||
	"mic_peakHold": "Spitze halten",
 | 
			
		||||
	"mic_resetPeaks": "Spitzen zurücksetzen",
 | 
			
		||||
	"mic_advanced": "Erweitert",
 | 
			
		||||
	"mic_default": "Standard",
 | 
			
		||||
	"mic_on": "Ein",
 | 
			
		||||
	"mic_off": "Aus",
 | 
			
		||||
	"mic_mono": "Mono",
 | 
			
		||||
	"sensors_title": "Sensoren",
 | 
			
		||||
	"sensors_geolocation": "Geolokalisierung",
 | 
			
		||||
	"sensors_start": "Start",
 | 
			
		||||
	"sensors_stop": "Stopp",
 | 
			
		||||
	"sensors_accuracy": "Genauigkeit (m)",
 | 
			
		||||
	"sensors_altitude": "Höhe (m)",
 | 
			
		||||
	"sensors_heading": "Richtung (Grad)",
 | 
			
		||||
	"sensors_speed": "Geschwindigkeit (m/s)",
 | 
			
		||||
	"sensors_timestamp": "Zeitstempel",
 | 
			
		||||
	"sensors_copy": "JSON kopieren",
 | 
			
		||||
	"sensors_copied": "In die Zwischenablage kopiert",
 | 
			
		||||
	"sensors_notSupported": "Auf diesem Gerät/Browser nicht unterstützt",
 | 
			
		||||
	"sensors_deviceMotion": "Gerätebewegung",
 | 
			
		||||
	"sensors_deviceOrientation": "Geräteausrichtung",
 | 
			
		||||
	"sensors_accelerometer": "Beschleunigungsmesser",
 | 
			
		||||
	"sensors_gyroscope": "Gyroskop",
 | 
			
		||||
	"sensors_magnetometer": "Magnetometer",
 | 
			
		||||
	"sensors_ambientLight": "Umgebungslicht",
 | 
			
		||||
	"sensors_illuminance": "Beleuchtungsstärke (lux)",
 | 
			
		||||
	"sensors_barometer": "Barometer",
 | 
			
		||||
	"sensors_pressure": "Druck (hPa)",
 | 
			
		||||
	"sensors_temperature": "Temperatur (°C)",
 | 
			
		||||
	"sensors_permissions": "Berechtigungen",
 | 
			
		||||
	"sensors_enableMotionOrientation": "Bewegung/Ausrichtung aktivieren",
 | 
			
		||||
	"sensors_motion": "Bewegung",
 | 
			
		||||
	"sensors_orientation": "Ausrichtung",
 | 
			
		||||
	"sensors_status_granted": "Gewährt",
 | 
			
		||||
	"sensors_status_denied": "Verweigert",
 | 
			
		||||
	"sensors_status_unknown": "Unbekannt"
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										170
									
								
								messages/en.json
									
										
									
									
									
								
							
							
						
						
									
										170
									
								
								messages/en.json
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -1,170 +0,0 @@
 | 
			
		|||
{
 | 
			
		||||
	"$schema": "https://inlang.com/schema/inlang-message-format",
 | 
			
		||||
	"search": "Search",
 | 
			
		||||
	"tests_audio_description": "Check your stereo channels or surround audio output, verify if your speakers are in phase.",
 | 
			
		||||
	"tests_audio_label": "Audio",
 | 
			
		||||
	"tests_av-sync_description": "Check if your audio and video are in sync, and measure the delay.",
 | 
			
		||||
	"tests_av-sync_label": "Audio/Video Sync",
 | 
			
		||||
	"tests_card_description": "Test card for your display or projector, check colors, resolution and geometry.",
 | 
			
		||||
	"tests_card_label": "Card",
 | 
			
		||||
	"tests_camera_description": "Check whether your webcam or capture device is working, its image quality, resolution and frame rate. Take a snapshot.",
 | 
			
		||||
	"tests_camera_label": "Camera",
 | 
			
		||||
	"tests_gamepad_description": "Test your gamepad, check if it's working, all the buttons and joysticks, stick drift, dead zones and calibration.",
 | 
			
		||||
	"tests_gamepad_label": "Gamepad",
 | 
			
		||||
	"tests_keyboard_description": "Check if all keys are working and what key codes they send.",
 | 
			
		||||
	"tests_keyboard_label": "Keyboard",
 | 
			
		||||
	"tests_microphone_description": "Check if your microphone is working, its quality, volume and noise.",
 | 
			
		||||
	"tests_microphone_label": "Microphone",
 | 
			
		||||
	"tests_mouse_description": "Check if your mouse or touch device works properly, if there are dead zones or jitter.",
 | 
			
		||||
	"tests_mouse_label": "Mouse",
 | 
			
		||||
	"tests_sensors_description": "See the output of your device's sensors, e.g. GPS, accelerometer, gyroscope, compass, etc.",
 | 
			
		||||
	"tests_sensors_label": "Sensors",
 | 
			
		||||
	"tests_internet_description": "Measure your internet speed and latency.",
 | 
			
		||||
	"tests_internet_label": "Internet speed",
 | 
			
		||||
	"tests_timer_description": "Check if your high resolution timer is working.",
 | 
			
		||||
	"tests_timer_label": "High resolution timer",
 | 
			
		||||
	"category_inputs": "Inputs",
 | 
			
		||||
	"category_outputs": "Outputs",
 | 
			
		||||
	"category_audio": "Audio",
 | 
			
		||||
	"category_video": "Video",
 | 
			
		||||
	"category_control": "Control",
 | 
			
		||||
	"category_misc": "Miscellaneous",
 | 
			
		||||
	"noTestsFound": "No tests found.",
 | 
			
		||||
	"camera_title": "Camera test",
 | 
			
		||||
	"camera_device": "Device",
 | 
			
		||||
	"camera_noCameraFound": "No camera found",
 | 
			
		||||
	"camera_refresh": "Refresh",
 | 
			
		||||
	"camera_resolution": "Resolution",
 | 
			
		||||
	"camera_frameRate": "Frame rate",
 | 
			
		||||
	"camera_noCameraSelected": "No camera selected",
 | 
			
		||||
	"camera_takePicture": "Take picture",
 | 
			
		||||
	"camera_unflipImage": "Unflip image",
 | 
			
		||||
	"camera_flipImage": "Flip image",
 | 
			
		||||
	"camera_closeSnapshot": "Close snapshot",
 | 
			
		||||
	"audio_channel_frontLeft": "Front Left",
 | 
			
		||||
	"audio_channel_frontCenter": "Front Center",
 | 
			
		||||
	"audio_channel_frontRight": "Front Right",
 | 
			
		||||
	"audio_channel_sideLeft": "Side Left",
 | 
			
		||||
	"audio_channel_sideRight": "Side Right",
 | 
			
		||||
	"audio_channel_rearLeft": "Rear Left",
 | 
			
		||||
	"audio_channel_rearRight": "Rear Right",
 | 
			
		||||
	"audio_channel_lfe": "LFE",
 | 
			
		||||
	"gamepad_title": "Gamepad & Joystick Tests",
 | 
			
		||||
	"gamepad_device": "Device",
 | 
			
		||||
	"gamepad_noGamepadsDetected": "No gamepads detected. (Try pressing a button)",
 | 
			
		||||
	"gamepad_refresh": "Refresh",
 | 
			
		||||
	"gamepad_buttons": "Buttons",
 | 
			
		||||
	"gamepad_axes": "Axes",
 | 
			
		||||
	"gamepad_history": "History",
 | 
			
		||||
	"audio_channelTests": "Channel tests",
 | 
			
		||||
	"audio_stereo": "Stereo",
 | 
			
		||||
	"audio_surroundAudio": "Surround audio",
 | 
			
		||||
	"audio_surround51": "5.1 Surround",
 | 
			
		||||
	"audio_surround71": "7.1 Surround",
 | 
			
		||||
	"audio_phaseTest": "Phase test",
 | 
			
		||||
	"audio_frequency": "Frequency",
 | 
			
		||||
	"audio_inPhase": "In Phase",
 | 
			
		||||
	"audio_outOfPhase": "Out of Phase",
 | 
			
		||||
	"audio_stop": "Stop",
 | 
			
		||||
	"screenInfo_screenResolution": "Screen Resolution",
 | 
			
		||||
	"screenInfo_windowResolution": "Window Resolution",
 | 
			
		||||
	"screenInfo_devicePixelRatio": "Device Pixel Ratio",
 | 
			
		||||
	"audio_channel_left": "Left",
 | 
			
		||||
	"audio_channel_center": "Center",
 | 
			
		||||
	"audio_channel_right": "Right",
 | 
			
		||||
	"keyboard_title": "Keyboard testing",
 | 
			
		||||
	"keyboard_instruction": "Press a key on the keyboard to see the event object and the key code.",
 | 
			
		||||
	"keyboard_pressedKeys": "Pressed keys:",
 | 
			
		||||
	"timer_title": "High resolution timer",
 | 
			
		||||
	"timer_fps": "FPS",
 | 
			
		||||
	"timer_restart": "Restart",
 | 
			
		||||
	"audio_stopCycling": "Stop Cycling",
 | 
			
		||||
	"audio_cycleThrough": "Cycle through",
 | 
			
		||||
	"common_back": "Back",
 | 
			
		||||
	"audio_title": "Audio test",
 | 
			
		||||
	"avSync_title": "Audio/Video Synchronization",
 | 
			
		||||
	"internet_title": "Internet speed",
 | 
			
		||||
	"tests_signal-generator_description": "Generate sine waves, noise (white, pink, brown) and frequency sweeps. Includes oscilloscope and spectrum.",
 | 
			
		||||
	"tests_signal-generator_label": "Signal Generator",
 | 
			
		||||
	"signalGen_title": "Signal Generator",
 | 
			
		||||
	"signalGen_type": "Type",
 | 
			
		||||
	"signalGen_sine": "Sine",
 | 
			
		||||
	"signalGen_sweep": "Sweep",
 | 
			
		||||
	"signalGen_noiseWhite": "White noise",
 | 
			
		||||
	"signalGen_noisePink": "Pink noise",
 | 
			
		||||
	"signalGen_noiseBrown": "Brown noise",
 | 
			
		||||
	"signalGen_frequency": "Frequency",
 | 
			
		||||
	"signalGen_from": "From",
 | 
			
		||||
	"signalGen_to": "To",
 | 
			
		||||
	"signalGen_duration": "Duration",
 | 
			
		||||
	"signalGen_gain": "Gain",
 | 
			
		||||
	"signalGen_start": "Start",
 | 
			
		||||
	"signalGen_stop": "Stop",
 | 
			
		||||
	"signalGen_scope": "Oscilloscope",
 | 
			
		||||
	"signalGen_spectrum": "Spectrum",
 | 
			
		||||
	"signalGen_loop": "Loop",
 | 
			
		||||
	"mic_title": "Microphone test",
 | 
			
		||||
	"mic_startMicrophone": "Start microphone",
 | 
			
		||||
	"mic_stop": "Stop",
 | 
			
		||||
	"mic_monitoringOn": "Monitoring: ON",
 | 
			
		||||
	"mic_monitoringOff": "Monitoring: OFF",
 | 
			
		||||
	"mic_gain": "Gain",
 | 
			
		||||
	"mic_monitorDelay": "Monitor delay",
 | 
			
		||||
	"mic_sampleRate": "Sample rate",
 | 
			
		||||
	"mic_inputDevice": "Input",
 | 
			
		||||
	"mic_volume": "Volume",
 | 
			
		||||
	"mic_recording": "Recording",
 | 
			
		||||
	"mic_startRecording": "Start recording",
 | 
			
		||||
	"mic_stopRecording": "Stop recording",
 | 
			
		||||
	"mic_downloadRecording": "Download",
 | 
			
		||||
	"mic_device": "Device",
 | 
			
		||||
	"mic_noMicFound": "No microphone found",
 | 
			
		||||
	"mic_refresh": "Refresh",
 | 
			
		||||
	"mic_clipping": "Clipping",
 | 
			
		||||
	"mic_constraints": "Constraints",
 | 
			
		||||
	"mic_echoCancellation": "Echo cancellation",
 | 
			
		||||
	"mic_noiseSuppression": "Noise suppression",
 | 
			
		||||
	"mic_agc": "Auto gain control",
 | 
			
		||||
	"mic_applyConstraints": "Apply",
 | 
			
		||||
	"mic_channels": "Channels",
 | 
			
		||||
	"mic_stereo": "Stereo",
 | 
			
		||||
	"mic_requested": "Requested",
 | 
			
		||||
	"mic_obtained": "Obtained",
 | 
			
		||||
	"mic_peakNow": "Peak",
 | 
			
		||||
	"mic_peakHold": "Peak hold",
 | 
			
		||||
	"mic_resetPeaks": "Reset peaks",
 | 
			
		||||
	"mic_advanced": "Advanced",
 | 
			
		||||
	"mic_default": "Default",
 | 
			
		||||
	"mic_on": "On",
 | 
			
		||||
	"mic_off": "Off",
 | 
			
		||||
	"mic_mono": "Mono",
 | 
			
		||||
	"sensors_title": "Sensors",
 | 
			
		||||
	"sensors_geolocation": "Geolocation",
 | 
			
		||||
	"sensors_start": "Start",
 | 
			
		||||
	"sensors_stop": "Stop",
 | 
			
		||||
	"sensors_accuracy": "Accuracy (m)",
 | 
			
		||||
	"sensors_altitude": "Altitude (m)",
 | 
			
		||||
	"sensors_heading": "Heading (deg)",
 | 
			
		||||
	"sensors_speed": "Speed (m/s)",
 | 
			
		||||
	"sensors_timestamp": "Timestamp",
 | 
			
		||||
	"sensors_copy": "Copy JSON",
 | 
			
		||||
	"sensors_copied": "Copied to clipboard",
 | 
			
		||||
	"sensors_notSupported": "Not supported on this device/browser",
 | 
			
		||||
	"sensors_deviceMotion": "Device Motion",
 | 
			
		||||
	"sensors_deviceOrientation": "Device Orientation",
 | 
			
		||||
	"sensors_accelerometer": "Accelerometer",
 | 
			
		||||
	"sensors_gyroscope": "Gyroscope",
 | 
			
		||||
	"sensors_magnetometer": "Magnetometer",
 | 
			
		||||
	"sensors_ambientLight": "Ambient Light",
 | 
			
		||||
	"sensors_illuminance": "Illuminance (lux)",
 | 
			
		||||
	"sensors_barometer": "Barometer",
 | 
			
		||||
	"sensors_pressure": "Pressure (hPa)",
 | 
			
		||||
	"sensors_temperature": "Temperature (°C)",
 | 
			
		||||
	"sensors_permissions": "Permissions",
 | 
			
		||||
	"sensors_enableMotionOrientation": "Enable Motion/Orientation",
 | 
			
		||||
	"sensors_motion": "Motion",
 | 
			
		||||
	"sensors_orientation": "Orientation",
 | 
			
		||||
	"sensors_status_granted": "Granted",
 | 
			
		||||
	"sensors_status_denied": "Denied",
 | 
			
		||||
	"sensors_status_unknown": "Unknown"
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										170
									
								
								messages/es.json
									
										
									
									
									
								
							
							
						
						
									
										170
									
								
								messages/es.json
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -1,170 +0,0 @@
 | 
			
		|||
{
 | 
			
		||||
	"$schema": "https://inlang.com/schema/inlang-message-format",
 | 
			
		||||
	"search": "Buscar",
 | 
			
		||||
	"tests_audio_description": "Comprueba tus canales estéreo o la salida de audio envolvente, verifica si tus altavoces están en fase.",
 | 
			
		||||
	"tests_audio_label": "Audio",
 | 
			
		||||
	"tests_av-sync_description": "Comprueba si el audio y el video están sincronizados y mide el retraso.",
 | 
			
		||||
	"tests_av-sync_label": "Sincronización de Audio/Video",
 | 
			
		||||
	"tests_card_description": "Tarjeta de prueba para tu pantalla o proyector, comprueba colores, resolución y geometría.",
 | 
			
		||||
	"tests_card_label": "Tarjeta",
 | 
			
		||||
	"tests_camera_description": "Comprueba si tu cámara web o dispositivo de captura funciona, su calidad de imagen, resolución y velocidad de fotogramas. Toma una instantánea.",
 | 
			
		||||
	"tests_camera_label": "Cámara",
 | 
			
		||||
	"tests_gamepad_description": "Prueba tu gamepad, comprueba si funciona, todos los botones y joysticks, deriva del stick, zonas muertas y calibración.",
 | 
			
		||||
	"tests_gamepad_label": "Gamepad",
 | 
			
		||||
	"tests_keyboard_description": "Comprueba si todas las teclas funcionan y qué códigos de tecla envían.",
 | 
			
		||||
	"tests_keyboard_label": "Teclado",
 | 
			
		||||
	"tests_microphone_description": "Comprueba si tu micrófono funciona, su calidad, volumen y ruido.",
 | 
			
		||||
	"tests_microphone_label": "Micrófono",
 | 
			
		||||
	"tests_mouse_description": "Comprueba si tu ratón o dispositivo táctil funciona correctamente, si hay zonas muertas o fluctuaciones.",
 | 
			
		||||
	"tests_mouse_label": "Ratón",
 | 
			
		||||
	"tests_sensors_description": "Consulta la salida de los sensores de tu dispositivo, p. ej., GPS, acelerómetro, giroscopio, brújula, etc.",
 | 
			
		||||
	"tests_sensors_label": "Sensores",
 | 
			
		||||
	"tests_internet_description": "Mide tu velocidad de internet y latencia.",
 | 
			
		||||
	"tests_internet_label": "Velocidad de internet",
 | 
			
		||||
	"tests_timer_description": "Comprueba si tu temporizador de alta resolución funciona.",
 | 
			
		||||
	"tests_timer_label": "Temporizador de alta resolución",
 | 
			
		||||
	"category_inputs": "Entradas",
 | 
			
		||||
	"category_outputs": "Salidas",
 | 
			
		||||
	"category_audio": "Audio",
 | 
			
		||||
	"category_video": "Video",
 | 
			
		||||
	"category_control": "Control",
 | 
			
		||||
	"category_misc": "Misceláneo",
 | 
			
		||||
	"noTestsFound": "No se encontraron pruebas.",
 | 
			
		||||
	"camera_title": "Prueba de cámara",
 | 
			
		||||
	"camera_device": "Dispositivo",
 | 
			
		||||
	"camera_noCameraFound": "No se encontró ninguna cámara",
 | 
			
		||||
	"camera_refresh": "Actualizar",
 | 
			
		||||
	"camera_resolution": "Resolución",
 | 
			
		||||
	"camera_frameRate": "Velocidad de fotogramas",
 | 
			
		||||
	"camera_noCameraSelected": "No se ha seleccionado ninguna cámara",
 | 
			
		||||
	"camera_takePicture": "Tomar foto",
 | 
			
		||||
	"camera_unflipImage": "Desvoltear imagen",
 | 
			
		||||
	"camera_flipImage": "Voltear imagen",
 | 
			
		||||
	"camera_closeSnapshot": "Cerrar instantánea",
 | 
			
		||||
	"audio_channel_frontLeft": "Frontal izquierdo",
 | 
			
		||||
	"audio_channel_frontCenter": "Frontal central",
 | 
			
		||||
	"audio_channel_frontRight": "Frontal derecho",
 | 
			
		||||
	"audio_channel_sideLeft": "Lateral izquierdo",
 | 
			
		||||
	"audio_channel_sideRight": "Lateral derecho",
 | 
			
		||||
	"audio_channel_rearLeft": "Trasero izquierdo",
 | 
			
		||||
	"audio_channel_rearRight": "Trasero derecho",
 | 
			
		||||
	"audio_channel_lfe": "LFE",
 | 
			
		||||
	"gamepad_title": "Pruebas de Gamepad y Joystick",
 | 
			
		||||
	"gamepad_device": "Dispositivo",
 | 
			
		||||
	"gamepad_noGamepadsDetected": "No se detectaron gamepads. (Intenta presionar un botón)",
 | 
			
		||||
	"gamepad_refresh": "Actualizar",
 | 
			
		||||
	"gamepad_buttons": "Botones",
 | 
			
		||||
	"gamepad_axes": "Ejes",
 | 
			
		||||
	"gamepad_history": "Historial",
 | 
			
		||||
	"audio_channelTests": "Pruebas de canal",
 | 
			
		||||
	"audio_stereo": "Estéreo",
 | 
			
		||||
	"audio_surroundAudio": "Audio envolvente",
 | 
			
		||||
	"audio_surround51": "Envolvente 5.1",
 | 
			
		||||
	"audio_surround71": "Envolvente 7.1",
 | 
			
		||||
	"audio_phaseTest": "Prueba de fase",
 | 
			
		||||
	"audio_frequency": "Frecuencia",
 | 
			
		||||
	"audio_inPhase": "En fase",
 | 
			
		||||
	"audio_outOfPhase": "Fuera de fase",
 | 
			
		||||
	"audio_stop": "Detener",
 | 
			
		||||
	"screenInfo_screenResolution": "Resolución de pantalla",
 | 
			
		||||
	"screenInfo_windowResolution": "Resolución de ventana",
 | 
			
		||||
	"screenInfo_devicePixelRatio": "Relación de píxeles del dispositivo",
 | 
			
		||||
	"audio_channel_left": "Izquierda",
 | 
			
		||||
	"audio_channel_center": "Centro",
 | 
			
		||||
	"audio_channel_right": "Derecha",
 | 
			
		||||
	"keyboard_title": "Prueba de teclado",
 | 
			
		||||
	"keyboard_instruction": "Presiona una tecla en el teclado para ver el objeto de evento y el código de tecla.",
 | 
			
		||||
	"keyboard_pressedKeys": "Teclas presionadas:",
 | 
			
		||||
	"timer_title": "Temporizador de alta resolución",
 | 
			
		||||
	"timer_fps": "FPS",
 | 
			
		||||
	"timer_restart": "Reiniciar",
 | 
			
		||||
	"audio_stopCycling": "Detener ciclo",
 | 
			
		||||
	"audio_cycleThrough": "Recorrer",
 | 
			
		||||
	"common_back": "Atrás",
 | 
			
		||||
	"audio_title": "Prueba de audio",
 | 
			
		||||
	"avSync_title": "Sincronización de Audio/Video",
 | 
			
		||||
	"internet_title": "Velocidad de internet",
 | 
			
		||||
	"tests_signal-generator_description": "Genera ondas senoidales, ruido (blanco, rosa, marrón) y barridos de frecuencia. Incluye osciloscopio y espectro.",
 | 
			
		||||
	"tests_signal-generator_label": "Generador de señal",
 | 
			
		||||
	"signalGen_title": "Generador de señal",
 | 
			
		||||
	"signalGen_type": "Tipo",
 | 
			
		||||
	"signalGen_sine": "Senoidal",
 | 
			
		||||
	"signalGen_sweep": "Barrido",
 | 
			
		||||
	"signalGen_noiseWhite": "Ruido blanco",
 | 
			
		||||
	"signalGen_noisePink": "Ruido rosa",
 | 
			
		||||
	"signalGen_noiseBrown": "Ruido marrón",
 | 
			
		||||
	"signalGen_frequency": "Frecuencia",
 | 
			
		||||
	"signalGen_from": "Desde",
 | 
			
		||||
	"signalGen_to": "Hasta",
 | 
			
		||||
	"signalGen_duration": "Duración",
 | 
			
		||||
	"signalGen_gain": "Ganancia",
 | 
			
		||||
	"signalGen_start": "Iniciar",
 | 
			
		||||
	"signalGen_stop": "Detener",
 | 
			
		||||
	"signalGen_scope": "Osciloscopio",
 | 
			
		||||
	"signalGen_spectrum": "Espectro",
 | 
			
		||||
	"signalGen_loop": "Bucle",
 | 
			
		||||
	"mic_title": "Prueba de micrófono",
 | 
			
		||||
	"mic_startMicrophone": "Iniciar micrófono",
 | 
			
		||||
	"mic_stop": "Detener",
 | 
			
		||||
	"mic_monitoringOn": "Monitorización: ON",
 | 
			
		||||
	"mic_monitoringOff": "Monitorización: OFF",
 | 
			
		||||
	"mic_gain": "Ganancia",
 | 
			
		||||
	"mic_monitorDelay": "Retraso del monitor",
 | 
			
		||||
	"mic_sampleRate": "Tasa de muestreo",
 | 
			
		||||
	"mic_inputDevice": "Dispositivo de entrada",
 | 
			
		||||
	"mic_volume": "Volumen",
 | 
			
		||||
	"mic_recording": "Grabación",
 | 
			
		||||
	"mic_startRecording": "Iniciar grabación",
 | 
			
		||||
	"mic_stopRecording": "Detener grabación",
 | 
			
		||||
	"mic_downloadRecording": "Descargar grabación",
 | 
			
		||||
	"mic_device": "Dispositivo",
 | 
			
		||||
	"mic_noMicFound": "No se encontró ningún micrófono",
 | 
			
		||||
	"mic_refresh": "Actualizar",
 | 
			
		||||
	"mic_clipping": "Recorte",
 | 
			
		||||
	"mic_constraints": "Restricciones",
 | 
			
		||||
	"mic_echoCancellation": "Cancelación de eco",
 | 
			
		||||
	"mic_noiseSuppression": "Supresión de ruido",
 | 
			
		||||
	"mic_agc": "Control automático de ganancia",
 | 
			
		||||
	"mic_applyConstraints": "Aplicar",
 | 
			
		||||
	"mic_channels": "Canales",
 | 
			
		||||
	"mic_stereo": "Estéreo",
 | 
			
		||||
	"mic_requested": "Solicitado",
 | 
			
		||||
	"mic_obtained": "Obtenido",
 | 
			
		||||
	"mic_peakNow": "Pico",
 | 
			
		||||
	"mic_peakHold": "Mantener pico",
 | 
			
		||||
	"mic_resetPeaks": "Restablecer picos",
 | 
			
		||||
	"mic_advanced": "Avanzado",
 | 
			
		||||
	"mic_default": "Predeterminado",
 | 
			
		||||
	"mic_on": "Encendido",
 | 
			
		||||
	"mic_off": "Apagado",
 | 
			
		||||
	"mic_mono": "Mono",
 | 
			
		||||
	"sensors_title": "Sensores",
 | 
			
		||||
	"sensors_geolocation": "Geolocalización",
 | 
			
		||||
	"sensors_start": "Iniciar",
 | 
			
		||||
	"sensors_stop": "Detener",
 | 
			
		||||
	"sensors_accuracy": "Precisión (m)",
 | 
			
		||||
	"sensors_altitude": "Altitud (m)",
 | 
			
		||||
	"sensors_heading": "Rumbo (grados)",
 | 
			
		||||
	"sensors_speed": "Velocidad (m/s)",
 | 
			
		||||
	"sensors_timestamp": "Marca de tiempo",
 | 
			
		||||
	"sensors_copy": "Copiar JSON",
 | 
			
		||||
	"sensors_copied": "Copiado al portapapeles",
 | 
			
		||||
	"sensors_notSupported": "No compatible con este dispositivo/navegador",
 | 
			
		||||
	"sensors_deviceMotion": "Movimiento del dispositivo",
 | 
			
		||||
	"sensors_deviceOrientation": "Orientación del dispositivo",
 | 
			
		||||
	"sensors_accelerometer": "Acelerómetro",
 | 
			
		||||
	"sensors_gyroscope": "Giroscopio",
 | 
			
		||||
	"sensors_magnetometer": "Magnetómetro",
 | 
			
		||||
	"sensors_ambientLight": "Luz ambiental",
 | 
			
		||||
	"sensors_illuminance": "Iluminancia (lux)",
 | 
			
		||||
	"sensors_barometer": "Barómetro",
 | 
			
		||||
	"sensors_pressure": "Presión (hPa)",
 | 
			
		||||
	"sensors_temperature": "Temperatura (°C)",
 | 
			
		||||
	"sensors_permissions": "Permisos",
 | 
			
		||||
	"sensors_enableMotionOrientation": "Activar Movimiento/Orientación",
 | 
			
		||||
	"sensors_motion": "Movimiento",
 | 
			
		||||
	"sensors_orientation": "Orientación",
 | 
			
		||||
	"sensors_status_granted": "Concedido",
 | 
			
		||||
	"sensors_status_denied": "Denegado",
 | 
			
		||||
	"sensors_status_unknown": "Desconocido"
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										170
									
								
								messages/fr.json
									
										
									
									
									
								
							
							
						
						
									
										170
									
								
								messages/fr.json
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -1,170 +0,0 @@
 | 
			
		|||
{
 | 
			
		||||
	"$schema": "https://inlang.com/schema/inlang-message-format",
 | 
			
		||||
	"search": "Rechercher",
 | 
			
		||||
	"tests_audio_description": "Vérifiez vos canaux stéréo ou votre sortie audio surround, vérifiez si vos haut-parleurs sont en phase.",
 | 
			
		||||
	"tests_audio_label": "Audio",
 | 
			
		||||
	"tests_av-sync_description": "Vérifiez si votre audio et votre vidéo sont synchronisés et mesurez le décalage.",
 | 
			
		||||
	"tests_av-sync_label": "Synchronisation Audio/Vidéo",
 | 
			
		||||
	"tests_card_description": "Carte de test pour votre écran ou projecteur, vérifiez les couleurs, la résolution et la géométrie.",
 | 
			
		||||
	"tests_card_label": "Carte",
 | 
			
		||||
	"tests_camera_description": "Vérifiez si votre webcam ou votre périphérique de capture fonctionne, sa qualité d'image, sa résolution et sa fréquence d'images. Prenez une photo.",
 | 
			
		||||
	"tests_camera_label": "Caméra",
 | 
			
		||||
	"tests_gamepad_description": "Testez votre manette de jeu, vérifiez si elle fonctionne, tous les boutons et joysticks, la dérive des sticks, les zones mortes et le calibrage.",
 | 
			
		||||
	"tests_gamepad_label": "Manette de jeu",
 | 
			
		||||
	"tests_keyboard_description": "Vérifiez si toutes les touches fonctionnent et quels codes de touche elles envoient.",
 | 
			
		||||
	"tests_keyboard_label": "Clavier",
 | 
			
		||||
	"tests_microphone_description": "Vérifiez si votre microphone fonctionne, sa qualité, son volume et le bruit.",
 | 
			
		||||
	"tests_microphone_label": "Microphone",
 | 
			
		||||
	"tests_mouse_description": "Vérifiez si votre souris ou votre périphérique tactile fonctionne correctement, s'il y a des zones mortes ou des sautillements.",
 | 
			
		||||
	"tests_mouse_label": "Souris",
 | 
			
		||||
	"tests_sensors_description": "Consultez la sortie des capteurs de votre appareil, par ex. GPS, accéléromètre, gyroscope, boussole, etc.",
 | 
			
		||||
	"tests_sensors_label": "Capteurs",
 | 
			
		||||
	"tests_internet_description": "Mesurez votre vitesse Internet et votre latence.",
 | 
			
		||||
	"tests_internet_label": "Vitesse Internet",
 | 
			
		||||
	"tests_timer_description": "Vérifiez si votre minuteur haute résolution fonctionne.",
 | 
			
		||||
	"tests_timer_label": "Minuteur haute résolution",
 | 
			
		||||
	"category_inputs": "Entrées",
 | 
			
		||||
	"category_outputs": "Sorties",
 | 
			
		||||
	"category_audio": "Audio",
 | 
			
		||||
	"category_video": "Vidéo",
 | 
			
		||||
	"category_control": "Contrôle",
 | 
			
		||||
	"category_misc": "Divers",
 | 
			
		||||
	"noTestsFound": "Aucun test trouvé.",
 | 
			
		||||
	"camera_title": "Test de la caméra",
 | 
			
		||||
	"camera_device": "Appareil",
 | 
			
		||||
	"camera_noCameraFound": "Aucune caméra trouvée",
 | 
			
		||||
	"camera_refresh": "Actualiser",
 | 
			
		||||
	"camera_resolution": "Résolution",
 | 
			
		||||
	"camera_frameRate": "Fréquence d'images",
 | 
			
		||||
	"camera_noCameraSelected": "Aucune caméra sélectionnée",
 | 
			
		||||
	"camera_takePicture": "Prendre une photo",
 | 
			
		||||
	"camera_unflipImage": "Retourner l'image",
 | 
			
		||||
	"camera_flipImage": "Inverser l'image",
 | 
			
		||||
	"camera_closeSnapshot": "Fermer l'instantané",
 | 
			
		||||
	"audio_channel_frontLeft": "Avant gauche",
 | 
			
		||||
	"audio_channel_frontCenter": "Avant centre",
 | 
			
		||||
	"audio_channel_frontRight": "Avant droit",
 | 
			
		||||
	"audio_channel_sideLeft": "Côté gauche",
 | 
			
		||||
	"audio_channel_sideRight": "Côté droit",
 | 
			
		||||
	"audio_channel_rearLeft": "Arrière gauche",
 | 
			
		||||
	"audio_channel_rearRight": "Arrière droit",
 | 
			
		||||
	"audio_channel_lfe": "LFE",
 | 
			
		||||
	"gamepad_title": "Tests de manette de jeu et de joystick",
 | 
			
		||||
	"gamepad_device": "Appareil",
 | 
			
		||||
	"gamepad_noGamepadsDetected": "Aucune manette de jeu détectée. (Essayez d'appuyer sur un bouton)",
 | 
			
		||||
	"gamepad_refresh": "Actualiser",
 | 
			
		||||
	"gamepad_buttons": "Boutons",
 | 
			
		||||
	"gamepad_axes": "Axes",
 | 
			
		||||
	"gamepad_history": "Historique",
 | 
			
		||||
	"audio_channelTests": "Tests de canaux",
 | 
			
		||||
	"audio_stereo": "Stéréo",
 | 
			
		||||
	"audio_surroundAudio": "Audio surround",
 | 
			
		||||
	"audio_surround51": "Surround 5.1",
 | 
			
		||||
	"audio_surround71": "Surround 7.1",
 | 
			
		||||
	"audio_phaseTest": "Test de phase",
 | 
			
		||||
	"audio_frequency": "Fréquence",
 | 
			
		||||
	"audio_inPhase": "En phase",
 | 
			
		||||
	"audio_outOfPhase": "Hors phase",
 | 
			
		||||
	"audio_stop": "Arrêter",
 | 
			
		||||
	"screenInfo_screenResolution": "Résolution de l'écran",
 | 
			
		||||
	"screenInfo_windowResolution": "Résolution de la fenêtre",
 | 
			
		||||
	"screenInfo_devicePixelRatio": "Ratio de pixels de l'appareil",
 | 
			
		||||
	"audio_channel_left": "Gauche",
 | 
			
		||||
	"audio_channel_center": "Centre",
 | 
			
		||||
	"audio_channel_right": "Droite",
 | 
			
		||||
	"keyboard_title": "Test du clavier",
 | 
			
		||||
	"keyboard_instruction": "Appuyez sur une touche du clavier pour voir l'objet événement et le code de la touche.",
 | 
			
		||||
	"keyboard_pressedKeys": "Touches enfoncées :",
 | 
			
		||||
	"timer_title": "Minuteur haute résolution",
 | 
			
		||||
	"timer_fps": "FPS",
 | 
			
		||||
	"timer_restart": "Redémarrer",
 | 
			
		||||
	"audio_stopCycling": "Arrêter le cycle",
 | 
			
		||||
	"audio_cycleThrough": "Parcourir",
 | 
			
		||||
	"common_back": "Retour",
 | 
			
		||||
	"audio_title": "Test audio",
 | 
			
		||||
	"avSync_title": "Synchronisation Audio/Vidéo",
 | 
			
		||||
	"internet_title": "Vitesse Internet",
 | 
			
		||||
	"tests_signal-generator_description": "Générez des ondes sinusoïdales, du bruit (blanc, rose, marron) et des balayages de fréquence. Comprend un oscilloscope et un spectre.",
 | 
			
		||||
	"tests_signal-generator_label": "Générateur de signaux",
 | 
			
		||||
	"signalGen_title": "Générateur de signaux",
 | 
			
		||||
	"signalGen_type": "Type",
 | 
			
		||||
	"signalGen_sine": "Sinusoïdal",
 | 
			
		||||
	"signalGen_sweep": "Balayage",
 | 
			
		||||
	"signalGen_noiseWhite": "Bruit blanc",
 | 
			
		||||
	"signalGen_noisePink": "Bruit rose",
 | 
			
		||||
	"signalGen_noiseBrown": "Bruit marron",
 | 
			
		||||
	"signalGen_frequency": "Fréquence",
 | 
			
		||||
	"signalGen_from": "De",
 | 
			
		||||
	"signalGen_to": "À",
 | 
			
		||||
	"signalGen_duration": "Durée",
 | 
			
		||||
	"signalGen_gain": "Volume",
 | 
			
		||||
	"signalGen_start": "Démarrer",
 | 
			
		||||
	"signalGen_stop": "Arrêter",
 | 
			
		||||
	"signalGen_scope": "Oscilloscope",
 | 
			
		||||
	"signalGen_spectrum": "Spectre",
 | 
			
		||||
	"signalGen_loop": "Boucle",
 | 
			
		||||
	"mic_title": "Test du microphone",
 | 
			
		||||
	"mic_startMicrophone": "Démarrer le microphone",
 | 
			
		||||
	"mic_stop": "Arrêter",
 | 
			
		||||
	"mic_monitoringOn": "Monitoring : ON",
 | 
			
		||||
	"mic_monitoringOff": "Monitoring : OFF",
 | 
			
		||||
	"mic_gain": "Gain",
 | 
			
		||||
	"mic_monitorDelay": "Délai du moniteur",
 | 
			
		||||
	"mic_sampleRate": "Taux d'échantillonnage",
 | 
			
		||||
	"mic_inputDevice": "Périphérique d'entrée",
 | 
			
		||||
	"mic_volume": "Volume",
 | 
			
		||||
	"mic_recording": "Enregistrement",
 | 
			
		||||
	"mic_startRecording": "Démarrer l'enregistrement",
 | 
			
		||||
	"mic_stopRecording": "Arrêter l'enregistrement",
 | 
			
		||||
	"mic_downloadRecording": "Télécharger l'enregistrement",
 | 
			
		||||
	"mic_device": "Appareil",
 | 
			
		||||
	"mic_noMicFound": "Aucun microphone trouvé",
 | 
			
		||||
	"mic_refresh": "Actualiser",
 | 
			
		||||
	"mic_clipping": "Écrêtage",
 | 
			
		||||
	"mic_constraints": "Contraintes",
 | 
			
		||||
	"mic_echoCancellation": "Annulation de l'écho",
 | 
			
		||||
	"mic_noiseSuppression": "Suppression du bruit",
 | 
			
		||||
	"mic_agc": "Contrôle automatique du gain",
 | 
			
		||||
	"mic_applyConstraints": "Appliquer",
 | 
			
		||||
	"mic_channels": "Canaux",
 | 
			
		||||
	"mic_stereo": "Stéréo",
 | 
			
		||||
	"mic_requested": "Demandé",
 | 
			
		||||
	"mic_obtained": "Obtenu",
 | 
			
		||||
	"mic_peakNow": "Pic",
 | 
			
		||||
	"mic_peakHold": "Maintien du pic",
 | 
			
		||||
	"mic_resetPeaks": "Réinitialiser les pics",
 | 
			
		||||
	"mic_advanced": "Avancé",
 | 
			
		||||
	"mic_default": "Défaut",
 | 
			
		||||
	"mic_on": "Activé",
 | 
			
		||||
	"mic_off": "Désactivé",
 | 
			
		||||
	"mic_mono": "Mono",
 | 
			
		||||
	"sensors_title": "Capteurs",
 | 
			
		||||
	"sensors_geolocation": "Géolocalisation",
 | 
			
		||||
	"sensors_start": "Démarrer",
 | 
			
		||||
	"sensors_stop": "Arrêter",
 | 
			
		||||
	"sensors_accuracy": "Précision (m)",
 | 
			
		||||
	"sensors_altitude": "Altitude (m)",
 | 
			
		||||
	"sensors_heading": "Cap (deg)",
 | 
			
		||||
	"sensors_speed": "Vitesse (m/s)",
 | 
			
		||||
	"sensors_timestamp": "Horodatage",
 | 
			
		||||
	"sensors_copy": "Copier JSON",
 | 
			
		||||
	"sensors_copied": "Copié dans le presse-papiers",
 | 
			
		||||
	"sensors_notSupported": "Non pris en charge sur cet appareil/navigateur",
 | 
			
		||||
	"sensors_deviceMotion": "Mouvement de l'appareil",
 | 
			
		||||
	"sensors_deviceOrientation": "Orientation de l'appareil",
 | 
			
		||||
	"sensors_accelerometer": "Accéléromètre",
 | 
			
		||||
	"sensors_gyroscope": "Gyroscope",
 | 
			
		||||
	"sensors_magnetometer": "Magnétomètre",
 | 
			
		||||
	"sensors_ambientLight": "Lumière ambiante",
 | 
			
		||||
	"sensors_illuminance": "Éclairement (lux)",
 | 
			
		||||
	"sensors_barometer": "Baromètre",
 | 
			
		||||
	"sensors_pressure": "Pression (hPa)",
 | 
			
		||||
	"sensors_temperature": "Température (°C)",
 | 
			
		||||
	"sensors_permissions": "Autorisations",
 | 
			
		||||
	"sensors_enableMotionOrientation": "Activer Mouvement/Orientation",
 | 
			
		||||
	"sensors_motion": "Mouvement",
 | 
			
		||||
	"sensors_orientation": "Orientation",
 | 
			
		||||
	"sensors_status_granted": "Accordé",
 | 
			
		||||
	"sensors_status_denied": "Refusé",
 | 
			
		||||
	"sensors_status_unknown": "Inconnu"
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										170
									
								
								messages/ja.json
									
										
									
									
									
								
							
							
						
						
									
										170
									
								
								messages/ja.json
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -1,170 +0,0 @@
 | 
			
		|||
{
 | 
			
		||||
	"$schema": "https://inlang.com/schema/inlang-message-format",
 | 
			
		||||
	"search": "検索",
 | 
			
		||||
	"tests_audio_description": "ステレオチャンネルまたはサラウンドオーディオ出力を確認し、スピーカーが同相であるかを確認します。",
 | 
			
		||||
	"tests_audio_label": "オーディオ",
 | 
			
		||||
	"tests_av-sync_description": "オーディオとビデオが同期しているかを確認し、遅延を測定します。",
 | 
			
		||||
	"tests_av-sync_label": "オーディオ/ビデオ同期",
 | 
			
		||||
	"tests_card_description": "ディスプレイまたはプロジェクターのテストカード、色、解像度、ジオメトリを確認します。",
 | 
			
		||||
	"tests_card_label": "カード",
 | 
			
		||||
	"tests_camera_description": "ウェブカメラまたはキャプチャデバイスが動作しているか、画質、解像度、フレームレートを確認します。スナップショットを撮ります。",
 | 
			
		||||
	"tests_camera_label": "カメラ",
 | 
			
		||||
	"tests_gamepad_description": "ゲームパッドをテストし、動作、すべてのボタンとジョイスティック、スティックのドリフト、デッドゾーン、キャリブレーションを確認します。",
 | 
			
		||||
	"tests_gamepad_label": "ゲームパッド",
 | 
			
		||||
	"tests_keyboard_description": "すべてのキーが機能しているかと、それらが送信するキーコードを確認します。",
 | 
			
		||||
	"tests_keyboard_label": "キーボード",
 | 
			
		||||
	"tests_microphone_description": "マイクが機能しているか、品質、音量、ノイズを確認します。",
 | 
			
		||||
	"tests_microphone_label": "マイク",
 | 
			
		||||
	"tests_mouse_description": "マウスまたはタッチデバイスが正しく機能しているか、デッドゾーンやジッターがないかを確認します。",
 | 
			
		||||
	"tests_mouse_label": "マウス",
 | 
			
		||||
	"tests_sensors_description": "デバイスのセンサー(GPS、加速度計、ジャイロスコープ、コンパスなど)の出力を確認します。",
 | 
			
		||||
	"tests_sensors_label": "センサー",
 | 
			
		||||
	"tests_internet_description": "インターネットの速度と遅延を測定します。",
 | 
			
		||||
	"tests_internet_label": "インターネット速度",
 | 
			
		||||
	"tests_timer_description": "高解像度タイマーが機能しているかを確認します。",
 | 
			
		||||
	"tests_timer_label": "高解像度タイマー",
 | 
			
		||||
	"category_inputs": "入力",
 | 
			
		||||
	"category_outputs": "出力",
 | 
			
		||||
	"category_audio": "オーディオ",
 | 
			
		||||
	"category_video": "ビデオ",
 | 
			
		||||
	"category_control": "コントロール",
 | 
			
		||||
	"category_misc": "その他",
 | 
			
		||||
	"noTestsFound": "テストが見つかりません。",
 | 
			
		||||
	"camera_title": "カメラテスト",
 | 
			
		||||
	"camera_device": "デバイス",
 | 
			
		||||
	"camera_noCameraFound": "カメラが見つかりません",
 | 
			
		||||
	"camera_refresh": "更新",
 | 
			
		||||
	"camera_resolution": "解像度",
 | 
			
		||||
	"camera_frameRate": "フレームレート",
 | 
			
		||||
	"camera_noCameraSelected": "カメラが選択されていません",
 | 
			
		||||
	"camera_takePicture": "写真を撮る",
 | 
			
		||||
	"camera_unflipImage": "画像の反転を元に戻す",
 | 
			
		||||
	"camera_flipImage": "画像を反転",
 | 
			
		||||
	"camera_closeSnapshot": "スナップショットを閉じる",
 | 
			
		||||
	"audio_channel_frontLeft": "フロント左",
 | 
			
		||||
	"audio_channel_frontCenter": "フロントセンター",
 | 
			
		||||
	"audio_channel_frontRight": "フロント右",
 | 
			
		||||
	"audio_channel_sideLeft": "サイド左",
 | 
			
		||||
	"audio_channel_sideRight": "サイド右",
 | 
			
		||||
	"audio_channel_rearLeft": "リア左",
 | 
			
		||||
	"audio_channel_rearRight": "リア右",
 | 
			
		||||
	"audio_channel_lfe": "LFE",
 | 
			
		||||
	"gamepad_title": "ゲームパッドとジョイスティックのテスト",
 | 
			
		||||
	"gamepad_device": "デバイス",
 | 
			
		||||
	"gamepad_noGamepadsDetected": "ゲームパッドが検出されません。(ボタンを押してみてください)",
 | 
			
		||||
	"gamepad_refresh": "更新",
 | 
			
		||||
	"gamepad_buttons": "ボタン",
 | 
			
		||||
	"gamepad_axes": "軸",
 | 
			
		||||
	"gamepad_history": "履歴",
 | 
			
		||||
	"audio_channelTests": "チャンネルテスト",
 | 
			
		||||
	"audio_stereo": "ステレオ",
 | 
			
		||||
	"audio_surroundAudio": "サラウンドオーディオ",
 | 
			
		||||
	"audio_surround51": "5.1サラウンド",
 | 
			
		||||
	"audio_surround71": "7.1サラウンド",
 | 
			
		||||
	"audio_phaseTest": "フェーズテスト",
 | 
			
		||||
	"audio_frequency": "周波数",
 | 
			
		||||
	"audio_inPhase": "同相",
 | 
			
		||||
	"audio_outOfPhase": "逆相",
 | 
			
		||||
	"audio_stop": "停止",
 | 
			
		||||
	"screenInfo_screenResolution": "画面解像度",
 | 
			
		||||
	"screenInfo_windowResolution": "ウィンドウ解像度",
 | 
			
		||||
	"screenInfo_devicePixelRatio": "デバイスピクセル比",
 | 
			
		||||
	"audio_channel_left": "左",
 | 
			
		||||
	"audio_channel_center": "中央",
 | 
			
		||||
	"audio_channel_right": "右",
 | 
			
		||||
	"keyboard_title": "キーボードテスト",
 | 
			
		||||
	"keyboard_instruction": "キーボードのキーを押して、イベントオブジェクトとキーコードを確認します。",
 | 
			
		||||
	"keyboard_pressedKeys": "押されたキー:",
 | 
			
		||||
	"timer_title": "高解像度タイマー",
 | 
			
		||||
	"timer_fps": "FPS",
 | 
			
		||||
	"timer_restart": "再起動",
 | 
			
		||||
	"audio_stopCycling": "サイクリングを停止",
 | 
			
		||||
	"audio_cycleThrough": "サイクルスルー",
 | 
			
		||||
	"common_back": "戻る",
 | 
			
		||||
	"audio_title": "オーディオテスト",
 | 
			
		||||
	"avSync_title": "オーディオ/ビデオ同期",
 | 
			
		||||
	"internet_title": "インターネット速度",
 | 
			
		||||
	"tests_signal-generator_description": "正弦波、ノイズ(ホワイト、ピンク、ブラウン)、周波数スイープを生成します。オシロスコープとスペクトラムが含まれています。",
 | 
			
		||||
	"tests_signal-generator_label": "信号発生器",
 | 
			
		||||
	"signalGen_title": "信号発生器",
 | 
			
		||||
	"signalGen_type": "タイプ",
 | 
			
		||||
	"signalGen_sine": "正弦波",
 | 
			
		||||
	"signalGen_sweep": "スイープ",
 | 
			
		||||
	"signalGen_noiseWhite": "ホワイトノイズ",
 | 
			
		||||
	"signalGen_noisePink": "ピンクノイズ",
 | 
			
		||||
	"signalGen_noiseBrown": "ブラウンノイズ",
 | 
			
		||||
	"signalGen_frequency": "周波数",
 | 
			
		||||
	"signalGen_from": "から",
 | 
			
		||||
	"signalGen_to": "まで",
 | 
			
		||||
	"signalGen_duration": "期間",
 | 
			
		||||
	"signalGen_gain": "音量",
 | 
			
		||||
	"signalGen_start": "開始",
 | 
			
		||||
	"signalGen_stop": "停止",
 | 
			
		||||
	"signalGen_scope": "オシロスコープ",
 | 
			
		||||
	"signalGen_spectrum": "スペクトラム",
 | 
			
		||||
	"signalGen_loop": "ループ",
 | 
			
		||||
	"mic_title": "マイクテスト",
 | 
			
		||||
	"mic_startMicrophone": "マイクを開始",
 | 
			
		||||
	"mic_stop": "停止",
 | 
			
		||||
	"mic_monitoringOn": "モニタリング:オン",
 | 
			
		||||
	"mic_monitoringOff": "モニタリング:オフ",
 | 
			
		||||
	"mic_gain": "ゲイン",
 | 
			
		||||
	"mic_monitorDelay": "モニター遅延",
 | 
			
		||||
	"mic_sampleRate": "サンプルレート",
 | 
			
		||||
	"mic_inputDevice": "入力デバイス",
 | 
			
		||||
	"mic_volume": "音量",
 | 
			
		||||
	"mic_recording": "録音",
 | 
			
		||||
	"mic_startRecording": "録音を開始",
 | 
			
		||||
	"mic_stopRecording": "録音を停止",
 | 
			
		||||
	"mic_downloadRecording": "録音をダウンロード",
 | 
			
		||||
	"mic_device": "デバイス",
 | 
			
		||||
	"mic_noMicFound": "マイクが見つかりません",
 | 
			
		||||
	"mic_refresh": "更新",
 | 
			
		||||
	"mic_clipping": "クリッピング",
 | 
			
		||||
	"mic_constraints": "制約",
 | 
			
		||||
	"mic_echoCancellation": "エコーキャンセル",
 | 
			
		||||
	"mic_noiseSuppression": "ノイズ抑制",
 | 
			
		||||
	"mic_agc": "自動ゲインコントロール",
 | 
			
		||||
	"mic_applyConstraints": "適用",
 | 
			
		||||
	"mic_channels": "チャンネル",
 | 
			
		||||
	"mic_stereo": "ステレオ",
 | 
			
		||||
	"mic_requested": "要求",
 | 
			
		||||
	"mic_obtained": "取得",
 | 
			
		||||
	"mic_peakNow": "ピーク",
 | 
			
		||||
	"mic_peakHold": "ピークホールド",
 | 
			
		||||
	"mic_resetPeaks": "ピークをリセット",
 | 
			
		||||
	"mic_advanced": "詳細",
 | 
			
		||||
	"mic_default": "デフォルト",
 | 
			
		||||
	"mic_on": "オン",
 | 
			
		||||
	"mic_off": "オフ",
 | 
			
		||||
	"mic_mono": "モノラル",
 | 
			
		||||
	"sensors_title": "センサー",
 | 
			
		||||
	"sensors_geolocation": "地理位置情報",
 | 
			
		||||
	"sensors_start": "開始",
 | 
			
		||||
	"sensors_stop": "停止",
 | 
			
		||||
	"sensors_accuracy": "精度 (m)",
 | 
			
		||||
	"sensors_altitude": "高度 (m)",
 | 
			
		||||
	"sensors_heading": "方角 (度)",
 | 
			
		||||
	"sensors_speed": "速度 (m/s)",
 | 
			
		||||
	"sensors_timestamp": "タイムスタンプ",
 | 
			
		||||
	"sensors_copy": "JSONをコピー",
 | 
			
		||||
	"sensors_copied": "クリップボードにコピーしました",
 | 
			
		||||
	"sensors_notSupported": "このデバイス/ブラウザではサポートされていません",
 | 
			
		||||
	"sensors_deviceMotion": "デバイスの動き",
 | 
			
		||||
	"sensors_deviceOrientation": "デバイスの向き",
 | 
			
		||||
	"sensors_accelerometer": "加速度計",
 | 
			
		||||
	"sensors_gyroscope": "ジャイロスコープ",
 | 
			
		||||
	"sensors_magnetometer": "磁力計",
 | 
			
		||||
	"sensors_ambientLight": "環境光",
 | 
			
		||||
	"sensors_illuminance": "照度 (lux)",
 | 
			
		||||
	"sensors_barometer": "気圧計",
 | 
			
		||||
	"sensors_pressure": "圧力 (hPa)",
 | 
			
		||||
	"sensors_temperature": "温度 (°C)",
 | 
			
		||||
	"sensors_permissions": "権限",
 | 
			
		||||
	"sensors_enableMotionOrientation": "モーション/オリエンテーションを有効にする",
 | 
			
		||||
	"sensors_motion": "モーション",
 | 
			
		||||
	"sensors_orientation": "オリエンテーション",
 | 
			
		||||
	"sensors_status_granted": "許可",
 | 
			
		||||
	"sensors_status_denied": "拒否",
 | 
			
		||||
	"sensors_status_unknown": "不明"
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,170 +0,0 @@
 | 
			
		|||
{
 | 
			
		||||
	"$schema": "https://inlang.com/schema/inlang-message-format",
 | 
			
		||||
	"search": "Пошук",
 | 
			
		||||
	"tests_audio_description": "Перевірте свої стереоканали або об'ємний аудіовихід, перевірте, чи ваші динаміки у фазі.",
 | 
			
		||||
	"tests_audio_label": "Аудіо",
 | 
			
		||||
	"tests_av-sync_description": "Перевірте, чи синхронізовані аудіо та відео, та виміряйте затримку.",
 | 
			
		||||
	"tests_av-sync_label": "Синхронізація аудіо/відео",
 | 
			
		||||
	"tests_card_description": "Тестова картка для вашого дисплея або проектора, перевірте кольори, роздільну здатність та геометрію.",
 | 
			
		||||
	"tests_card_label": "Картка",
 | 
			
		||||
	"tests_camera_description": "Перевірте, чи працює ваша веб-камера або пристрій захоплення, її якість зображення, роздільну здатність та частоту кадрів. Зробіть знімок.",
 | 
			
		||||
	"tests_camera_label": "Камера",
 | 
			
		||||
	"tests_gamepad_description": "Перевірте свій геймпад, перевірте, чи він працює, всі кнопки та джойстики, дрейф стіків, мертві зони та калібрування.",
 | 
			
		||||
	"tests_gamepad_label": "Геймпад",
 | 
			
		||||
	"tests_keyboard_description": "Перевірте, чи всі клавіші працюють і які коди клавіш вони надсилають.",
 | 
			
		||||
	"tests_keyboard_label": "Клавіатура",
 | 
			
		||||
	"tests_microphone_description": "Перевірте, чи працює ваш мікрофон, його якість, гучність та шум.",
 | 
			
		||||
	"tests_microphone_label": "Мікрофон",
 | 
			
		||||
	"tests_mouse_description": "Перевірте, чи правильно працює ваша миша або сенсорний пристрій, чи є мертві зони або тремтіння.",
 | 
			
		||||
	"tests_mouse_label": "Миша",
 | 
			
		||||
	"tests_sensors_description": "Перегляньте вивід датчиків вашого пристрою, наприклад, GPS, акселерометр, гіроскоп, компас тощо.",
 | 
			
		||||
	"tests_sensors_label": "Датчики",
 | 
			
		||||
	"tests_internet_description": "Виміряйте швидкість та затримку вашого Інтернету.",
 | 
			
		||||
	"tests_internet_label": "Швидкість Інтернету",
 | 
			
		||||
	"tests_timer_description": "Перевірте, чи працює ваш таймер високої роздільної здатності.",
 | 
			
		||||
	"tests_timer_label": "Таймер високої роздільної здатності",
 | 
			
		||||
	"category_inputs": "Входи",
 | 
			
		||||
	"category_outputs": "Виходи",
 | 
			
		||||
	"category_audio": "Аудіо",
 | 
			
		||||
	"category_video": "Відео",
 | 
			
		||||
	"category_control": "Управління",
 | 
			
		||||
	"category_misc": "Різне",
 | 
			
		||||
	"noTestsFound": "Тестів не знайдено.",
 | 
			
		||||
	"camera_title": "Тест камери",
 | 
			
		||||
	"camera_device": "Пристрій",
 | 
			
		||||
	"camera_noCameraFound": "Камеру не знайдено",
 | 
			
		||||
	"camera_refresh": "Оновити",
 | 
			
		||||
	"camera_resolution": "Роздільна здатність",
 | 
			
		||||
	"camera_frameRate": "Частота кадрів",
 | 
			
		||||
	"camera_noCameraSelected": "Камеру не вибрано",
 | 
			
		||||
	"camera_takePicture": "Зробити знімок",
 | 
			
		||||
	"camera_unflipImage": "Повернути зображення",
 | 
			
		||||
	"camera_flipImage": "Перевернути зображення",
 | 
			
		||||
	"camera_closeSnapshot": "Закрити знімок",
 | 
			
		||||
	"audio_channel_frontLeft": "Передній лівий",
 | 
			
		||||
	"audio_channel_frontCenter": "Передній центральний",
 | 
			
		||||
	"audio_channel_frontRight": "Передній правий",
 | 
			
		||||
	"audio_channel_sideLeft": "Бічний лівий",
 | 
			
		||||
	"audio_channel_sideRight": "Бічний правий",
 | 
			
		||||
	"audio_channel_rearLeft": "Задній лівий",
 | 
			
		||||
	"audio_channel_rearRight": "Задній правий",
 | 
			
		||||
	"audio_channel_lfe": "LFE",
 | 
			
		||||
	"gamepad_title": "Тести геймпада та джойстика",
 | 
			
		||||
	"gamepad_device": "Пристрій",
 | 
			
		||||
	"gamepad_noGamepadsDetected": "Геймпади не виявлено. (Спробуйте натиснути кнопку)",
 | 
			
		||||
	"gamepad_refresh": "Оновити",
 | 
			
		||||
	"gamepad_buttons": "Кнопки",
 | 
			
		||||
	"gamepad_axes": "Осі",
 | 
			
		||||
	"gamepad_history": "Історія",
 | 
			
		||||
	"audio_channelTests": "Тести каналів",
 | 
			
		||||
	"audio_stereo": "Стерео",
 | 
			
		||||
	"audio_surroundAudio": "Об'ємний звук",
 | 
			
		||||
	"audio_surround51": "5.1 Об'ємний",
 | 
			
		||||
	"audio_surround71": "7.1 Об'ємний",
 | 
			
		||||
	"audio_phaseTest": "Тест фази",
 | 
			
		||||
	"audio_frequency": "Частота",
 | 
			
		||||
	"audio_inPhase": "У фазі",
 | 
			
		||||
	"audio_outOfPhase": "Поза фазою",
 | 
			
		||||
	"audio_stop": "Зупинити",
 | 
			
		||||
	"screenInfo_screenResolution": "Роздільна здатність екрана",
 | 
			
		||||
	"screenInfo_windowResolution": "Роздільна здатність вікна",
 | 
			
		||||
	"screenInfo_devicePixelRatio": "Співвідношення пікселів пристрою",
 | 
			
		||||
	"audio_channel_left": "Лівий",
 | 
			
		||||
	"audio_channel_center": "Центр",
 | 
			
		||||
	"audio_channel_right": "Правий",
 | 
			
		||||
	"keyboard_title": "Тест клавіатури",
 | 
			
		||||
	"keyboard_instruction": "Натисніть клавішу на клавіатурі, щоб побачити об'єкт події та код клавіші.",
 | 
			
		||||
	"keyboard_pressedKeys": "Натиснуті клавіші:",
 | 
			
		||||
	"timer_title": "Таймер високої роздільної здатності",
 | 
			
		||||
	"timer_fps": "FPS",
 | 
			
		||||
	"timer_restart": "Перезапустити",
 | 
			
		||||
	"audio_stopCycling": "Зупинити цикл",
 | 
			
		||||
	"audio_cycleThrough": "Перебирати",
 | 
			
		||||
	"common_back": "Назад",
 | 
			
		||||
	"audio_title": "Тест аудіо",
 | 
			
		||||
	"avSync_title": "Синхронізація аудіо/відео",
 | 
			
		||||
	"internet_title": "Швидкість Інтернету",
 | 
			
		||||
	"tests_signal-generator_description": "Генеруйте синусоїди, шум (білий, рожевий, коричневий) та частотні розгортки. Включає осцилограф та спектр.",
 | 
			
		||||
	"tests_signal-generator_label": "Генератор сигналів",
 | 
			
		||||
	"signalGen_title": "Генератор сигналів",
 | 
			
		||||
	"signalGen_type": "Тип",
 | 
			
		||||
	"signalGen_sine": "Синусоїда",
 | 
			
		||||
	"signalGen_sweep": "Розгортка",
 | 
			
		||||
	"signalGen_noiseWhite": "Білий шум",
 | 
			
		||||
	"signalGen_noisePink": "Рожевий шум",
 | 
			
		||||
	"signalGen_noiseBrown": "Коричневий шум",
 | 
			
		||||
	"signalGen_frequency": "Частота",
 | 
			
		||||
	"signalGen_from": "Від",
 | 
			
		||||
	"signalGen_to": "До",
 | 
			
		||||
	"signalGen_duration": "Тривалість",
 | 
			
		||||
	"signalGen_gain": "Гучність",
 | 
			
		||||
	"signalGen_start": "Старт",
 | 
			
		||||
	"signalGen_stop": "Стоп",
 | 
			
		||||
	"signalGen_scope": "Осцилограф",
 | 
			
		||||
	"signalGen_spectrum": "Спектр",
 | 
			
		||||
	"signalGen_loop": "Цикл",
 | 
			
		||||
	"mic_title": "Тест мікрофона",
 | 
			
		||||
	"mic_startMicrophone": "Почати мікрофон",
 | 
			
		||||
	"mic_stop": "Зупинити",
 | 
			
		||||
	"mic_monitoringOn": "Моніторинг: УВІМК",
 | 
			
		||||
	"mic_monitoringOff": "Моніторинг: ВИМК",
 | 
			
		||||
	"mic_gain": "Посилення",
 | 
			
		||||
	"mic_monitorDelay": "Затримка монітора",
 | 
			
		||||
	"mic_sampleRate": "Частота дискретизації",
 | 
			
		||||
	"mic_inputDevice": "Пристрій введення",
 | 
			
		||||
	"mic_volume": "Гучність",
 | 
			
		||||
	"mic_recording": "Запис",
 | 
			
		||||
	"mic_startRecording": "Почати запис",
 | 
			
		||||
	"mic_stopRecording": "Зупинити запис",
 | 
			
		||||
	"mic_downloadRecording": "Завантажити запис",
 | 
			
		||||
	"mic_device": "Пристрій",
 | 
			
		||||
	"mic_noMicFound": "Мікрофон не знайдено",
 | 
			
		||||
	"mic_refresh": "Оновити",
 | 
			
		||||
	"mic_clipping": "Відсікання",
 | 
			
		||||
	"mic_constraints": "Обмеження",
 | 
			
		||||
	"mic_echoCancellation": "Скасування відлуння",
 | 
			
		||||
	"mic_noiseSuppression": "Придушення шуму",
 | 
			
		||||
	"mic_agc": "Автоматичне регулювання посилення",
 | 
			
		||||
	"mic_applyConstraints": "Застосувати",
 | 
			
		||||
	"mic_channels": "Канали",
 | 
			
		||||
	"mic_stereo": "Стерео",
 | 
			
		||||
	"mic_requested": "Запитано",
 | 
			
		||||
	"mic_obtained": "Отримано",
 | 
			
		||||
	"mic_peakNow": "Пік",
 | 
			
		||||
	"mic_peakHold": "Утримання піку",
 | 
			
		||||
	"mic_resetPeaks": "Скинути піки",
 | 
			
		||||
	"mic_advanced": "Розширений",
 | 
			
		||||
	"mic_default": "За замовчуванням",
 | 
			
		||||
	"mic_on": "Увімкнено",
 | 
			
		||||
	"mic_off": "Вимкнено",
 | 
			
		||||
	"mic_mono": "Моно",
 | 
			
		||||
	"sensors_title": "Датчики",
 | 
			
		||||
	"sensors_geolocation": "Геолокація",
 | 
			
		||||
	"sensors_start": "Старт",
 | 
			
		||||
	"sensors_stop": "Стоп",
 | 
			
		||||
	"sensors_accuracy": "Точність (м)",
 | 
			
		||||
	"sensors_altitude": "Висота (м)",
 | 
			
		||||
	"sensors_heading": "Напрямок (град)",
 | 
			
		||||
	"sensors_speed": "Швидкість (м/с)",
 | 
			
		||||
	"sensors_timestamp": "Часова мітка",
 | 
			
		||||
	"sensors_copy": "Копіювати JSON",
 | 
			
		||||
	"sensors_copied": "Скопійовано в буфер обміну",
 | 
			
		||||
	"sensors_notSupported": "Не підтримується на цьому пристрої/браузері",
 | 
			
		||||
	"sensors_deviceMotion": "Рух пристрою",
 | 
			
		||||
	"sensors_deviceOrientation": "Орієнтація пристрою",
 | 
			
		||||
	"sensors_accelerometer": "Акселерометр",
 | 
			
		||||
	"sensors_gyroscope": "Гіроскоп",
 | 
			
		||||
	"sensors_magnetometer": "Магнітометр",
 | 
			
		||||
	"sensors_ambientLight": "Освітленість",
 | 
			
		||||
	"sensors_illuminance": "Освітленість (люкс)",
 | 
			
		||||
	"sensors_barometer": "Барометр",
 | 
			
		||||
	"sensors_pressure": "Тиск (гПа)",
 | 
			
		||||
	"sensors_temperature": "Температура (°C)",
 | 
			
		||||
	"sensors_permissions": "Дозволи",
 | 
			
		||||
	"sensors_enableMotionOrientation": "Увімкнути рух/орієнтацію",
 | 
			
		||||
	"sensors_motion": "Рух",
 | 
			
		||||
	"sensors_orientation": "Орієнтація",
 | 
			
		||||
	"sensors_status_granted": "Надано",
 | 
			
		||||
	"sensors_status_denied": "Відмовлено",
 | 
			
		||||
	"sensors_status_unknown": "Невідомо"
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,170 +0,0 @@
 | 
			
		|||
{
 | 
			
		||||
	"$schema": "https://inlang.com/schema/inlang-message-format",
 | 
			
		||||
	"search": "搜索",
 | 
			
		||||
	"tests_audio_description": "检查您的立体声通道或环绕声音频输出,验证您的扬声器是否同相。",
 | 
			
		||||
	"tests_audio_label": "音频",
 | 
			
		||||
	"tests_av-sync_description": "检查您的音频和视频是否同步,并测量延迟。",
 | 
			
		||||
	"tests_av-sync_label": "音视频同步",
 | 
			
		||||
	"tests_card_description": "用于您的显示器或投影仪的测试卡,检查颜色、分辨率和几何形状。",
 | 
			
		||||
	"tests_card_label": "测试卡",
 | 
			
		||||
	"tests_camera_description": "检查您的网络摄像头或采集设备是否正常工作,其图像质量、分辨率和帧率。拍摄快照。",
 | 
			
		||||
	"tests_camera_label": "摄像头",
 | 
			
		||||
	"tests_gamepad_description": "测试您的游戏手柄,检查其是否正常工作,所有按钮和摇杆,摇杆漂移,死区和校准。",
 | 
			
		||||
	"tests_gamepad_label": "游戏手柄",
 | 
			
		||||
	"tests_keyboard_description": "检查所有按键是否正常工作以及它们发送的键码。",
 | 
			
		||||
	"tests_keyboard_label": "键盘",
 | 
			
		||||
	"tests_microphone_description": "检查您的麦克风是否正常工作,其质量、音量和噪音。",
 | 
			
		||||
	"tests_microphone_label": "麦克风",
 | 
			
		||||
	"tests_mouse_description": "检查您的鼠标或触摸设备是否正常工作,是否存在死区或抖动。",
 | 
			
		||||
	"tests_mouse_label": "鼠标",
 | 
			
		||||
	"tests_sensors_description": "查看您设备的传感器输出,例如GPS、加速度计、陀螺仪、指南针等。",
 | 
			
		||||
	"tests_sensors_label": "传感器",
 | 
			
		||||
	"tests_internet_description": "测量您的互联网速度和延迟。",
 | 
			
		||||
	"tests_internet_label": "网速",
 | 
			
		||||
	"tests_timer_description": "检查您的高分辨率计时器是否正常工作。",
 | 
			
		||||
	"tests_timer_label": "高分辨率计时器",
 | 
			
		||||
	"category_inputs": "输入",
 | 
			
		||||
	"category_outputs": "输出",
 | 
			
		||||
	"category_audio": "音频",
 | 
			
		||||
	"category_video": "视频",
 | 
			
		||||
	"category_control": "控制",
 | 
			
		||||
	"category_misc": "杂项",
 | 
			
		||||
	"noTestsFound": "未找到测试。",
 | 
			
		||||
	"camera_title": "摄像头测试",
 | 
			
		||||
	"camera_device": "设备",
 | 
			
		||||
	"camera_noCameraFound": "未找到摄像头",
 | 
			
		||||
	"camera_refresh": "刷新",
 | 
			
		||||
	"camera_resolution": "分辨率",
 | 
			
		||||
	"camera_frameRate": "帧率",
 | 
			
		||||
	"camera_noCameraSelected": "未选择摄像头",
 | 
			
		||||
	"camera_takePicture": "拍照",
 | 
			
		||||
	"camera_unflipImage": "取消翻转图像",
 | 
			
		||||
	"camera_flipImage": "翻转图像",
 | 
			
		||||
	"camera_closeSnapshot": "关闭快照",
 | 
			
		||||
	"audio_channel_frontLeft": "左前置",
 | 
			
		||||
	"audio_channel_frontCenter": "中前置",
 | 
			
		||||
	"audio_channel_frontRight": "右前置",
 | 
			
		||||
	"audio_channel_sideLeft": "左侧",
 | 
			
		||||
	"audio_channel_sideRight": "右侧",
 | 
			
		||||
	"audio_channel_rearLeft": "左后置",
 | 
			
		||||
	"audio_channel_rearRight": "右后置",
 | 
			
		||||
	"audio_channel_lfe": "LFE",
 | 
			
		||||
	"gamepad_title": "游戏手柄和摇杆测试",
 | 
			
		||||
	"gamepad_device": "设备",
 | 
			
		||||
	"gamepad_noGamepadsDetected": "未检测到游戏手柄。(请尝试按下一个按钮)",
 | 
			
		||||
	"gamepad_refresh": "刷新",
 | 
			
		||||
	"gamepad_buttons": "按钮",
 | 
			
		||||
	"gamepad_axes": "轴",
 | 
			
		||||
	"gamepad_history": "历史",
 | 
			
		||||
	"audio_channelTests": "声道测试",
 | 
			
		||||
	"audio_stereo": "立体声",
 | 
			
		||||
	"audio_surroundAudio": "环绕声",
 | 
			
		||||
	"audio_surround51": "5.1环绕声",
 | 
			
		||||
	"audio_surround71": "7.1环绕声",
 | 
			
		||||
	"audio_phaseTest": "相位测试",
 | 
			
		||||
	"audio_frequency": "频率",
 | 
			
		||||
	"audio_inPhase": "同相",
 | 
			
		||||
	"audio_outOfPhase": "异相",
 | 
			
		||||
	"audio_stop": "停止",
 | 
			
		||||
	"screenInfo_screenResolution": "屏幕分辨率",
 | 
			
		||||
	"screenInfo_windowResolution": "窗口分辨率",
 | 
			
		||||
	"screenInfo_devicePixelRatio": "设备像素比",
 | 
			
		||||
	"audio_channel_left": "左",
 | 
			
		||||
	"audio_channel_center": "中",
 | 
			
		||||
	"audio_channel_right": "右",
 | 
			
		||||
	"keyboard_title": "键盘测试",
 | 
			
		||||
	"keyboard_instruction": "按键盘上的一个键以查看事件对象和键码。",
 | 
			
		||||
	"keyboard_pressedKeys": "按下的键:",
 | 
			
		||||
	"timer_title": "高分辨率计时器",
 | 
			
		||||
	"timer_fps": "FPS",
 | 
			
		||||
	"timer_restart": "重新开始",
 | 
			
		||||
	"audio_stopCycling": "停止循环",
 | 
			
		||||
	"audio_cycleThrough": "循环浏览",
 | 
			
		||||
	"common_back": "返回",
 | 
			
		||||
	"audio_title": "音频测试",
 | 
			
		||||
	"avSync_title": "音视频同步",
 | 
			
		||||
	"internet_title": "网速",
 | 
			
		||||
	"tests_signal-generator_description": "生成正弦波、噪声(白噪声、粉红噪声、布朗噪声)和频率扫描。包括示波器和频谱图。",
 | 
			
		||||
	"tests_signal-generator_label": "信号发生器",
 | 
			
		||||
	"signalGen_title": "信号发生器",
 | 
			
		||||
	"signalGen_type": "类型",
 | 
			
		||||
	"signalGen_sine": "正弦波",
 | 
			
		||||
	"signalGen_sweep": "扫描",
 | 
			
		||||
	"signalGen_noiseWhite": "白噪声",
 | 
			
		||||
	"signalGen_noisePink": "粉红噪声",
 | 
			
		||||
	"signalGen_noiseBrown": "布朗噪声",
 | 
			
		||||
	"signalGen_frequency": "频率",
 | 
			
		||||
	"signalGen_from": "从",
 | 
			
		||||
	"signalGen_to": "到",
 | 
			
		||||
	"signalGen_duration": "持续时间",
 | 
			
		||||
	"signalGen_gain": "音量",
 | 
			
		||||
	"signalGen_start": "开始",
 | 
			
		||||
	"signalGen_stop": "停止",
 | 
			
		||||
	"signalGen_scope": "示波器",
 | 
			
		||||
	"signalGen_spectrum": "频谱图",
 | 
			
		||||
	"signalGen_loop": "循环",
 | 
			
		||||
	"mic_title": "麦克风测试",
 | 
			
		||||
	"mic_startMicrophone": "开始麦克风",
 | 
			
		||||
	"mic_stop": "停止",
 | 
			
		||||
	"mic_monitoringOn": "监听:开",
 | 
			
		||||
	"mic_monitoringOff": "监听:关",
 | 
			
		||||
	"mic_gain": "增益",
 | 
			
		||||
	"mic_monitorDelay": "监听延迟",
 | 
			
		||||
	"mic_sampleRate": "采样率",
 | 
			
		||||
	"mic_inputDevice": "输入设备",
 | 
			
		||||
	"mic_volume": "音量",
 | 
			
		||||
	"mic_recording": "录音",
 | 
			
		||||
	"mic_startRecording": "开始录音",
 | 
			
		||||
	"mic_stopRecording": "停止录音",
 | 
			
		||||
	"mic_downloadRecording": "下载录音",
 | 
			
		||||
	"mic_device": "设备",
 | 
			
		||||
	"mic_noMicFound": "未找到麦克风",
 | 
			
		||||
	"mic_refresh": "刷新",
 | 
			
		||||
	"mic_clipping": "削波",
 | 
			
		||||
	"mic_constraints": "约束",
 | 
			
		||||
	"mic_echoCancellation": "回声消除",
 | 
			
		||||
	"mic_noiseSuppression": "噪声抑制",
 | 
			
		||||
	"mic_agc": "自动增益控制",
 | 
			
		||||
	"mic_applyConstraints": "应用",
 | 
			
		||||
	"mic_channels": "声道",
 | 
			
		||||
	"mic_stereo": "立体声",
 | 
			
		||||
	"mic_requested": "已请求",
 | 
			
		||||
	"mic_obtained": "已获取",
 | 
			
		||||
	"mic_peakNow": "峰值",
 | 
			
		||||
	"mic_peakHold": "峰值保持",
 | 
			
		||||
	"mic_resetPeaks": "重置峰值",
 | 
			
		||||
	"mic_advanced": "高级",
 | 
			
		||||
	"mic_default": "默认",
 | 
			
		||||
	"mic_on": "开",
 | 
			
		||||
	"mic_off": "关",
 | 
			
		||||
	"mic_mono": "单声道",
 | 
			
		||||
	"sensors_title": "传感器",
 | 
			
		||||
	"sensors_geolocation": "地理位置",
 | 
			
		||||
	"sensors_start": "开始",
 | 
			
		||||
	"sensors_stop": "停止",
 | 
			
		||||
	"sensors_accuracy": "精度 (m)",
 | 
			
		||||
	"sensors_altitude": "高度 (m)",
 | 
			
		||||
	"sensors_heading": "方向 (度)",
 | 
			
		||||
	"sensors_speed": "速度 (m/s)",
 | 
			
		||||
	"sensors_timestamp": "时间戳",
 | 
			
		||||
	"sensors_copy": "复制JSON",
 | 
			
		||||
	"sensors_copied": "已复制到剪贴板",
 | 
			
		||||
	"sensors_notSupported": "此设备/浏览器不支持",
 | 
			
		||||
	"sensors_deviceMotion": "设备运动",
 | 
			
		||||
	"sensors_deviceOrientation": "设备方向",
 | 
			
		||||
	"sensors_accelerometer": "加速度计",
 | 
			
		||||
	"sensors_gyroscope": "陀螺仪",
 | 
			
		||||
	"sensors_magnetometer": "磁力计",
 | 
			
		||||
	"sensors_ambientLight": "环境光",
 | 
			
		||||
	"sensors_illuminance": "照度 (lux)",
 | 
			
		||||
	"sensors_barometer": "气压计",
 | 
			
		||||
	"sensors_pressure": "压力 (hPa)",
 | 
			
		||||
	"sensors_temperature": "温度 (°C)",
 | 
			
		||||
	"sensors_permissions": "权限",
 | 
			
		||||
	"sensors_enableMotionOrientation": "启用运动/方向",
 | 
			
		||||
	"sensors_motion": "运动",
 | 
			
		||||
	"sensors_orientation": "方向",
 | 
			
		||||
	"sensors_status_granted": "已授予",
 | 
			
		||||
	"sensors_status_denied": "已拒绝",
 | 
			
		||||
	"sensors_status_unknown": "未知"
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										63
									
								
								package.json
									
										
									
									
									
								
							
							
						
						
									
										63
									
								
								package.json
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -16,57 +16,38 @@
 | 
			
		|||
		"av:render:audio": "cd av-sync && node render-audio.js"
 | 
			
		||||
	},
 | 
			
		||||
	"devDependencies": {
 | 
			
		||||
		"@inlang/paraglide-js": "^2.0.0",
 | 
			
		||||
		"@inlang/plugin-m-function-matcher": "^2.1.0",
 | 
			
		||||
		"@inlang/plugin-message-format": "^4.0.0",
 | 
			
		||||
		"@tsconfig/svelte": "^5.0.4",
 | 
			
		||||
		"@tsconfig/svelte": "^5.0.2",
 | 
			
		||||
		"@types/debug": "^4.1.12",
 | 
			
		||||
		"@types/eslint": "8.56.0",
 | 
			
		||||
		"@types/lodash": "^4.17.15",
 | 
			
		||||
		"@types/pngjs": "^6.0.5",
 | 
			
		||||
		"@types/wait-on": "^5.3.4",
 | 
			
		||||
		"@typescript-eslint/eslint-plugin": "^6.21.0",
 | 
			
		||||
		"@typescript-eslint/parser": "^6.21.0",
 | 
			
		||||
		"commander": "^12.1.0",
 | 
			
		||||
		"@types/lodash": "^4.14.202",
 | 
			
		||||
		"@typescript-eslint/eslint-plugin": "^6.0.0",
 | 
			
		||||
		"@typescript-eslint/parser": "^6.0.0",
 | 
			
		||||
		"commander": "^12.0.0",
 | 
			
		||||
		"concurrently": "^8.2.2",
 | 
			
		||||
		"eslint": "^8.57.1",
 | 
			
		||||
		"eslint": "^8.56.0",
 | 
			
		||||
		"eslint-config-prettier": "^9.1.0",
 | 
			
		||||
		"eslint-plugin-svelte": "^2.46.1",
 | 
			
		||||
		"eslint-plugin-svelte": "^2.35.1",
 | 
			
		||||
		"node-wav": "^0.0.2",
 | 
			
		||||
		"pixelmatch": "^7.1.0",
 | 
			
		||||
		"pngjs": "^7.0.0",
 | 
			
		||||
		"prettier": "^3.5.0",
 | 
			
		||||
		"prettier-plugin-svelte": "^3.3.3",
 | 
			
		||||
		"puppeteer": "^22.15.0",
 | 
			
		||||
		"svelte-check": "^4.0.0",
 | 
			
		||||
		"vitest": "^3.2.4",
 | 
			
		||||
		"wait-on": "^9.0.1"
 | 
			
		||||
		"prettier": "^3.1.1",
 | 
			
		||||
		"prettier-plugin-svelte": "^3.1.2",
 | 
			
		||||
		"puppeteer": "^22.1.0",
 | 
			
		||||
		"svelte-check": "^3.6.0",
 | 
			
		||||
		"wait-on": "^7.2.0"
 | 
			
		||||
	},
 | 
			
		||||
	"type": "module",
 | 
			
		||||
	"dependencies": {
 | 
			
		||||
		"@fontsource/atkinson-hyperlegible": "^5.1.1",
 | 
			
		||||
		"@fontsource/b612": "^5.1.1",
 | 
			
		||||
		"@sveltejs/adapter-auto": "^3.3.1",
 | 
			
		||||
		"@sveltejs/adapter-static": "^3.0.8",
 | 
			
		||||
		"@sveltejs/kit": "^2.17.1",
 | 
			
		||||
		"@sveltejs/vite-plugin-svelte": "^4.0.0",
 | 
			
		||||
		"@fontsource/b612": "^5.0.8",
 | 
			
		||||
		"@sveltejs/adapter-auto": "^3.0.0",
 | 
			
		||||
		"@sveltejs/adapter-static": "^3.0.1",
 | 
			
		||||
		"@sveltejs/kit": "^2.0.0",
 | 
			
		||||
		"@sveltejs/vite-plugin-svelte": "^3.0.0",
 | 
			
		||||
		"@tabler/icons-webfont": "^2.47.0",
 | 
			
		||||
		"debug": "^4.4.0",
 | 
			
		||||
		"debug": "^4.3.4",
 | 
			
		||||
		"lodash": "^4.17.21",
 | 
			
		||||
		"normalize.css": "^8.0.1",
 | 
			
		||||
		"svelte": "^5.0.0",
 | 
			
		||||
		"tslib": "^2.8.1",
 | 
			
		||||
		"typescript": "^5.7.3",
 | 
			
		||||
		"vite": "^5.4.14"
 | 
			
		||||
	},
 | 
			
		||||
	"trustedDependencies": [
 | 
			
		||||
		"esbuild",
 | 
			
		||||
		"puppeteer",
 | 
			
		||||
		"svelte-preprocess"
 | 
			
		||||
	],
 | 
			
		||||
	"pnpm": {
 | 
			
		||||
		"onlyBuiltDependencies": [
 | 
			
		||||
			"esbuild"
 | 
			
		||||
		]
 | 
			
		||||
		"svelte": "^4.2.7",
 | 
			
		||||
		"tslib": "^2.4.1",
 | 
			
		||||
		"typescript": "^5.0.0",
 | 
			
		||||
		"vite": "^5.0.3"
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										3015
									
								
								pnpm-lock.yaml
									
										
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										3015
									
								
								pnpm-lock.yaml
									
										
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							
							
								
								
									
										1
									
								
								project.inlang/.gitignore
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								project.inlang/.gitignore
									
										
									
									
										vendored
									
									
								
							| 
						 | 
				
			
			@ -1 +0,0 @@
 | 
			
		|||
cache
 | 
			
		||||
| 
						 | 
				
			
			@ -1 +0,0 @@
 | 
			
		|||
bkDf3wpqa4gWMSsmqk
 | 
			
		||||
| 
						 | 
				
			
			@ -1,12 +0,0 @@
 | 
			
		|||
{
 | 
			
		||||
	"$schema": "https://inlang.com/schema/project-settings",
 | 
			
		||||
	"modules": [
 | 
			
		||||
		"./node_modules/@inlang/plugin-message-format/dist/index.js",
 | 
			
		||||
		"./node_modules/@inlang/plugin-m-function-matcher/dist/index.js"
 | 
			
		||||
	],
 | 
			
		||||
	"plugin.inlang.messageFormat": {
 | 
			
		||||
		"pathPattern": "./messages/{locale}.json"
 | 
			
		||||
	},
 | 
			
		||||
	"baseLocale": "en",
 | 
			
		||||
	"locales": ["en", "es", "fr", "de", "zh-CN", "ja", "cs", "ukr"]
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,10 +1,9 @@
 | 
			
		|||
<!doctype html>
 | 
			
		||||
<html lang="%paraglide.lang%">
 | 
			
		||||
<html lang="en">
 | 
			
		||||
	<head>
 | 
			
		||||
		<meta charset="utf-8" />
 | 
			
		||||
		<meta name="viewport" content="width=device-width, initial-scale=1" />
 | 
			
		||||
		<title>TOTAL TECH TEST</title>
 | 
			
		||||
		<link rel="icon" type="image/png" href="/favicon.png" />
 | 
			
		||||
		<title>TEST CARD</title>
 | 
			
		||||
		%sveltekit.head%
 | 
			
		||||
	</head>
 | 
			
		||||
	<body data-sveltekit-preload-data="hover">
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,13 +0,0 @@
 | 
			
		|||
import type { Handle } from '@sveltejs/kit';
 | 
			
		||||
import { paraglideMiddleware } from '$lib/paraglide/server';
 | 
			
		||||
 | 
			
		||||
const handleParaglide: Handle = ({ event, resolve }) =>
 | 
			
		||||
	paraglideMiddleware(event.request, ({ request, locale }) => {
 | 
			
		||||
		event.request = request;
 | 
			
		||||
 | 
			
		||||
		return resolve(event, {
 | 
			
		||||
			transformPageChunk: ({ html }) => html.replace('%paraglide.lang%', locale)
 | 
			
		||||
		});
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
export const handle: Handle = handleParaglide;
 | 
			
		||||
| 
						 | 
				
			
			@ -1,3 +0,0 @@
 | 
			
		|||
import { deLocalizeUrl } from '$lib/paraglide/runtime';
 | 
			
		||||
 | 
			
		||||
export const reroute = (request) => deLocalizeUrl(request.url).pathname;
 | 
			
		||||
| 
						 | 
				
			
			@ -1,81 +1,53 @@
 | 
			
		|||
body,
 | 
			
		||||
html {
 | 
			
		||||
	height: 100%;
 | 
			
		||||
	margin: 0;
 | 
			
		||||
	display: flex;
 | 
			
		||||
	justify-content: center;
 | 
			
		||||
	align-items: center;
 | 
			
		||||
body, html {
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  margin: 0;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
 | 
			
		||||
	color: white;
 | 
			
		||||
	background-color: black;
 | 
			
		||||
  color: white;
 | 
			
		||||
  background-color: black;
 | 
			
		||||
 | 
			
		||||
	font-family:
 | 
			
		||||
		'Atkinson Hyperlegible', 'B612', 'IBM Plex Sans', 'Helvetica Neue', Arial, sans-serif;
 | 
			
		||||
	font-size: 20px;
 | 
			
		||||
  font-family: 'B612', 'IBM Plex Sans', 'Helvetica Neue', Arial, sans-serif;
 | 
			
		||||
  font-size: min(1.5vw, 1.5vh);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
* {
 | 
			
		||||
	box-sizing: border-box;
 | 
			
		||||
  box-sizing: border-box;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
a {
 | 
			
		||||
	color: white;
 | 
			
		||||
  color: white;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
h1,
 | 
			
		||||
h2,
 | 
			
		||||
h3,
 | 
			
		||||
h4 {
 | 
			
		||||
	margin-top: 0;
 | 
			
		||||
 | 
			
		||||
h1, h2, h3, h4 {
 | 
			
		||||
  margin-top: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
button,
 | 
			
		||||
.button {
 | 
			
		||||
	display: inline-flex;
 | 
			
		||||
	align-items: center;
 | 
			
		||||
	gap: 0.25em;
 | 
			
		||||
	text-decoration: none;
 | 
			
		||||
	border: 1px solid white;
 | 
			
		||||
	cursor: pointer;
 | 
			
		||||
button, .button {
 | 
			
		||||
  display: inline-flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  gap: 0.25em;
 | 
			
		||||
  text-decoration: none;
 | 
			
		||||
  border: 1px solid white;
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
 | 
			
		||||
	padding: 0.25em 0.5em;
 | 
			
		||||
	border-radius: 0.25em;
 | 
			
		||||
  padding: 0.25em 0.5em;
 | 
			
		||||
  border-radius: 0.25em;
 | 
			
		||||
 | 
			
		||||
	background: black;
 | 
			
		||||
	color: white;
 | 
			
		||||
 | 
			
		||||
	&:disabled {
 | 
			
		||||
		opacity: 0.7;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
input[type='number'],
 | 
			
		||||
input[type='search'],
 | 
			
		||||
input[type='text'] {
 | 
			
		||||
	background: transparent;
 | 
			
		||||
	color: white;
 | 
			
		||||
	border: 1px solid white;
 | 
			
		||||
	border-radius: 4px;
 | 
			
		||||
	padding: 0.2em;
 | 
			
		||||
 | 
			
		||||
	&:focus {
 | 
			
		||||
		outline: solid rgba(255, 255, 255, 0.66);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	&:disabled {
 | 
			
		||||
		opacity: 0.7;
 | 
			
		||||
		pointer-events: none;
 | 
			
		||||
	}
 | 
			
		||||
  background: black;
 | 
			
		||||
  color: white;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
select {
 | 
			
		||||
	background: black;
 | 
			
		||||
	color: white;
 | 
			
		||||
	padding: 0.25em 0.5em;
 | 
			
		||||
	border-radius: 0.25em;
 | 
			
		||||
	border: 1px solid white;
 | 
			
		||||
  background: black;
 | 
			
		||||
  color: white;
 | 
			
		||||
  padding: 0.25em 0.5em;
 | 
			
		||||
  border-radius: 0.25em;
 | 
			
		||||
  border: 1px solid white;
 | 
			
		||||
 | 
			
		||||
	&:disabled {
 | 
			
		||||
		opacity: 0.7;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
  &:disabled {
 | 
			
		||||
    opacity: 0.7;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,8 +1,8 @@
 | 
			
		|||
<script>
 | 
			
		||||
	import { onMount } from 'svelte';
 | 
			
		||||
 | 
			
		||||
	let heightOdd = $state(false);
 | 
			
		||||
	let widthOdd = $state(false);
 | 
			
		||||
	let heightOdd = false;
 | 
			
		||||
	let widthOdd = false;
 | 
			
		||||
 | 
			
		||||
	function updateOdd() {
 | 
			
		||||
		heightOdd = window.innerHeight % 2 === 1;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,33 +1,23 @@
 | 
			
		|||
<script lang="ts">
 | 
			
		||||
	import _ from 'lodash';
 | 
			
		||||
	import { onMount } from 'svelte';
 | 
			
		||||
	import { createEventDispatcher, onMount } from 'svelte';
 | 
			
		||||
	const dispatch = createEventDispatcher();
 | 
			
		||||
 | 
			
		||||
	const START_COUNT = 27;
 | 
			
		||||
	const MAX_COUNT = 33;
 | 
			
		||||
	const MARGIN_SIZE = 16;
 | 
			
		||||
 | 
			
		||||
	let horizontalCount = $state(START_COUNT);
 | 
			
		||||
	let verticalCount = $state(START_COUNT);
 | 
			
		||||
	let blockSize = $state(64);
 | 
			
		||||
	let cornerBlocks = $state(2);
 | 
			
		||||
	let horizontalCount = START_COUNT;
 | 
			
		||||
	let verticalCount = START_COUNT;
 | 
			
		||||
	let blockSize = 64;
 | 
			
		||||
	let cornerBlocks = 2;
 | 
			
		||||
 | 
			
		||||
	let horizontalMargin = $state(MARGIN_SIZE);
 | 
			
		||||
	let verticalMargin = $state(MARGIN_SIZE);
 | 
			
		||||
	let unloaded = $state(true);
 | 
			
		||||
	let horizontalMargin = MARGIN_SIZE;
 | 
			
		||||
	let verticalMargin = MARGIN_SIZE;
 | 
			
		||||
	let unloaded = true;
 | 
			
		||||
 | 
			
		||||
	interface Props {
 | 
			
		||||
		transparent?: boolean;
 | 
			
		||||
		subdued?: boolean;
 | 
			
		||||
		onchange?: (detail: {
 | 
			
		||||
			horizontalCount: number;
 | 
			
		||||
			verticalCount: number;
 | 
			
		||||
			blockSize: number;
 | 
			
		||||
			horizontalMargin: number;
 | 
			
		||||
			verticalMargin: number;
 | 
			
		||||
		}) => void;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	let { transparent = false, subdued = false, onchange }: Props = $props();
 | 
			
		||||
	export let transparent = false;
 | 
			
		||||
	export let subdued = false;
 | 
			
		||||
 | 
			
		||||
	function updateCounts() {
 | 
			
		||||
		const gridWidth = window.innerWidth - MARGIN_SIZE;
 | 
			
		||||
| 
						 | 
				
			
			@ -62,7 +52,7 @@
 | 
			
		|||
		horizontalMargin = (window.innerWidth - blockSize * horizontalCount) / 2;
 | 
			
		||||
		verticalMargin = (window.innerHeight - blockSize * verticalCount) / 2;
 | 
			
		||||
		cornerBlocks = shorterCount > 8 ? 3 : Math.max(1, Math.floor(shorterCount / 4));
 | 
			
		||||
		onchange?.({
 | 
			
		||||
		dispatch('change', {
 | 
			
		||||
			horizontalCount,
 | 
			
		||||
			verticalCount,
 | 
			
		||||
			blockSize,
 | 
			
		||||
| 
						 | 
				
			
			@ -121,9 +111,9 @@
 | 
			
		|||
	</div>
 | 
			
		||||
	<div class="grid">
 | 
			
		||||
		{#each [...Array(verticalCount).keys()] as x}
 | 
			
		||||
			<div class="row" data-idx={x}>
 | 
			
		||||
			<div class="row">
 | 
			
		||||
				{#each [...Array(horizontalCount).keys()] as y}
 | 
			
		||||
					<div class="block" data-idx={y}></div>
 | 
			
		||||
					<div class="block"></div>
 | 
			
		||||
				{/each}
 | 
			
		||||
			</div>
 | 
			
		||||
		{/each}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,34 +0,0 @@
 | 
			
		|||
<script lang="ts">
 | 
			
		||||
	import { onMount } from 'svelte';
 | 
			
		||||
 | 
			
		||||
	let time = $state(new Date());
 | 
			
		||||
 | 
			
		||||
	onMount(() => {
 | 
			
		||||
		setInterval(() => {
 | 
			
		||||
			time = new Date();
 | 
			
		||||
		}, 1000);
 | 
			
		||||
	});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<div class="clock">
 | 
			
		||||
	<div class="h">{time.getHours()}</div>
 | 
			
		||||
	<div class="separator">:</div>
 | 
			
		||||
	<div class="m">{time.getMinutes().toString().padStart(2, '0')}</div>
 | 
			
		||||
	<div class="separator">:</div>
 | 
			
		||||
	<div class="s">{time.getSeconds().toString().padStart(2, '0')}</div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
	.clock {
 | 
			
		||||
		display: flex;
 | 
			
		||||
		font-size: 3em;
 | 
			
		||||
		text-align: center;
 | 
			
		||||
		letter-spacing: -0.05em;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.separator {
 | 
			
		||||
		position: relative;
 | 
			
		||||
		bottom: 0.15em;
 | 
			
		||||
		margin: 0 0.05em;
 | 
			
		||||
	}
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			@ -1,20 +1,17 @@
 | 
			
		|||
<script lang="ts">
 | 
			
		||||
	import { onMount } from 'svelte';
 | 
			
		||||
	import { fade } from 'svelte/transition';
 | 
			
		||||
	import { m } from '$lib/paraglide/messages';
 | 
			
		||||
 | 
			
		||||
	let screenResolution = $state('... x ...');
 | 
			
		||||
	let windowResolution = $state('');
 | 
			
		||||
	let dpr = $state('1');
 | 
			
		||||
	let screenResolution = '... x ...';
 | 
			
		||||
	let windowResolution = '';
 | 
			
		||||
 | 
			
		||||
	function updateResolution() {
 | 
			
		||||
		const realWidth = Math.round(screen.width) * window.devicePixelRatio;
 | 
			
		||||
		const realHeight = Math.round(screen.height) * window.devicePixelRatio;
 | 
			
		||||
		const realWidth = Math.round(screen.width * window.devicePixelRatio);
 | 
			
		||||
		const realHeight = Math.round(screen.height * window.devicePixelRatio);
 | 
			
		||||
		const windowWidth = Math.round(window.innerWidth * window.devicePixelRatio);
 | 
			
		||||
		const windowHeight = Math.round(window.innerHeight * window.devicePixelRatio);
 | 
			
		||||
		screenResolution = `${realWidth} x ${realHeight}`;
 | 
			
		||||
		windowResolution = `${windowWidth} x ${windowHeight}`;
 | 
			
		||||
		dpr = String(Math.round(window.devicePixelRatio * 100) / 100);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	onMount(() => {
 | 
			
		||||
| 
						 | 
				
			
			@ -27,32 +24,27 @@
 | 
			
		|||
 | 
			
		||||
<div class="info">
 | 
			
		||||
	<div class="resolution">
 | 
			
		||||
		<div class="title">{m.screenInfo_screenResolution()}</div>
 | 
			
		||||
		<div class="title">Screen Resolution</div>
 | 
			
		||||
		<div class="value">{screenResolution}</div>
 | 
			
		||||
		{#if windowResolution && windowResolution !== screenResolution}
 | 
			
		||||
			<div class="window" transition:fade>
 | 
			
		||||
				<div class="title">{m.screenInfo_windowResolution()}</div>
 | 
			
		||||
				<div class="title">Window Resolution</div>
 | 
			
		||||
				<div class="value">{windowResolution}</div>
 | 
			
		||||
			</div>
 | 
			
		||||
		{/if}
 | 
			
		||||
		{#if dpr !== '1'}
 | 
			
		||||
			<div class="dpr">{m.screenInfo_devicePixelRatio()}: {dpr}</div>
 | 
			
		||||
		{/if}
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
	.info {
 | 
			
		||||
		text-align: center;
 | 
			
		||||
		font-size: 0.8em;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.title {
 | 
			
		||||
		font-weight: bold;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.window,
 | 
			
		||||
	.dpr {
 | 
			
		||||
	.window {
 | 
			
		||||
		margin-top: calc(1em / 0.8);
 | 
			
		||||
		font-size: 0.8em;
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										22
									
								
								src/lib/Spinner.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								src/lib/Spinner.svelte
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,22 @@
 | 
			
		|||
<script>
 | 
			
		||||
	import { IconSpiral } from '@tabler/icons-svelte';
 | 
			
		||||
	export let size = 32;
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<div class="spinner"><IconSpiral {size} class="spinner-icon" /></div>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
	.spinner {
 | 
			
		||||
		text-align: center;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	:global(.spinner-icon) {
 | 
			
		||||
		animation: spin 1s linear infinite;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@keyframes spin {
 | 
			
		||||
		100% {
 | 
			
		||||
			transform: rotate(360deg);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			@ -4,38 +4,31 @@
 | 
			
		|||
	import Axes from '$lib/Axes.svelte';
 | 
			
		||||
	import ColorGradient from '$lib/ColorGradient.svelte';
 | 
			
		||||
	import BrightnessGradient from '$lib/BrightnessGradient.svelte';
 | 
			
		||||
	import Clock from '$lib/Clock.svelte';
 | 
			
		||||
	import { createEventDispatcher } from 'svelte';
 | 
			
		||||
	const dispatch = createEventDispatcher<{ focus: void }>();
 | 
			
		||||
 | 
			
		||||
	interface Props {
 | 
			
		||||
		bg?: boolean;
 | 
			
		||||
		onfocus?: () => void;
 | 
			
		||||
	}
 | 
			
		||||
	export let full = false;
 | 
			
		||||
 | 
			
		||||
	let { bg = false, onfocus }: Props = $props();
 | 
			
		||||
 | 
			
		||||
	let sizes = $state({
 | 
			
		||||
	let sizes = {
 | 
			
		||||
		blockSize: 64,
 | 
			
		||||
		horizontalCount: 16,
 | 
			
		||||
		verticalCount: 16,
 | 
			
		||||
		horizontalMargin: 0,
 | 
			
		||||
		verticalMargin: 0
 | 
			
		||||
	});
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	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(
 | 
			
		||||
	$: 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)
 | 
			
		||||
	);
 | 
			
		||||
		(sizes.horizontalCount % 2);
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
 | 
			
		||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
 | 
			
		||||
<div
 | 
			
		||||
	class="test-card"
 | 
			
		||||
	class:bg
 | 
			
		||||
	class:full
 | 
			
		||||
	style="--block-size: {sizes.blockSize}px;
 | 
			
		||||
				 --horizontal-margin: {sizes.horizontalMargin}px;
 | 
			
		||||
				 --vertical-margin: {sizes.verticalMargin}px;
 | 
			
		||||
| 
						 | 
				
			
			@ -43,9 +36,9 @@
 | 
			
		|||
         --column-width: {columnWidth};
 | 
			
		||||
         --column-height: {columnHeight};
 | 
			
		||||
         --left-column: {leftColumn};"
 | 
			
		||||
	ondblclick={() => onfocus?.() && document.body.requestFullscreen()}
 | 
			
		||||
	on:dblclick={() => dispatch('focus') && document.body.requestFullscreen()}
 | 
			
		||||
>
 | 
			
		||||
	<BackgroundGrid onchange={(detail) => (sizes = detail)} subdued={bg} />
 | 
			
		||||
	<BackgroundGrid on:change={(ev) => (sizes = ev.detail)} subdued={!full} />
 | 
			
		||||
 | 
			
		||||
	<div class="axes">
 | 
			
		||||
		<Axes />
 | 
			
		||||
| 
						 | 
				
			
			@ -55,7 +48,6 @@
 | 
			
		|||
	<div class="inner"></div>
 | 
			
		||||
 | 
			
		||||
	<div class="info">
 | 
			
		||||
		<Clock />
 | 
			
		||||
		<ScreenInfo />
 | 
			
		||||
	</div>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -86,10 +78,6 @@
 | 
			
		|||
		width: 100vw;
 | 
			
		||||
		height: 100vh;
 | 
			
		||||
		overflow: hidden;
 | 
			
		||||
 | 
			
		||||
		font-family: 'Atkinson Hyperlegible', 'IBM Plex Sans', 'Helvetica Neue', Arial, sans-serif;
 | 
			
		||||
		font-variant-numeric: tabular-nums;
 | 
			
		||||
		font-size: min(4vw, 4vh);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.inner {
 | 
			
		||||
| 
						 | 
				
			
			@ -126,11 +114,6 @@
 | 
			
		|||
		left: 50%;
 | 
			
		||||
		transform: translate(-50%, -50%);
 | 
			
		||||
		z-index: 10;
 | 
			
		||||
		display: flex;
 | 
			
		||||
		flex-direction: column;
 | 
			
		||||
		align-items: center;
 | 
			
		||||
		justify-content: center;
 | 
			
		||||
		gap: 1em;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.column {
 | 
			
		||||
| 
						 | 
				
			
			@ -158,7 +141,7 @@
 | 
			
		|||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.test-card.bg {
 | 
			
		||||
	.test-card:not(.full) {
 | 
			
		||||
		& .info,
 | 
			
		||||
		& .column,
 | 
			
		||||
		& .axes,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,86 +0,0 @@
 | 
			
		|||
<script lang="ts">
 | 
			
		||||
	import { onMount } from 'svelte';
 | 
			
		||||
 | 
			
		||||
	interface Props {
 | 
			
		||||
		analyser: AnalyserNode;
 | 
			
		||||
		title?: string;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	let { analyser, title = 'Oscilloscope' }: Props = $props();
 | 
			
		||||
 | 
			
		||||
	let canvas: HTMLCanvasElement | null = null;
 | 
			
		||||
	let animationId: number | null = null;
 | 
			
		||||
 | 
			
		||||
	function draw() {
 | 
			
		||||
		if (!canvas) return;
 | 
			
		||||
		const ctx = canvas.getContext('2d');
 | 
			
		||||
		if (!ctx) return;
 | 
			
		||||
 | 
			
		||||
		const dpr = Math.max(1, window.devicePixelRatio || 1);
 | 
			
		||||
		const w = canvas.clientWidth * dpr;
 | 
			
		||||
		const h = canvas.clientHeight * dpr;
 | 
			
		||||
		if (canvas.width !== w || canvas.height !== h) {
 | 
			
		||||
			canvas.width = w;
 | 
			
		||||
			canvas.height = h;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const bufferLength = analyser.fftSize;
 | 
			
		||||
		const dataArray = new Uint8Array(bufferLength);
 | 
			
		||||
 | 
			
		||||
		analyser.getByteTimeDomainData(dataArray);
 | 
			
		||||
 | 
			
		||||
		ctx.clearRect(0, 0, w, h);
 | 
			
		||||
		ctx.lineWidth = 2 * dpr;
 | 
			
		||||
		ctx.strokeStyle = '#5cb85c';
 | 
			
		||||
		ctx.beginPath();
 | 
			
		||||
 | 
			
		||||
		const sliceWidth = w / bufferLength;
 | 
			
		||||
		let x = 0;
 | 
			
		||||
		for (let i = 0; i < bufferLength; i++) {
 | 
			
		||||
			const v = dataArray[i] / 128.0; // 0..255 -> 0..2
 | 
			
		||||
			const y = (v * h) / 2;
 | 
			
		||||
			if (i === 0) {
 | 
			
		||||
				ctx.moveTo(x, y);
 | 
			
		||||
			} else {
 | 
			
		||||
				ctx.lineTo(x, y);
 | 
			
		||||
			}
 | 
			
		||||
			x += sliceWidth;
 | 
			
		||||
		}
 | 
			
		||||
		ctx.stroke();
 | 
			
		||||
 | 
			
		||||
		animationId = requestAnimationFrame(draw);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	onMount(() => {
 | 
			
		||||
		analyser.fftSize = 2048;
 | 
			
		||||
		analyser.smoothingTimeConstant = 0.2;
 | 
			
		||||
		animationId = requestAnimationFrame(draw);
 | 
			
		||||
		return () => {
 | 
			
		||||
			if (animationId) cancelAnimationFrame(animationId);
 | 
			
		||||
		};
 | 
			
		||||
	});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<div class="panel">
 | 
			
		||||
	<div class="title">{title}</div>
 | 
			
		||||
	<canvas bind:this={canvas} class="scope"></canvas>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
	.panel {
 | 
			
		||||
		display: flex;
 | 
			
		||||
		flex-direction: column;
 | 
			
		||||
		gap: 0.5rem;
 | 
			
		||||
	}
 | 
			
		||||
	.title {
 | 
			
		||||
		opacity: 0.85;
 | 
			
		||||
		font-size: 0.9rem;
 | 
			
		||||
	}
 | 
			
		||||
	.scope {
 | 
			
		||||
		width: 100%;
 | 
			
		||||
		height: 200px;
 | 
			
		||||
		background: rgba(255, 255, 255, 0.06);
 | 
			
		||||
		border: 1px solid rgba(255, 255, 255, 0.15);
 | 
			
		||||
		border-radius: 4px;
 | 
			
		||||
	}
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			@ -1,170 +0,0 @@
 | 
			
		|||
<script lang="ts">
 | 
			
		||||
	import { onMount } from 'svelte';
 | 
			
		||||
 | 
			
		||||
	interface Props {
 | 
			
		||||
		analyser: AnalyserNode;
 | 
			
		||||
		title?: string;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	let { analyser, title = 'Spectrum' }: Props = $props();
 | 
			
		||||
 | 
			
		||||
	let canvas: HTMLCanvasElement | null = null;
 | 
			
		||||
	let animationId: number | null = null;
 | 
			
		||||
 | 
			
		||||
	// Format frequency values nicely for axis labels
 | 
			
		||||
	function formatHz(f: number): string {
 | 
			
		||||
		if (f >= 1000) {
 | 
			
		||||
			const k = f / 1000;
 | 
			
		||||
			// Show at most one decimal place when needed
 | 
			
		||||
			return `${k % 1 === 0 ? k.toFixed(0) : k.toFixed(1)} kHz`;
 | 
			
		||||
		}
 | 
			
		||||
		return `${Math.round(f)} Hz`;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Generate 1-2-5 decade ticks up to Nyquist
 | 
			
		||||
	function generateFreqTicks(maxF: number): number[] {
 | 
			
		||||
		const ticks: number[] = [];
 | 
			
		||||
		const bases = [1, 2, 5];
 | 
			
		||||
		const maxExp = Math.floor(Math.log10(Math.max(1, maxF)));
 | 
			
		||||
		for (let e = 0; e <= maxExp; e++) {
 | 
			
		||||
			const pow = Math.pow(10, e);
 | 
			
		||||
			for (const b of bases) {
 | 
			
		||||
				const f = b * pow;
 | 
			
		||||
				if (f >= 20 && f <= maxF) ticks.push(f);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		// Ensure sorted unique ticks
 | 
			
		||||
		return [...new Set(ticks)].sort((a, b) => a - b);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function draw() {
 | 
			
		||||
		if (!canvas) return;
 | 
			
		||||
		const ctx = canvas.getContext('2d');
 | 
			
		||||
		if (!ctx) return;
 | 
			
		||||
 | 
			
		||||
		const dpr = Math.max(1, window.devicePixelRatio || 1);
 | 
			
		||||
		const w = canvas.clientWidth * dpr;
 | 
			
		||||
		const h = canvas.clientHeight * dpr;
 | 
			
		||||
		if (canvas.width !== w || canvas.height !== h) {
 | 
			
		||||
			canvas.width = w;
 | 
			
		||||
			canvas.height = h;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const bufferLength = analyser.frequencyBinCount;
 | 
			
		||||
		const dataArray = new Uint8Array(bufferLength);
 | 
			
		||||
		analyser.getByteFrequencyData(dataArray);
 | 
			
		||||
 | 
			
		||||
		ctx.clearRect(0, 0, w, h);
 | 
			
		||||
 | 
			
		||||
		// Layout for axis and grid
 | 
			
		||||
		const sampleRate = analyser.context.sampleRate;
 | 
			
		||||
		const nyquist = sampleRate / 2;
 | 
			
		||||
		const axisPad = 24 * dpr; // space reserved at bottom for tick labels
 | 
			
		||||
		const plotTop = 0;
 | 
			
		||||
		const plotBottom = h - axisPad;
 | 
			
		||||
		const plotH = Math.max(1, plotBottom - plotTop);
 | 
			
		||||
 | 
			
		||||
		ctx.fillStyle = 'rgba(92, 184, 92, 0.25)';
 | 
			
		||||
		ctx.strokeStyle = '#5cb85c';
 | 
			
		||||
		ctx.lineWidth = 2 * dpr;
 | 
			
		||||
		ctx.beginPath();
 | 
			
		||||
 | 
			
		||||
		const barWidth = w / bufferLength;
 | 
			
		||||
		let x = 0;
 | 
			
		||||
		for (let i = 0; i < bufferLength; i++) {
 | 
			
		||||
			const v = dataArray[i] / 255.0;
 | 
			
		||||
			const y = plotBottom - v * plotH;
 | 
			
		||||
			if (i === 0) {
 | 
			
		||||
				ctx.moveTo(x, y);
 | 
			
		||||
			} else {
 | 
			
		||||
				ctx.lineTo(x, y);
 | 
			
		||||
			}
 | 
			
		||||
			x += barWidth;
 | 
			
		||||
		}
 | 
			
		||||
		ctx.stroke();
 | 
			
		||||
 | 
			
		||||
		// Draw frequency axis, gridlines, and labels
 | 
			
		||||
		ctx.save();
 | 
			
		||||
		const gridColor = 'rgba(255, 255, 255, 0.15)';
 | 
			
		||||
		const labelColor = 'rgba(255, 255, 255, 0.8)';
 | 
			
		||||
		// Baseline
 | 
			
		||||
		ctx.strokeStyle = gridColor;
 | 
			
		||||
		ctx.lineWidth = 1 * dpr;
 | 
			
		||||
		ctx.setLineDash([]);
 | 
			
		||||
		ctx.beginPath();
 | 
			
		||||
		ctx.moveTo(0, plotBottom);
 | 
			
		||||
		ctx.lineTo(w, plotBottom);
 | 
			
		||||
		ctx.stroke();
 | 
			
		||||
 | 
			
		||||
		// Ticks
 | 
			
		||||
		const ticks = generateFreqTicks(nyquist);
 | 
			
		||||
		const tickLen = 6 * dpr;
 | 
			
		||||
		ctx.font = `${Math.round(11 * dpr)}px system-ui, -apple-system, Segoe UI, Roboto, sans-serif`;
 | 
			
		||||
		ctx.textAlign = 'center';
 | 
			
		||||
		ctx.textBaseline = 'top';
 | 
			
		||||
		let lastLabelX = -Infinity;
 | 
			
		||||
		const minLabelSpacing = 60 * dpr;
 | 
			
		||||
 | 
			
		||||
		for (const f of ticks) {
 | 
			
		||||
			const xTick = (f / nyquist) * w;
 | 
			
		||||
			// Gridline
 | 
			
		||||
			ctx.strokeStyle = gridColor;
 | 
			
		||||
			ctx.lineWidth = 1 * dpr;
 | 
			
		||||
			ctx.setLineDash([2 * dpr, 4 * dpr]);
 | 
			
		||||
			ctx.beginPath();
 | 
			
		||||
			ctx.moveTo(xTick, plotTop);
 | 
			
		||||
			ctx.lineTo(xTick, plotBottom);
 | 
			
		||||
			ctx.stroke();
 | 
			
		||||
 | 
			
		||||
			// Tick mark at the axis
 | 
			
		||||
			ctx.setLineDash([]);
 | 
			
		||||
			ctx.beginPath();
 | 
			
		||||
			ctx.moveTo(xTick, plotBottom);
 | 
			
		||||
			ctx.lineTo(xTick, plotBottom + tickLen);
 | 
			
		||||
			ctx.stroke();
 | 
			
		||||
 | 
			
		||||
			// Label if there's room
 | 
			
		||||
			if (xTick - lastLabelX >= minLabelSpacing) {
 | 
			
		||||
				ctx.fillStyle = labelColor;
 | 
			
		||||
				ctx.fillText(formatHz(f), xTick, plotBottom + tickLen + 2 * dpr);
 | 
			
		||||
				lastLabelX = xTick;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		ctx.restore();
 | 
			
		||||
 | 
			
		||||
		animationId = requestAnimationFrame(draw);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	onMount(() => {
 | 
			
		||||
		analyser.fftSize = 2048;
 | 
			
		||||
		analyser.smoothingTimeConstant = 0.8;
 | 
			
		||||
		animationId = requestAnimationFrame(draw);
 | 
			
		||||
		return () => {
 | 
			
		||||
			if (animationId) cancelAnimationFrame(animationId);
 | 
			
		||||
		};
 | 
			
		||||
	});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<div class="panel">
 | 
			
		||||
	<div class="title">{title}</div>
 | 
			
		||||
	<canvas bind:this={canvas} class="spectrum"></canvas>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
	.panel {
 | 
			
		||||
		display: flex;
 | 
			
		||||
		flex-direction: column;
 | 
			
		||||
		gap: 0.5rem;
 | 
			
		||||
	}
 | 
			
		||||
	.title {
 | 
			
		||||
		opacity: 0.85;
 | 
			
		||||
		font-size: 0.9rem;
 | 
			
		||||
	}
 | 
			
		||||
	.spectrum {
 | 
			
		||||
		width: 100%;
 | 
			
		||||
		height: 200px;
 | 
			
		||||
		background: rgba(255, 255, 255, 0.06);
 | 
			
		||||
		border: 1px solid rgba(255, 255, 255, 0.15);
 | 
			
		||||
		border-radius: 4px;
 | 
			
		||||
	}
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			@ -1,5 +0,0 @@
 | 
			
		|||
<script>
 | 
			
		||||
	import TestCard from '$lib/TestCard.svelte';
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<TestCard />
 | 
			
		||||
| 
						 | 
				
			
			@ -1,116 +0,0 @@
 | 
			
		|||
<script lang="ts">
 | 
			
		||||
	import TestCard from '$lib/TestCard.svelte';
 | 
			
		||||
 | 
			
		||||
	let x = $state(-1);
 | 
			
		||||
	let y = $state(-1);
 | 
			
		||||
	let leftButton = $state(false);
 | 
			
		||||
	let rightButton = $state(false);
 | 
			
		||||
 | 
			
		||||
	function onMouseMove(ev: MouseEvent) {
 | 
			
		||||
		x = ev.x;
 | 
			
		||||
		y = ev.y;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function onMouseDown(ev: MouseEvent) {
 | 
			
		||||
		if (ev.button === 0) {
 | 
			
		||||
			leftButton = true;
 | 
			
		||||
		} else if (ev.button === 2) {
 | 
			
		||||
			rightButton = true;
 | 
			
		||||
		}
 | 
			
		||||
		ev.preventDefault();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function onMouseUp(ev: MouseEvent) {
 | 
			
		||||
		if (ev.button === 0) {
 | 
			
		||||
			leftButton = false;
 | 
			
		||||
		} else if (ev.button === 2) {
 | 
			
		||||
			rightButton = false;
 | 
			
		||||
		}
 | 
			
		||||
		ev.preventDefault();
 | 
			
		||||
	}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<svelte:body
 | 
			
		||||
	onmousemove={onMouseMove}
 | 
			
		||||
	onmousedown={onMouseDown}
 | 
			
		||||
	onmouseup={onMouseUp}
 | 
			
		||||
	oncontextmenu={(ev) => {
 | 
			
		||||
		ev.preventDefault();
 | 
			
		||||
		return false;
 | 
			
		||||
	}}
 | 
			
		||||
/>
 | 
			
		||||
 | 
			
		||||
<div class="background">
 | 
			
		||||
	<TestCard bg />
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<div class="indicator" style="--x: {x}px; --y: {y}px">
 | 
			
		||||
	<div class="x"></div>
 | 
			
		||||
	<div class="y"></div>
 | 
			
		||||
	<div class="click left" class:pressed={leftButton}></div>
 | 
			
		||||
	<div class="click right" class:pressed={rightButton}></div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
	.background {
 | 
			
		||||
		opacity: 0.33;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.indicator {
 | 
			
		||||
		position: absolute;
 | 
			
		||||
		top: 0;
 | 
			
		||||
		left: 0;
 | 
			
		||||
 | 
			
		||||
		overflow: hidden;
 | 
			
		||||
		width: 100vw;
 | 
			
		||||
		height: 100vh;
 | 
			
		||||
 | 
			
		||||
		& .x,
 | 
			
		||||
		& .y {
 | 
			
		||||
			position: absolute;
 | 
			
		||||
			background: white;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		& .x {
 | 
			
		||||
			height: 100vh;
 | 
			
		||||
			width: 1px;
 | 
			
		||||
			top: 0;
 | 
			
		||||
			left: var(--x);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		& .y {
 | 
			
		||||
			height: 1px;
 | 
			
		||||
			width: 100vw;
 | 
			
		||||
			top: var(--y);
 | 
			
		||||
			left: 0;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		& .click {
 | 
			
		||||
			position: absolute;
 | 
			
		||||
			top: var(--y);
 | 
			
		||||
			left: var(--x);
 | 
			
		||||
			transform: translate(-50%, -50%);
 | 
			
		||||
 | 
			
		||||
			width: 3rem;
 | 
			
		||||
			height: 3rem;
 | 
			
		||||
			border-radius: 50%;
 | 
			
		||||
 | 
			
		||||
			opacity: 0;
 | 
			
		||||
 | 
			
		||||
			&.left {
 | 
			
		||||
				background: red;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			&.right {
 | 
			
		||||
				background: yellow;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			&.pressed {
 | 
			
		||||
				opacity: 0.5;
 | 
			
		||||
				transition: none;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			transition: opacity 1s ease-out;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			@ -1,53 +0,0 @@
 | 
			
		|||
<script lang="ts">
 | 
			
		||||
	import TestCard from '$lib/TestCard.svelte';
 | 
			
		||||
	import { page } from '$app/state';
 | 
			
		||||
	import { goto } from '$app/navigation';
 | 
			
		||||
	import { m } from '$lib/paraglide/messages';
 | 
			
		||||
	interface Props {
 | 
			
		||||
		children?: import('svelte').Snippet;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	let { children }: Props = $props();
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<TestCard bg onfocus={() => goto('/card')} />
 | 
			
		||||
<main class:sub={!page.data.root}>
 | 
			
		||||
	<a href=".." class="button button-back"><i class="ti ti-arrow-back"></i>{m.common_back()}</a>
 | 
			
		||||
	{@render children?.()}
 | 
			
		||||
</main>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
	main {
 | 
			
		||||
		position: absolute;
 | 
			
		||||
		top: 50%;
 | 
			
		||||
		left: 50%;
 | 
			
		||||
		transform: translate(-50%, -50%);
 | 
			
		||||
		height: 90vh;
 | 
			
		||||
		width: 90vw;
 | 
			
		||||
 | 
			
		||||
		background: rgba(0, 0, 0, 0.85);
 | 
			
		||||
		border-radius: 0.5rem;
 | 
			
		||||
		border: 1px solid white;
 | 
			
		||||
 | 
			
		||||
		padding: 1rem;
 | 
			
		||||
 | 
			
		||||
		display: flex;
 | 
			
		||||
		flex-direction: column;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.button-back {
 | 
			
		||||
		position: absolute;
 | 
			
		||||
		top: 1rem;
 | 
			
		||||
		right: 1rem;
 | 
			
		||||
 | 
			
		||||
		opacity: 0.66;
 | 
			
		||||
		transition: opacity 0.3s;
 | 
			
		||||
		&:hover {
 | 
			
		||||
			opacity: 1;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	main:not(.sub) .button-back {
 | 
			
		||||
		display: none;
 | 
			
		||||
	}
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			@ -1,409 +0,0 @@
 | 
			
		|||
<script lang="ts">
 | 
			
		||||
	import { version } from '../../../package.json';
 | 
			
		||||
	import type { Snapshot } from '@sveltejs/kit';
 | 
			
		||||
	import { fade } from 'svelte/transition';
 | 
			
		||||
	import { onMount } from 'svelte';
 | 
			
		||||
	const buildDate = import.meta.env.VITE_BUILD_DATE || '???';
 | 
			
		||||
	import { m } from '$lib/paraglide/messages.js';
 | 
			
		||||
	import { setLocale } from '$lib/paraglide/runtime';
 | 
			
		||||
 | 
			
		||||
	let search = $state('');
 | 
			
		||||
 | 
			
		||||
	type Entry = {
 | 
			
		||||
		id: string;
 | 
			
		||||
		icon: string;
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	type Test = Entry & {
 | 
			
		||||
		categories: Array<Category>;
 | 
			
		||||
		disabled?: boolean;
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	type Category = (typeof categories)[number]['id'] | (typeof superCategories)[number]['id'];
 | 
			
		||||
 | 
			
		||||
	let superCategories = [
 | 
			
		||||
		{
 | 
			
		||||
			id: 'inputs',
 | 
			
		||||
			icon: 'ti-arrow-down-to-arc'
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			id: 'outputs',
 | 
			
		||||
			icon: 'ti-arrow-down-from-arc'
 | 
			
		||||
		}
 | 
			
		||||
	] as const satisfies Entry[];
 | 
			
		||||
 | 
			
		||||
	let categories = [
 | 
			
		||||
		{
 | 
			
		||||
			id: 'audio',
 | 
			
		||||
			icon: 'ti-volume'
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			id: 'video',
 | 
			
		||||
			icon: 'ti-video'
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			id: 'control',
 | 
			
		||||
			icon: 'ti-hand-finger'
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			id: 'misc',
 | 
			
		||||
			icon: 'ti-circle-plus'
 | 
			
		||||
		}
 | 
			
		||||
	] as const satisfies Entry[];
 | 
			
		||||
 | 
			
		||||
	const tests = [
 | 
			
		||||
		{
 | 
			
		||||
			id: 'card',
 | 
			
		||||
			icon: 'ti-device-desktop',
 | 
			
		||||
			categories: ['outputs', 'video']
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			id: 'audio',
 | 
			
		||||
			icon: 'ti-volume',
 | 
			
		||||
			categories: ['outputs', 'audio']
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			id: 'signal-generator',
 | 
			
		||||
			icon: 'ti-wave-sine',
 | 
			
		||||
			categories: ['outputs', 'audio']
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			id: 'av-sync',
 | 
			
		||||
			icon: 'ti-time-duration-off',
 | 
			
		||||
			categories: ['outputs', 'video', 'audio']
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			id: 'keyboard',
 | 
			
		||||
			icon: 'ti-keyboard',
 | 
			
		||||
			categories: ['inputs', 'control']
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			id: 'mouse',
 | 
			
		||||
			icon: 'ti-mouse',
 | 
			
		||||
			categories: ['inputs', 'control']
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			id: 'gamepad',
 | 
			
		||||
			icon: 'ti-device-gamepad',
 | 
			
		||||
			categories: ['inputs', 'control']
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			id: 'camera',
 | 
			
		||||
			icon: 'ti-camera',
 | 
			
		||||
			categories: ['inputs', 'video']
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			id: 'microphone',
 | 
			
		||||
			icon: 'ti-microphone',
 | 
			
		||||
			categories: ['inputs', 'audio']
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			id: 'sensors',
 | 
			
		||||
			icon: 'ti-cpu-2',
 | 
			
		||||
			categories: ['inputs', 'misc']
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			id: 'internet',
 | 
			
		||||
			icon: 'ti-world',
 | 
			
		||||
			categories: ['inputs', 'outputs', 'misc']
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			id: 'timer',
 | 
			
		||||
			icon: 'ti-alarm',
 | 
			
		||||
			categories: ['video']
 | 
			
		||||
		}
 | 
			
		||||
	] as const satisfies Test[];
 | 
			
		||||
 | 
			
		||||
	let categoriesToIcons = $derived.by(() => {
 | 
			
		||||
		const map = new Map<string, string>();
 | 
			
		||||
		for (const category of [...superCategories, ...categories]) {
 | 
			
		||||
			map.set(category.id, category.icon);
 | 
			
		||||
		}
 | 
			
		||||
		return map;
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	let filteredTests: Array<(typeof tests)[number]> = $state(tests);
 | 
			
		||||
	let filteredCategories: Category[] = $state([]);
 | 
			
		||||
 | 
			
		||||
	function doSearch(search: string) {
 | 
			
		||||
		filteredTests = tests.filter((test) => {
 | 
			
		||||
			if (!search) return true;
 | 
			
		||||
 | 
			
		||||
			const searchValue = search.toLocaleLowerCase();
 | 
			
		||||
			return (
 | 
			
		||||
				test.id.includes(searchValue) ||
 | 
			
		||||
				m[`tests_${test.id}_label`]().includes(searchValue) ||
 | 
			
		||||
				m[`tests_${test.id}_description`]().includes(searchValue)
 | 
			
		||||
			);
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
	$effect(() => {
 | 
			
		||||
		doSearch(search);
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	function setFilter(category: Category) {
 | 
			
		||||
		if (filteredCategories.includes(category)) {
 | 
			
		||||
			filteredCategories = filteredCategories.filter((c) => c !== category);
 | 
			
		||||
		} else {
 | 
			
		||||
			filteredCategories = [...filteredCategories, category];
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	let nonEmptyCategories = $derived(
 | 
			
		||||
		categories.filter((category) => {
 | 
			
		||||
			const categoryTests = filteredTests.filter((test) =>
 | 
			
		||||
				(test.categories as readonly Category[]).includes(category.id)
 | 
			
		||||
			);
 | 
			
		||||
			return categoryTests.some(
 | 
			
		||||
				(test) =>
 | 
			
		||||
					!filteredCategories.length ||
 | 
			
		||||
					filteredCategories.every((f) => (test.categories as readonly Category[]).includes(f))
 | 
			
		||||
			);
 | 
			
		||||
		})
 | 
			
		||||
	);
 | 
			
		||||
 | 
			
		||||
	export const snapshot: Snapshot<string> = {
 | 
			
		||||
		capture: () => JSON.stringify({ filtered: filteredCategories, search }),
 | 
			
		||||
		restore: (value) => {
 | 
			
		||||
			const { filtered: restoredFiltered, search: restoredSearch } = JSON.parse(value);
 | 
			
		||||
			filteredCategories = restoredFiltered;
 | 
			
		||||
			search = restoredSearch;
 | 
			
		||||
		}
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	// Locale selection (cookie-based persistence)
 | 
			
		||||
	type LocaleOption = { tag: string; native: string };
 | 
			
		||||
	const locales = [
 | 
			
		||||
		{ tag: 'en', native: 'English' },
 | 
			
		||||
		{ tag: 'es', native: 'Español' },
 | 
			
		||||
		{ tag: 'fr', native: 'Français' },
 | 
			
		||||
		{ tag: 'de', native: 'Deutsch' },
 | 
			
		||||
		{ tag: 'zh-CN', native: '简体中文' },
 | 
			
		||||
		{ tag: 'ja', native: '日本語' },
 | 
			
		||||
		{ tag: 'cs', native: 'Čeština' },
 | 
			
		||||
		{ tag: 'ukr', native: 'Українська' }
 | 
			
		||||
	] as const satisfies LocaleOption[];
 | 
			
		||||
	type Locale = (typeof locales)[number]['tag'];
 | 
			
		||||
 | 
			
		||||
	let selectedLang = $state('en');
 | 
			
		||||
 | 
			
		||||
	onMount(() => {
 | 
			
		||||
		const current = document.documentElement.getAttribute('lang') || selectedLang;
 | 
			
		||||
		selectedLang = locales.some((l) => l.tag === current) ? current : selectedLang;
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	function changeLocale(tag: string) {
 | 
			
		||||
		if (locales.some((l) => l.tag === tag)) {
 | 
			
		||||
			setLocale(tag as Locale);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<h1>Total Tech Test</h1>
 | 
			
		||||
 | 
			
		||||
<div class="locale-select">
 | 
			
		||||
	<label for="locale-picker"><i class="ti ti-language"></i></label>
 | 
			
		||||
	<select
 | 
			
		||||
		id="locale-picker"
 | 
			
		||||
		bind:value={selectedLang}
 | 
			
		||||
		onchange={(e) => changeLocale((e.target as HTMLSelectElement).value)}
 | 
			
		||||
	>
 | 
			
		||||
		{#each locales as l}
 | 
			
		||||
			<option value={l.tag}>{l.native}</option>
 | 
			
		||||
		{/each}
 | 
			
		||||
	</select>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<nav>
 | 
			
		||||
	<!-- svelte-ignore a11y_autofocus -->
 | 
			
		||||
	<input type="search" placeholder={m.search()} bind:value={search} autofocus />
 | 
			
		||||
 | 
			
		||||
	<div class="options">
 | 
			
		||||
		{#each superCategories as category}
 | 
			
		||||
			<button
 | 
			
		||||
				onclick={() => setFilter(category.id)}
 | 
			
		||||
				class:active={!filteredCategories.length || filteredCategories.includes(category.id)}
 | 
			
		||||
				class="super"
 | 
			
		||||
			>
 | 
			
		||||
				<i class="ti {category.icon}"></i>
 | 
			
		||||
				{m[`category_${category.id}`]()}
 | 
			
		||||
			</button>
 | 
			
		||||
		{/each}
 | 
			
		||||
		<div class="separator"></div>
 | 
			
		||||
		{#each categories as category}
 | 
			
		||||
			<button
 | 
			
		||||
				onclick={() => setFilter(category.id)}
 | 
			
		||||
				class:active={!filteredCategories.length || filteredCategories.includes(category.id)}
 | 
			
		||||
			>
 | 
			
		||||
				<i class="ti {category.icon}"></i>
 | 
			
		||||
				{m[`category_${category.id}`]()}
 | 
			
		||||
			</button>
 | 
			
		||||
		{/each}
 | 
			
		||||
	</div>
 | 
			
		||||
	<div class="tests">
 | 
			
		||||
		{#each nonEmptyCategories as category}
 | 
			
		||||
			{#if tests.filter( (test) => (test.categories as readonly Category[]).includes(category.id) ).length > 0}
 | 
			
		||||
				<h2 transition:fade={{ duration: 200 }}>{m[`category_${category.id}`]()}</h2>
 | 
			
		||||
				{#each filteredTests.filter((test) => (test.categories as readonly Category[]).includes(category.id) && filteredCategories.every( (f) => (test.categories as readonly Category[]).includes(f) )) as test}
 | 
			
		||||
					<a
 | 
			
		||||
						class="test"
 | 
			
		||||
						href={test.id}
 | 
			
		||||
						class:disabled={(test as Test).disabled}
 | 
			
		||||
						transition:fade={{ duration: 200 }}
 | 
			
		||||
					>
 | 
			
		||||
						<i class="ti {test.icon}"></i>
 | 
			
		||||
						<div class="label">
 | 
			
		||||
							<span class="name">{m[`tests_${test.id}_label`]()}</span>
 | 
			
		||||
							{#each test.categories as category}
 | 
			
		||||
								<span class="category"><i class="ti {categoriesToIcons.get(category)}"></i></span>
 | 
			
		||||
							{/each}
 | 
			
		||||
						</div>
 | 
			
		||||
						<div class="description">
 | 
			
		||||
							{m[`tests_${test.id}_description`]()}
 | 
			
		||||
						</div>
 | 
			
		||||
					</a>
 | 
			
		||||
				{/each}
 | 
			
		||||
			{/if}
 | 
			
		||||
		{:else}
 | 
			
		||||
			<p>
 | 
			
		||||
				{m.noTestsFound()}
 | 
			
		||||
			</p>
 | 
			
		||||
		{/each}
 | 
			
		||||
	</div>
 | 
			
		||||
</nav>
 | 
			
		||||
<footer>
 | 
			
		||||
	<a href="https://git.thm.place/thm/test-card"
 | 
			
		||||
		>testcard v{version}
 | 
			
		||||
		{#if version.startsWith('0')}({buildDate}){/if}</a
 | 
			
		||||
	>
 | 
			
		||||
</footer>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
	nav {
 | 
			
		||||
		display: flex;
 | 
			
		||||
		flex-direction: column;
 | 
			
		||||
		gap: 1rem;
 | 
			
		||||
		flex-grow: 1;
 | 
			
		||||
		padding: 0 4rem;
 | 
			
		||||
		overflow: hidden;
 | 
			
		||||
 | 
			
		||||
		& input[type='search'] {
 | 
			
		||||
			padding: 0.5em;
 | 
			
		||||
			width: 100%;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.tests {
 | 
			
		||||
		overflow-y: auto;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	h1 {
 | 
			
		||||
		text-align: center;
 | 
			
		||||
		font-size: 3rem;
 | 
			
		||||
		margin: 1rem;
 | 
			
		||||
		text-transform: uppercase;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	h2 {
 | 
			
		||||
		margin: 0.25em 0;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.options {
 | 
			
		||||
		display: flex;
 | 
			
		||||
		justify-content: space-evenly;
 | 
			
		||||
		align-items: center;
 | 
			
		||||
		gap: 2em;
 | 
			
		||||
 | 
			
		||||
		& button {
 | 
			
		||||
			border: none;
 | 
			
		||||
			background: none;
 | 
			
		||||
			display: flex;
 | 
			
		||||
			flex-direction: column;
 | 
			
		||||
 | 
			
		||||
			opacity: 0.5;
 | 
			
		||||
 | 
			
		||||
			transition: opacity 0.2s ease-in-out;
 | 
			
		||||
 | 
			
		||||
			&.active {
 | 
			
		||||
				opacity: 1;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		& .separator {
 | 
			
		||||
			border-left: 1px solid currentColor;
 | 
			
		||||
			height: 3rem;
 | 
			
		||||
			opacity: 0.8;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		& .ti {
 | 
			
		||||
			display: block;
 | 
			
		||||
			font-size: 3rem;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.locale-select {
 | 
			
		||||
		display: inline-flex;
 | 
			
		||||
		align-items: center;
 | 
			
		||||
		gap: 0.5rem;
 | 
			
		||||
		align-self: flex-end;
 | 
			
		||||
		position: absolute;
 | 
			
		||||
		right: 1rem;
 | 
			
		||||
		top: 1rem;
 | 
			
		||||
		opacity: 0.5;
 | 
			
		||||
		transition: opacity 0.2s ease-in-out;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.locale-select:hover {
 | 
			
		||||
		opacity: 1;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.locale-select select {
 | 
			
		||||
		background: rgba(255, 255, 255, 0.08);
 | 
			
		||||
		color: inherit;
 | 
			
		||||
		border: 1px solid currentColor;
 | 
			
		||||
		padding: 0.25rem 0.5rem;
 | 
			
		||||
		border-radius: 0.25rem;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.test {
 | 
			
		||||
		display: grid;
 | 
			
		||||
		grid-template-columns: auto 1fr;
 | 
			
		||||
		align-items: center;
 | 
			
		||||
		gap: 0 0.25em;
 | 
			
		||||
		margin-bottom: 0.25em;
 | 
			
		||||
 | 
			
		||||
		text-decoration: none;
 | 
			
		||||
		color: inherit;
 | 
			
		||||
 | 
			
		||||
		& .label {
 | 
			
		||||
			display: inline-flex;
 | 
			
		||||
			align-items: baseline;
 | 
			
		||||
			gap: 0.25em;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		& .label .category,
 | 
			
		||||
		& .description {
 | 
			
		||||
			opacity: 0.85;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		& .description {
 | 
			
		||||
			font-size: 0.85em;
 | 
			
		||||
			grid-column-start: 2;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		&.disabled {
 | 
			
		||||
			opacity: 0.5;
 | 
			
		||||
			pointer-events: none;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	footer {
 | 
			
		||||
		text-align: center;
 | 
			
		||||
		opacity: 0.6;
 | 
			
		||||
		margin-top: 1rem;
 | 
			
		||||
 | 
			
		||||
		& a {
 | 
			
		||||
			text-decoration: none;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			@ -1,11 +0,0 @@
 | 
			
		|||
<script lang="ts">
 | 
			
		||||
	import { m } from '$lib/paraglide/messages';
 | 
			
		||||
	interface Props {
 | 
			
		||||
		children?: import('svelte').Snippet;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	let { children }: Props = $props();
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<h2><i class="ti ti-volume"></i> {m.audio_title()}</h2>
 | 
			
		||||
{@render children?.()}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,41 +0,0 @@
 | 
			
		|||
<script lang="ts">
 | 
			
		||||
	import StereoTest from './(channels)/stereo-test.svelte';
 | 
			
		||||
	import PhaseTest from './phase.svelte';
 | 
			
		||||
	import { m } from '$lib/paraglide/messages';
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<article>
 | 
			
		||||
	<h3>{m.audio_channelTests()}</h3>
 | 
			
		||||
	<h4>{m.audio_stereo()}</h4>
 | 
			
		||||
	<section>
 | 
			
		||||
		<StereoTest />
 | 
			
		||||
	</section>
 | 
			
		||||
	<h4>{m.audio_surroundAudio()}</h4>
 | 
			
		||||
	<section>
 | 
			
		||||
		<ul>
 | 
			
		||||
			<li><a class="button" href="channels-5.1">{m.audio_surround51()}</a></li>
 | 
			
		||||
			<li><a class="button" href="channels-7.1">{m.audio_surround71()}</a></li>
 | 
			
		||||
		</ul>
 | 
			
		||||
	</section>
 | 
			
		||||
	<h3>{m.audio_phaseTest()}</h3>
 | 
			
		||||
	<PhaseTest />
 | 
			
		||||
</article>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
	h4 {
 | 
			
		||||
		margin-bottom: 0;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ul {
 | 
			
		||||
		list-style-type: none;
 | 
			
		||||
		padding: 0;
 | 
			
		||||
		margin: 0;
 | 
			
		||||
 | 
			
		||||
		display: inline-flex;
 | 
			
		||||
		gap: 1em;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	section {
 | 
			
		||||
		margin: 1em 0;
 | 
			
		||||
	}
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			@ -1,87 +0,0 @@
 | 
			
		|||
<script lang="ts">
 | 
			
		||||
	import { onMount } from 'svelte';
 | 
			
		||||
	import { m } from '$lib/paraglide/messages';
 | 
			
		||||
 | 
			
		||||
	let frequency = $state(60);
 | 
			
		||||
	let playing = $state(false);
 | 
			
		||||
 | 
			
		||||
	let audioCtx: AudioContext | undefined;
 | 
			
		||||
	let oscillatorL: OscillatorNode | undefined;
 | 
			
		||||
	let oscillatorR: OscillatorNode | undefined;
 | 
			
		||||
 | 
			
		||||
	onMount(() => {
 | 
			
		||||
		audioCtx = new window.AudioContext();
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	function start(mode: 'inPhase' | 'outOfPhase') {
 | 
			
		||||
		if (!audioCtx) return;
 | 
			
		||||
		oscillatorL?.stop();
 | 
			
		||||
		oscillatorR?.stop();
 | 
			
		||||
 | 
			
		||||
		oscillatorL = audioCtx.createOscillator();
 | 
			
		||||
		oscillatorR = audioCtx.createOscillator();
 | 
			
		||||
		const gainNode = audioCtx.createGain();
 | 
			
		||||
 | 
			
		||||
		const stereoPannerL = audioCtx.createStereoPanner();
 | 
			
		||||
		const stereoPannerR = audioCtx.createStereoPanner();
 | 
			
		||||
 | 
			
		||||
		oscillatorL.frequency.setValueAtTime(frequency, audioCtx.currentTime);
 | 
			
		||||
		oscillatorR.frequency.setValueAtTime(frequency, audioCtx.currentTime);
 | 
			
		||||
 | 
			
		||||
		stereoPannerL.pan.setValueAtTime(-1, audioCtx.currentTime);
 | 
			
		||||
		stereoPannerR.pan.setValueAtTime(1, audioCtx.currentTime);
 | 
			
		||||
 | 
			
		||||
		oscillatorL.connect(stereoPannerL).connect(audioCtx.destination);
 | 
			
		||||
		oscillatorR.connect(gainNode).connect(stereoPannerR).connect(audioCtx.destination);
 | 
			
		||||
 | 
			
		||||
		if (mode === 'inPhase') {
 | 
			
		||||
			gainNode?.gain.setValueAtTime(1, audioCtx.currentTime); // Normal phase
 | 
			
		||||
		} else {
 | 
			
		||||
			gainNode?.gain.setValueAtTime(-1, audioCtx.currentTime); // Invert phase
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		oscillatorL?.start();
 | 
			
		||||
		oscillatorR?.start();
 | 
			
		||||
		playing = true;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	async function stop() {
 | 
			
		||||
		oscillatorL?.stop();
 | 
			
		||||
		oscillatorR?.stop();
 | 
			
		||||
		playing = false;
 | 
			
		||||
	}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<div class="test">
 | 
			
		||||
	<label>
 | 
			
		||||
		{m.audio_frequency()}
 | 
			
		||||
		<input type="number" bind:value={frequency} min="20" max="20000" disabled={playing} />Hz
 | 
			
		||||
	</label>
 | 
			
		||||
	<div class="controls">
 | 
			
		||||
		<button onclick={() => start('inPhase')}>{m.audio_inPhase()}</button>
 | 
			
		||||
		<button onclick={() => start('outOfPhase')}>{m.audio_outOfPhase()}</button>
 | 
			
		||||
		<button class="stop" onclick={stop} disabled={!playing}>{m.audio_stop()}</button>
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
	.test {
 | 
			
		||||
		margin: 1em 0;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.controls {
 | 
			
		||||
		margin-top: 0.5em;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.stop {
 | 
			
		||||
		margin-left: 1em;
 | 
			
		||||
 | 
			
		||||
		&:not(:disabled) {
 | 
			
		||||
			background: darkred;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	input {
 | 
			
		||||
		width: 5em;
 | 
			
		||||
	}
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			@ -1,31 +0,0 @@
 | 
			
		|||
<script>
 | 
			
		||||
	import { m } from '$lib/paraglide/messages';
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<h2><i class="ti ti-world"></i> {m.internet_title()}</h2>
 | 
			
		||||
 | 
			
		||||
<div class="test">
 | 
			
		||||
	<iframe src="//openspeedtest.com/speedtest" title="OpenSpeedTest Embed"></iframe>
 | 
			
		||||
</div>
 | 
			
		||||
<div class="attribution">
 | 
			
		||||
	Provided by <a href="https://openspeedtest.com">OpenSpeedtest.com</a>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
	iframe {
 | 
			
		||||
		border: none;
 | 
			
		||||
		flex-grow: 1;
 | 
			
		||||
		max-height: 50vh;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.test {
 | 
			
		||||
		display: flex;
 | 
			
		||||
		flex-direction: column;
 | 
			
		||||
		justify-content: center;
 | 
			
		||||
		flex-grow: 1;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.attribution {
 | 
			
		||||
		text-align: right;
 | 
			
		||||
	}
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			@ -1,622 +0,0 @@
 | 
			
		|||
<script lang="ts">
 | 
			
		||||
	import { onMount } from 'svelte';
 | 
			
		||||
	import Spectrum from '$lib/audio/Spectrum.svelte';
 | 
			
		||||
	import Oscilloscope from '$lib/audio/Oscilloscope.svelte';
 | 
			
		||||
	import { m } from '$lib/paraglide/messages';
 | 
			
		||||
 | 
			
		||||
	// Mic / audio graph state
 | 
			
		||||
	let audioCtx: AudioContext | null = null;
 | 
			
		||||
	let mediaStream: MediaStream | null = null;
 | 
			
		||||
	let source: MediaStreamAudioSourceNode | null = null;
 | 
			
		||||
	let analyserTime: AnalyserNode | null = $state(null);
 | 
			
		||||
	let analyserFreq: AnalyserNode | null = $state(null);
 | 
			
		||||
	let monitorGain: GainNode | null = null;
 | 
			
		||||
	let delayNode: DelayNode | null = null;
 | 
			
		||||
 | 
			
		||||
	// UI state (Svelte 5 runes-style)
 | 
			
		||||
	let micActive = $state(false);
 | 
			
		||||
	let monitoring = $state(false);
 | 
			
		||||
	let gain = $state(1.0);
 | 
			
		||||
	let delayMs = $state(0);
 | 
			
		||||
	let sampleRate = $state<number | null>(null);
 | 
			
		||||
	let deviceLabel = $state<string>('');
 | 
			
		||||
 | 
			
		||||
	// Volume meter (RMS 0..1)
 | 
			
		||||
	let volume = $state(0);
 | 
			
		||||
	let rafId: number | null = null;
 | 
			
		||||
	// Peak metrics
 | 
			
		||||
	let peak = $state(0); // instantaneous peak 0..1
 | 
			
		||||
	let peakHold = $state(0); // hold 0..1
 | 
			
		||||
	let peakHoldDecayPerSec = 0.5; // how fast the hold marker decays
 | 
			
		||||
 | 
			
		||||
	// Recording
 | 
			
		||||
	let recorder: MediaRecorder | null = null;
 | 
			
		||||
	let isRecording = $state(false);
 | 
			
		||||
	let recordedChunks: BlobPart[] = [];
 | 
			
		||||
	let recordingUrl = $state<string | null>(null);
 | 
			
		||||
 | 
			
		||||
	// Devices
 | 
			
		||||
	let devices: MediaDeviceInfo[] = $state([]);
 | 
			
		||||
	let selectedDeviceId: string | null = $state(null);
 | 
			
		||||
 | 
			
		||||
	// Clipping indicator
 | 
			
		||||
	let clipping = $state(false);
 | 
			
		||||
	let lastClipTs = 0;
 | 
			
		||||
 | 
			
		||||
	// Track settings and constraints
 | 
			
		||||
	let channels = $state<number | null>(null);
 | 
			
		||||
	let obtainedEchoCancellation = $state<boolean | null>(null);
 | 
			
		||||
	let obtainedNoiseSuppression = $state<boolean | null>(null);
 | 
			
		||||
	let obtainedAGC = $state<boolean | null>(null);
 | 
			
		||||
 | 
			
		||||
	// Requested constraints (best-effort, tri-state: null=default, true, false)
 | 
			
		||||
	let reqEchoCancellation = $state<boolean | null>(null);
 | 
			
		||||
	let reqNoiseSuppression = $state<boolean | null>(null);
 | 
			
		||||
	let reqAGC = $state<boolean | null>(null);
 | 
			
		||||
 | 
			
		||||
	async function startMic() {
 | 
			
		||||
		if (micActive) return;
 | 
			
		||||
		try {
 | 
			
		||||
			const audioConstraints: MediaTrackConstraints = selectedDeviceId
 | 
			
		||||
				? { deviceId: { exact: selectedDeviceId } }
 | 
			
		||||
				: {};
 | 
			
		||||
			if (reqEchoCancellation !== null) audioConstraints.echoCancellation = reqEchoCancellation;
 | 
			
		||||
			if (reqNoiseSuppression !== null) audioConstraints.noiseSuppression = reqNoiseSuppression;
 | 
			
		||||
			if (reqAGC !== null) audioConstraints.autoGainControl = reqAGC;
 | 
			
		||||
 | 
			
		||||
			const constraints: MediaStreamConstraints = {
 | 
			
		||||
				audio: Object.keys(audioConstraints).length ? audioConstraints : true,
 | 
			
		||||
				video: false
 | 
			
		||||
			};
 | 
			
		||||
			mediaStream = await navigator.mediaDevices.getUserMedia(constraints);
 | 
			
		||||
			deviceLabel = mediaStream.getAudioTracks()[0]?.label ?? '';
 | 
			
		||||
 | 
			
		||||
			audioCtx = new AudioContext();
 | 
			
		||||
			sampleRate = audioCtx.sampleRate;
 | 
			
		||||
 | 
			
		||||
			source = audioCtx.createMediaStreamSource(mediaStream);
 | 
			
		||||
			analyserTime = audioCtx.createAnalyser();
 | 
			
		||||
			analyserFreq = audioCtx.createAnalyser();
 | 
			
		||||
			monitorGain = audioCtx.createGain();
 | 
			
		||||
			delayNode = audioCtx.createDelay(2.0); // up to 2 seconds
 | 
			
		||||
 | 
			
		||||
			// Default params
 | 
			
		||||
			analyserTime.fftSize = 2048;
 | 
			
		||||
			analyserTime.smoothingTimeConstant = 0.2;
 | 
			
		||||
			analyserFreq.fftSize = 2048;
 | 
			
		||||
			analyserFreq.smoothingTimeConstant = 0.8;
 | 
			
		||||
			monitorGain.gain.value = gain;
 | 
			
		||||
			delayNode.delayTime.value = delayMs / 1000;
 | 
			
		||||
 | 
			
		||||
			// Fan-out: source -> (analysers)
 | 
			
		||||
			source.connect(analyserTime);
 | 
			
		||||
			source.connect(analyserFreq);
 | 
			
		||||
 | 
			
		||||
			// Monitoring path (initially disconnected; toggleMonitoring() handles it)
 | 
			
		||||
			updateMonitoringGraph();
 | 
			
		||||
 | 
			
		||||
			// Start volume meter loop
 | 
			
		||||
			startVolumeLoop();
 | 
			
		||||
			micActive = true;
 | 
			
		||||
			// Update device list after permission to get labels
 | 
			
		||||
			refreshDevices();
 | 
			
		||||
 | 
			
		||||
			// Read obtained settings
 | 
			
		||||
			const track = mediaStream.getAudioTracks()[0];
 | 
			
		||||
			const s = track.getSettings?.() as MediaTrackSettings | undefined;
 | 
			
		||||
			channels = s?.channelCount ?? null;
 | 
			
		||||
			obtainedEchoCancellation = (s?.echoCancellation as boolean | undefined) ?? null;
 | 
			
		||||
			obtainedNoiseSuppression = (s?.noiseSuppression as boolean | undefined) ?? null;
 | 
			
		||||
			obtainedAGC = (s?.autoGainControl as boolean | undefined) ?? null;
 | 
			
		||||
		} catch (err) {
 | 
			
		||||
			console.error('Failed to start microphone', err);
 | 
			
		||||
			stopMic();
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	async function refreshDevices() {
 | 
			
		||||
		try {
 | 
			
		||||
			const list = await navigator.mediaDevices.enumerateDevices();
 | 
			
		||||
			devices = list.filter((d) => d.kind === 'audioinput');
 | 
			
		||||
			if (devices.length > 0 && !selectedDeviceId) {
 | 
			
		||||
				selectedDeviceId = devices[0].deviceId;
 | 
			
		||||
			}
 | 
			
		||||
		} catch (e) {
 | 
			
		||||
			console.error('enumerateDevices failed', e);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function onDeviceChange(e: Event) {
 | 
			
		||||
		selectedDeviceId = (e.target as HTMLSelectElement).value || null;
 | 
			
		||||
		if (micActive) {
 | 
			
		||||
			// Restart mic with the new device
 | 
			
		||||
			const wasMonitoring = monitoring;
 | 
			
		||||
			stopMic();
 | 
			
		||||
			monitoring = wasMonitoring; // preserve desired state; will reconnect on start
 | 
			
		||||
			startMic();
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function updateMonitoringGraph() {
 | 
			
		||||
		if (!audioCtx || !source || !monitorGain || !delayNode) return;
 | 
			
		||||
 | 
			
		||||
		try {
 | 
			
		||||
			// Always disconnect monitoring path before reconnecting to avoid duplicate connections
 | 
			
		||||
			monitorGain.disconnect();
 | 
			
		||||
			delayNode.disconnect();
 | 
			
		||||
		} catch {}
 | 
			
		||||
 | 
			
		||||
		if (monitoring) {
 | 
			
		||||
			// source -> monitorGain -> delayNode -> destination
 | 
			
		||||
			monitorGain.gain.value = gain;
 | 
			
		||||
			delayNode.delayTime.value = delayMs / 1000;
 | 
			
		||||
 | 
			
		||||
			source.connect(monitorGain);
 | 
			
		||||
			monitorGain.connect(delayNode);
 | 
			
		||||
			delayNode.connect(audioCtx.destination);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function startVolumeLoop() {
 | 
			
		||||
		cancelVolumeLoop();
 | 
			
		||||
		const buf = new Uint8Array(analyserTime?.fftSize ?? 2048);
 | 
			
		||||
		const loop = () => {
 | 
			
		||||
			if (analyserTime) {
 | 
			
		||||
				analyserTime.getByteTimeDomainData(buf);
 | 
			
		||||
				// Compute RMS
 | 
			
		||||
				let sum = 0;
 | 
			
		||||
				let pk = 0;
 | 
			
		||||
				for (let i = 0; i < buf.length; i++) {
 | 
			
		||||
					const v = (buf[i] - 128) / 128; // -1..1
 | 
			
		||||
					sum += v * v;
 | 
			
		||||
					const a = Math.abs(v);
 | 
			
		||||
					if (a > pk) pk = a;
 | 
			
		||||
				}
 | 
			
		||||
				const rms = Math.sqrt(sum / buf.length);
 | 
			
		||||
				volume = rms; // 0..1
 | 
			
		||||
				peak = pk;
 | 
			
		||||
				// Update hold
 | 
			
		||||
				const dt = 1 / 60; // approx
 | 
			
		||||
				peakHold = Math.max(pk, Math.max(0, peakHold - peakHoldDecayPerSec * dt));
 | 
			
		||||
				// Clipping detection (very near full-scale)
 | 
			
		||||
				const now = performance.now();
 | 
			
		||||
				if (pk >= 0.985) {
 | 
			
		||||
					clipping = true;
 | 
			
		||||
					lastClipTs = now;
 | 
			
		||||
				} else if (clipping && now - lastClipTs > 500) {
 | 
			
		||||
					// Auto clear after 500ms without clipping
 | 
			
		||||
					clipping = false;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			rafId = requestAnimationFrame(loop);
 | 
			
		||||
		};
 | 
			
		||||
		rafId = requestAnimationFrame(loop);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function cancelVolumeLoop() {
 | 
			
		||||
		if (rafId) cancelAnimationFrame(rafId);
 | 
			
		||||
		rafId = null;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function stopMic() {
 | 
			
		||||
		cancelVolumeLoop();
 | 
			
		||||
		micActive = false;
 | 
			
		||||
		monitoring = false;
 | 
			
		||||
 | 
			
		||||
		try {
 | 
			
		||||
			recorder?.state !== 'inactive' && recorder?.stop();
 | 
			
		||||
		} catch {}
 | 
			
		||||
		recorder = null;
 | 
			
		||||
		isRecording = false;
 | 
			
		||||
 | 
			
		||||
		try {
 | 
			
		||||
			source?.disconnect();
 | 
			
		||||
		} catch {}
 | 
			
		||||
		try {
 | 
			
		||||
			monitorGain?.disconnect();
 | 
			
		||||
		} catch {}
 | 
			
		||||
		try {
 | 
			
		||||
			delayNode?.disconnect();
 | 
			
		||||
		} catch {}
 | 
			
		||||
 | 
			
		||||
		analyserTime = null;
 | 
			
		||||
		analyserFreq = null;
 | 
			
		||||
		monitorGain = null;
 | 
			
		||||
		delayNode = null;
 | 
			
		||||
		source = null;
 | 
			
		||||
 | 
			
		||||
		if (audioCtx) {
 | 
			
		||||
			audioCtx.close().catch(() => {});
 | 
			
		||||
			audioCtx = null;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (mediaStream) {
 | 
			
		||||
			for (const t of mediaStream.getTracks()) t.stop();
 | 
			
		||||
			mediaStream = null;
 | 
			
		||||
		}
 | 
			
		||||
		peak = 0;
 | 
			
		||||
		peakHold = 0;
 | 
			
		||||
		channels = null;
 | 
			
		||||
		obtainedEchoCancellation = obtainedNoiseSuppression = obtainedAGC = null;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function toggleMonitoring() {
 | 
			
		||||
		monitoring = !monitoring;
 | 
			
		||||
		updateMonitoringGraph();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function onGainChange(e: Event) {
 | 
			
		||||
		gain = Number((e.target as HTMLInputElement).value);
 | 
			
		||||
		if (monitorGain) monitorGain.gain.value = gain;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function onDelayChange(e: Event) {
 | 
			
		||||
		delayMs = Number((e.target as HTMLInputElement).value);
 | 
			
		||||
		if (delayNode) delayNode.delayTime.value = delayMs / 1000;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Recording via MediaRecorder on raw MediaStream
 | 
			
		||||
	function startRecording() {
 | 
			
		||||
		if (!mediaStream || isRecording) return;
 | 
			
		||||
		try {
 | 
			
		||||
			// If there's an old blob URL, release it
 | 
			
		||||
			if (recordingUrl) {
 | 
			
		||||
				URL.revokeObjectURL(recordingUrl);
 | 
			
		||||
				recordingUrl = null;
 | 
			
		||||
			}
 | 
			
		||||
			recordedChunks = [];
 | 
			
		||||
			recorder = new MediaRecorder(mediaStream);
 | 
			
		||||
			recorder.ondataavailable = (ev) => {
 | 
			
		||||
				if (ev.data && ev.data.size > 0) recordedChunks.push(ev.data);
 | 
			
		||||
			};
 | 
			
		||||
			recorder.onstop = () => {
 | 
			
		||||
				const blob = new Blob(recordedChunks, { type: recorder?.mimeType || 'audio/webm' });
 | 
			
		||||
				recordingUrl = URL.createObjectURL(blob);
 | 
			
		||||
			};
 | 
			
		||||
			recorder.start();
 | 
			
		||||
			isRecording = true;
 | 
			
		||||
		} catch (e) {
 | 
			
		||||
			console.error('Unable to start recording', e);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function stopRecording() {
 | 
			
		||||
		if (!recorder || recorder.state === 'inactive') return;
 | 
			
		||||
		recorder.stop();
 | 
			
		||||
		isRecording = false;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	onMount(() => {
 | 
			
		||||
		// Try to prefetch devices (may require prior permission for labels)
 | 
			
		||||
		refreshDevices();
 | 
			
		||||
		return () => {
 | 
			
		||||
			stopMic();
 | 
			
		||||
			if (recordingUrl) URL.revokeObjectURL(recordingUrl);
 | 
			
		||||
		};
 | 
			
		||||
	});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<article>
 | 
			
		||||
	<h3>{m.mic_title()}</h3>
 | 
			
		||||
 | 
			
		||||
	<section class="controls">
 | 
			
		||||
		<div class="row">
 | 
			
		||||
			<button class="button" onclick={startMic} disabled={micActive}
 | 
			
		||||
				>{m.mic_startMicrophone()}</button
 | 
			
		||||
			>
 | 
			
		||||
			<button class="button stop" onclick={stopMic} disabled={!micActive}>{m.mic_stop()}</button>
 | 
			
		||||
			<button
 | 
			
		||||
				class="button"
 | 
			
		||||
				onclick={toggleMonitoring}
 | 
			
		||||
				disabled={!micActive}
 | 
			
		||||
				aria-pressed={monitoring}
 | 
			
		||||
			>
 | 
			
		||||
				{monitoring ? m.mic_monitoringOn() : m.mic_monitoringOff()}
 | 
			
		||||
			</button>
 | 
			
		||||
		</div>
 | 
			
		||||
 | 
			
		||||
		<div class="row inputs">
 | 
			
		||||
			<label>
 | 
			
		||||
				{m.mic_device()}
 | 
			
		||||
				<select onchange={onDeviceChange} disabled={devices.length === 0}>
 | 
			
		||||
					{#if devices.length === 0}
 | 
			
		||||
						<option value="">{m.mic_noMicFound()}</option>
 | 
			
		||||
					{:else}
 | 
			
		||||
						{#each devices as d}
 | 
			
		||||
							<option value={d.deviceId} selected={d.deviceId === selectedDeviceId}
 | 
			
		||||
								>{d.label || m.mic_device()}</option
 | 
			
		||||
							>
 | 
			
		||||
						{/each}
 | 
			
		||||
					{/if}
 | 
			
		||||
				</select>
 | 
			
		||||
			</label>
 | 
			
		||||
			<button class="button" onclick={refreshDevices}>{m.mic_refresh()}</button>
 | 
			
		||||
		</div>
 | 
			
		||||
 | 
			
		||||
		<div class="row inputs">
 | 
			
		||||
			<label>
 | 
			
		||||
				{m.mic_gain()}
 | 
			
		||||
				<input
 | 
			
		||||
					type="range"
 | 
			
		||||
					min="0"
 | 
			
		||||
					max="2"
 | 
			
		||||
					step="0.01"
 | 
			
		||||
					value={gain}
 | 
			
		||||
					oninput={onGainChange}
 | 
			
		||||
					disabled={!micActive}
 | 
			
		||||
				/>
 | 
			
		||||
				<span class="value">{gain.toFixed(2)}x</span>
 | 
			
		||||
			</label>
 | 
			
		||||
			<label>
 | 
			
		||||
				{m.mic_monitorDelay()}
 | 
			
		||||
				<input
 | 
			
		||||
					type="range"
 | 
			
		||||
					min="0"
 | 
			
		||||
					max="1000"
 | 
			
		||||
					step="1"
 | 
			
		||||
					value={delayMs}
 | 
			
		||||
					oninput={onDelayChange}
 | 
			
		||||
					disabled={!micActive}
 | 
			
		||||
				/>
 | 
			
		||||
				<span class="value">{delayMs} ms</span>
 | 
			
		||||
			</label>
 | 
			
		||||
		</div>
 | 
			
		||||
 | 
			
		||||
		<details>
 | 
			
		||||
			<summary>{m.mic_advanced()}</summary>
 | 
			
		||||
			<div class="row inputs">
 | 
			
		||||
				<div class="label">{m.mic_constraints()}</div>
 | 
			
		||||
				<label>
 | 
			
		||||
					{m.mic_echoCancellation()}
 | 
			
		||||
					<select
 | 
			
		||||
						onchange={(e) => {
 | 
			
		||||
							const v = (e.target as HTMLSelectElement).value;
 | 
			
		||||
							reqEchoCancellation = v === 'default' ? null : v === 'on';
 | 
			
		||||
						}}
 | 
			
		||||
					>
 | 
			
		||||
						<option value="default" selected>{m.mic_default()}</option>
 | 
			
		||||
						<option value="on">{m.mic_on()}</option>
 | 
			
		||||
						<option value="off">{m.mic_off()}</option>
 | 
			
		||||
					</select>
 | 
			
		||||
				</label>
 | 
			
		||||
				<label>
 | 
			
		||||
					{m.mic_noiseSuppression()}
 | 
			
		||||
					<select
 | 
			
		||||
						onchange={(e) => {
 | 
			
		||||
							const v = (e.target as HTMLSelectElement).value;
 | 
			
		||||
							reqNoiseSuppression = v === 'default' ? null : v === 'on';
 | 
			
		||||
						}}
 | 
			
		||||
					>
 | 
			
		||||
						<option value="default" selected>{m.mic_default()}</option>
 | 
			
		||||
						<option value="on">{m.mic_on()}</option>
 | 
			
		||||
						<option value="off">{m.mic_off()}</option>
 | 
			
		||||
					</select>
 | 
			
		||||
				</label>
 | 
			
		||||
				<label>
 | 
			
		||||
					{m.mic_agc()}
 | 
			
		||||
					<select
 | 
			
		||||
						onchange={(e) => {
 | 
			
		||||
							const v = (e.target as HTMLSelectElement).value;
 | 
			
		||||
							reqAGC = v === 'default' ? null : v === 'on';
 | 
			
		||||
						}}
 | 
			
		||||
					>
 | 
			
		||||
						<option value="default" selected>{m.mic_default()}</option>
 | 
			
		||||
						<option value="on">{m.mic_on()}</option>
 | 
			
		||||
						<option value="off">{m.mic_off()}</option>
 | 
			
		||||
					</select>
 | 
			
		||||
				</label>
 | 
			
		||||
				<button
 | 
			
		||||
					class="button"
 | 
			
		||||
					onclick={() => {
 | 
			
		||||
						if (micActive) {
 | 
			
		||||
							const wasMonitoring = monitoring;
 | 
			
		||||
							stopMic();
 | 
			
		||||
							monitoring = wasMonitoring;
 | 
			
		||||
						}
 | 
			
		||||
						startMic();
 | 
			
		||||
					}}>{m.mic_applyConstraints()}</button
 | 
			
		||||
				>
 | 
			
		||||
			</div>
 | 
			
		||||
 | 
			
		||||
			<div class="obtained">
 | 
			
		||||
				<div>
 | 
			
		||||
					<strong>{m.mic_requested()}:</strong> EC={reqEchoCancellation === null
 | 
			
		||||
						? '–'
 | 
			
		||||
						: reqEchoCancellation
 | 
			
		||||
							? 'on'
 | 
			
		||||
							: 'off'}, NS={reqNoiseSuppression === null
 | 
			
		||||
						? '–'
 | 
			
		||||
						: reqNoiseSuppression
 | 
			
		||||
							? 'on'
 | 
			
		||||
							: 'off'}, AGC={reqAGC === null ? '–' : reqAGC ? 'on' : 'off'}
 | 
			
		||||
				</div>
 | 
			
		||||
				<div>
 | 
			
		||||
					<strong>{m.mic_obtained()}:</strong> EC={(obtainedEchoCancellation ??
 | 
			
		||||
						'–') as unknown as string}, NS={(obtainedNoiseSuppression ?? '–') as unknown as string},
 | 
			
		||||
					AGC={(obtainedAGC ?? '–') as unknown as string}
 | 
			
		||||
				</div>
 | 
			
		||||
			</div>
 | 
			
		||||
		</details>
 | 
			
		||||
	</section>
 | 
			
		||||
 | 
			
		||||
	<section class="info">
 | 
			
		||||
		<div><strong>{m.mic_sampleRate()}:</strong> {sampleRate ?? '–'} Hz</div>
 | 
			
		||||
		{#if deviceLabel}
 | 
			
		||||
			<div><strong>{m.mic_inputDevice()}:</strong> {deviceLabel}</div>
 | 
			
		||||
		{/if}
 | 
			
		||||
		<div>
 | 
			
		||||
			<strong>{m.mic_channels()}:</strong>
 | 
			
		||||
			{#if channels === null}
 | 
			
		||||
				–
 | 
			
		||||
			{:else if channels === 1}
 | 
			
		||||
				1 ({m.mic_mono()})
 | 
			
		||||
			{:else if channels === 2}
 | 
			
		||||
				2 ({m.mic_stereo()})
 | 
			
		||||
			{:else}
 | 
			
		||||
				{channels}
 | 
			
		||||
			{/if}
 | 
			
		||||
		</div>
 | 
			
		||||
	</section>
 | 
			
		||||
 | 
			
		||||
	<section class="meter">
 | 
			
		||||
		<div class="label">{m.mic_volume()}</div>
 | 
			
		||||
		<div class="bar">
 | 
			
		||||
			<div class="fill" style={`transform: scaleX(${Math.min(1, volume).toFixed(3)})`}></div>
 | 
			
		||||
			{#if peakHold > 0}
 | 
			
		||||
				<div class="peak-hold" style={`left: ${Math.min(100, peakHold * 100)}%`}></div>
 | 
			
		||||
			{/if}
 | 
			
		||||
		</div>
 | 
			
		||||
		<div class="volval">
 | 
			
		||||
			{m.mic_peakNow()}: {(20 * Math.log10(Math.max(1e-5, peak))).toFixed(1)} dBFS ·
 | 
			
		||||
			{m.mic_peakHold()}: {(20 * Math.log10(Math.max(1e-5, peakHold))).toFixed(1)} dBFS · RMS: {(
 | 
			
		||||
				20 * Math.log10(Math.max(1e-5, volume))
 | 
			
		||||
			).toFixed(1)} dBFS
 | 
			
		||||
			<button class="button small" onclick={() => (peakHold = 0)}>{m.mic_resetPeaks()}</button>
 | 
			
		||||
		</div>
 | 
			
		||||
		{#if clipping}
 | 
			
		||||
			<div class="clip">{m.mic_clipping()}</div>
 | 
			
		||||
		{/if}
 | 
			
		||||
	</section>
 | 
			
		||||
 | 
			
		||||
	<section class="graphs">
 | 
			
		||||
		{#if analyserTime && analyserFreq}
 | 
			
		||||
			<Oscilloscope analyser={analyserTime} title={m.signalGen_scope()} />
 | 
			
		||||
			<Spectrum analyser={analyserFreq} title={m.signalGen_spectrum()} />
 | 
			
		||||
		{/if}
 | 
			
		||||
	</section>
 | 
			
		||||
 | 
			
		||||
	<section class="recording">
 | 
			
		||||
		<h4>{m.mic_recording()}</h4>
 | 
			
		||||
		<div class="row">
 | 
			
		||||
			<button class="button" onclick={startRecording} disabled={!micActive || isRecording}
 | 
			
		||||
				>{m.mic_startRecording()}</button
 | 
			
		||||
			>
 | 
			
		||||
			<button class="button stop" onclick={stopRecording} disabled={!isRecording}
 | 
			
		||||
				>{m.mic_stopRecording()}</button
 | 
			
		||||
			>
 | 
			
		||||
		</div>
 | 
			
		||||
		{#if recordingUrl}
 | 
			
		||||
			<audio src={recordingUrl} controls></audio>
 | 
			
		||||
			<a class="button" href={recordingUrl} download="mic-test.webm">{m.mic_downloadRecording()}</a>
 | 
			
		||||
		{/if}
 | 
			
		||||
	</section>
 | 
			
		||||
</article>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
	h3 {
 | 
			
		||||
		margin-top: 0;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	article {
 | 
			
		||||
		display: grid;
 | 
			
		||||
		grid-template-columns: 1fr;
 | 
			
		||||
		gap: 1rem;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.controls .row {
 | 
			
		||||
		display: flex;
 | 
			
		||||
		flex-wrap: wrap;
 | 
			
		||||
		gap: 0.5rem;
 | 
			
		||||
		align-items: center;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.controls .inputs {
 | 
			
		||||
		gap: 1.25rem;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.controls label {
 | 
			
		||||
		display: inline-flex;
 | 
			
		||||
		align-items: center;
 | 
			
		||||
		gap: 0.5rem;
 | 
			
		||||
		white-space: nowrap;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.controls input[type='range'] {
 | 
			
		||||
		width: min(40vw, 280px);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.value {
 | 
			
		||||
		opacity: 0.8;
 | 
			
		||||
		min-width: 4ch;
 | 
			
		||||
		text-align: right;
 | 
			
		||||
		display: inline-block;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.info {
 | 
			
		||||
		display: flex;
 | 
			
		||||
		gap: 1rem;
 | 
			
		||||
		flex-wrap: wrap;
 | 
			
		||||
		align-items: center;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.meter {
 | 
			
		||||
		display: grid;
 | 
			
		||||
		gap: 0.25rem;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.bar {
 | 
			
		||||
		position: relative;
 | 
			
		||||
		height: 14px;
 | 
			
		||||
		background: rgba(255, 255, 255, 0.08);
 | 
			
		||||
		border: 1px solid rgba(255, 255, 255, 0.2);
 | 
			
		||||
		border-radius: 999px;
 | 
			
		||||
		overflow: hidden;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.bar .fill {
 | 
			
		||||
		position: absolute;
 | 
			
		||||
		inset: 0;
 | 
			
		||||
		background: linear-gradient(90deg, #5cb85c, #f0ad4e 70%, #d9534f);
 | 
			
		||||
		transform-origin: left center;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/* Peak hold marker */
 | 
			
		||||
	.bar .peak-hold {
 | 
			
		||||
		position: absolute;
 | 
			
		||||
		top: 0;
 | 
			
		||||
		bottom: 0;
 | 
			
		||||
		width: 2px;
 | 
			
		||||
		background: #fff;
 | 
			
		||||
		opacity: 0.9;
 | 
			
		||||
		transform: translateX(-1px);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.volval {
 | 
			
		||||
		opacity: 0.8;
 | 
			
		||||
		font-variant-numeric: tabular-nums;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.button.small {
 | 
			
		||||
		padding: 0.1rem 0.4rem;
 | 
			
		||||
		font-size: 0.85em;
 | 
			
		||||
		margin-left: 0.5rem;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.graphs {
 | 
			
		||||
		display: grid;
 | 
			
		||||
		grid-template-columns: 1fr;
 | 
			
		||||
		gap: 1rem;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.recording .row {
 | 
			
		||||
		display: flex;
 | 
			
		||||
		gap: 0.5rem;
 | 
			
		||||
		align-items: center;
 | 
			
		||||
		margin-bottom: 0.5rem;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.obtained {
 | 
			
		||||
		display: grid;
 | 
			
		||||
		gap: 0.25rem;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.button.stop:not(:disabled) {
 | 
			
		||||
		background: darkred;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@media (min-width: 900px) {
 | 
			
		||||
		.graphs {
 | 
			
		||||
			grid-template-columns: 1fr 1fr;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			@ -1,545 +0,0 @@
 | 
			
		|||
<script lang="ts">
 | 
			
		||||
	import { onDestroy, onMount } from 'svelte';
 | 
			
		||||
	import { browser } from '$app/environment';
 | 
			
		||||
	import { m } from '$lib/paraglide/messages';
 | 
			
		||||
 | 
			
		||||
	// Geolocation
 | 
			
		||||
	let geoWatchId: number | null = $state(null);
 | 
			
		||||
	let geolocation: GeolocationPosition | null = $state(null);
 | 
			
		||||
	let geoError: string | null = $state(null);
 | 
			
		||||
	let geoSupported = $state<boolean>(false);
 | 
			
		||||
 | 
			
		||||
	// DeviceMotion / DeviceOrientation (useful fallbacks on iOS/Safari)
 | 
			
		||||
	type Motion = { ax?: number; ay?: number; az?: number; gx?: number; gy?: number; gz?: number };
 | 
			
		||||
	let deviceMotion: Motion | null = $state(null);
 | 
			
		||||
	let deviceOrientation: { alpha?: number; beta?: number; gamma?: number } | null = $state(null);
 | 
			
		||||
	let motionSupported = $state(false);
 | 
			
		||||
	let orientationSupported = $state(false);
 | 
			
		||||
 | 
			
		||||
	// iOS Safari permission flow for motion/orientation
 | 
			
		||||
	let motionPermissionAvailable = $state(false);
 | 
			
		||||
	let orientationPermissionAvailable = $state(false);
 | 
			
		||||
	let motionPermission: 'unknown' | 'granted' | 'denied' = $state('unknown');
 | 
			
		||||
	let orientationPermission: 'unknown' | 'granted' | 'denied' = $state('unknown');
 | 
			
		||||
 | 
			
		||||
	// Generic Sensor API (subject to browser/flag support)
 | 
			
		||||
	type SensorHandle = {
 | 
			
		||||
		instance?: any;
 | 
			
		||||
		supported: boolean;
 | 
			
		||||
		active: boolean;
 | 
			
		||||
		error?: string | null;
 | 
			
		||||
		data: Record<string, number | string | undefined>;
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	let accelerometer: SensorHandle = $state({ supported: false, active: false, data: {} });
 | 
			
		||||
	let gyroscope: SensorHandle = $state({ supported: false, active: false, data: {} });
 | 
			
		||||
	let magnetometer: SensorHandle = $state({ supported: false, active: false, data: {} });
 | 
			
		||||
	let ambientLight: SensorHandle = $state({ supported: false, active: false, data: {} });
 | 
			
		||||
	let barometer: SensorHandle = $state({ supported: false, active: false, data: {} });
 | 
			
		||||
 | 
			
		||||
	const w = browser ? (window as any) : undefined;
 | 
			
		||||
 | 
			
		||||
	function detectSupport() {
 | 
			
		||||
		geoSupported = browser && 'geolocation' in navigator;
 | 
			
		||||
		motionSupported = browser && 'DeviceMotionEvent' in (window as any);
 | 
			
		||||
		orientationSupported = browser && 'DeviceOrientationEvent' in (window as any);
 | 
			
		||||
 | 
			
		||||
		accelerometer.supported = Boolean(w?.Accelerometer);
 | 
			
		||||
		gyroscope.supported = Boolean(w?.Gyroscope);
 | 
			
		||||
		magnetometer.supported = Boolean(w?.Magnetometer);
 | 
			
		||||
		ambientLight.supported = Boolean(w?.AmbientLightSensor);
 | 
			
		||||
		barometer.supported = Boolean(w?.Barometer);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	onMount(() => {
 | 
			
		||||
		detectSupport();
 | 
			
		||||
 | 
			
		||||
		// Check for iOS-style permission request APIs
 | 
			
		||||
		motionPermissionAvailable =
 | 
			
		||||
			browser &&
 | 
			
		||||
			typeof (window as any).DeviceMotionEvent !== 'undefined' &&
 | 
			
		||||
			typeof (DeviceMotionEvent as any).requestPermission === 'function';
 | 
			
		||||
		orientationPermissionAvailable =
 | 
			
		||||
			browser &&
 | 
			
		||||
			typeof (window as any).DeviceOrientationEvent !== 'undefined' &&
 | 
			
		||||
			typeof (DeviceOrientationEvent as any).requestPermission === 'function';
 | 
			
		||||
 | 
			
		||||
		if (orientationSupported) {
 | 
			
		||||
			const handler = (e: DeviceOrientationEvent) => {
 | 
			
		||||
				deviceOrientation = {
 | 
			
		||||
					alpha: e.alpha ?? undefined,
 | 
			
		||||
					beta: e.beta ?? undefined,
 | 
			
		||||
					gamma: e.gamma ?? undefined
 | 
			
		||||
				};
 | 
			
		||||
			};
 | 
			
		||||
			window.addEventListener('deviceorientation', handler);
 | 
			
		||||
			onDestroy(() => window.removeEventListener('deviceorientation', handler));
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (motionSupported) {
 | 
			
		||||
			const handler = (e: DeviceMotionEvent) => {
 | 
			
		||||
				deviceMotion = {
 | 
			
		||||
					ax: e.acceleration?.x ?? undefined,
 | 
			
		||||
					ay: e.acceleration?.y ?? undefined,
 | 
			
		||||
					az: e.acceleration?.z ?? undefined,
 | 
			
		||||
					gx: e.rotationRate?.alpha ?? undefined,
 | 
			
		||||
					gy: e.rotationRate?.beta ?? undefined,
 | 
			
		||||
					gz: e.rotationRate?.gamma ?? undefined
 | 
			
		||||
				};
 | 
			
		||||
			};
 | 
			
		||||
			window.addEventListener('devicemotion', handler);
 | 
			
		||||
			onDestroy(() => window.removeEventListener('devicemotion', handler));
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	onDestroy(() => {
 | 
			
		||||
		stopGeolocation();
 | 
			
		||||
		stopSensor(accelerometer);
 | 
			
		||||
		stopSensor(gyroscope);
 | 
			
		||||
		stopSensor(magnetometer);
 | 
			
		||||
		stopSensor(ambientLight);
 | 
			
		||||
		stopSensor(barometer);
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	// (Permissions are requested implicitly when starting sensors where applicable)
 | 
			
		||||
 | 
			
		||||
	// Geolocation controls
 | 
			
		||||
	async function startGeolocation() {
 | 
			
		||||
		if (!geoSupported) return;
 | 
			
		||||
		try {
 | 
			
		||||
			geoError = null;
 | 
			
		||||
			geolocation = await new Promise<GeolocationPosition>((resolve, reject) => {
 | 
			
		||||
				navigator.geolocation.getCurrentPosition(resolve, reject, {
 | 
			
		||||
					enableHighAccuracy: true,
 | 
			
		||||
					timeout: 10000
 | 
			
		||||
				});
 | 
			
		||||
			});
 | 
			
		||||
			geoWatchId = navigator.geolocation.watchPosition(
 | 
			
		||||
				(pos) => (geolocation = pos),
 | 
			
		||||
				(err) => (geoError = err?.message || String(err)),
 | 
			
		||||
				{ enableHighAccuracy: true }
 | 
			
		||||
			);
 | 
			
		||||
		} catch (e: any) {
 | 
			
		||||
			geoError = e?.message ?? String(e);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function stopGeolocation() {
 | 
			
		||||
		if (geoWatchId != null) {
 | 
			
		||||
			navigator.geolocation.clearWatch(geoWatchId);
 | 
			
		||||
			geoWatchId = null;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Generic Sensor helpers
 | 
			
		||||
	function startSensor(handle: SensorHandle, ctorName: string, options: any = { frequency: 60 }) {
 | 
			
		||||
		try {
 | 
			
		||||
			handle.error = null;
 | 
			
		||||
			if (!w?.[ctorName]) {
 | 
			
		||||
				handle.supported = false;
 | 
			
		||||
				return;
 | 
			
		||||
			}
 | 
			
		||||
			handle.instance = new w[ctorName](options);
 | 
			
		||||
			handle.instance.addEventListener('reading', () => {
 | 
			
		||||
				// Populate based on sensor type
 | 
			
		||||
				if (ctorName === 'Accelerometer') {
 | 
			
		||||
					handle.data = { x: handle.instance.x, y: handle.instance.y, z: handle.instance.z };
 | 
			
		||||
				} else if (ctorName === 'Gyroscope') {
 | 
			
		||||
					handle.data = { x: handle.instance.x, y: handle.instance.y, z: handle.instance.z };
 | 
			
		||||
				} else if (ctorName === 'Magnetometer') {
 | 
			
		||||
					handle.data = { x: handle.instance.x, y: handle.instance.y, z: handle.instance.z };
 | 
			
		||||
				} else if (ctorName === 'AmbientLightSensor') {
 | 
			
		||||
					handle.data = { illuminance: handle.instance.illuminance };
 | 
			
		||||
				} else if (ctorName === 'Barometer') {
 | 
			
		||||
					handle.data = {
 | 
			
		||||
						pressure: handle.instance.pressure,
 | 
			
		||||
						temperature: handle.instance?.temperature
 | 
			
		||||
					};
 | 
			
		||||
				}
 | 
			
		||||
			});
 | 
			
		||||
			handle.instance.addEventListener('error', (event: any) => {
 | 
			
		||||
				handle.error = event?.error?.message || String(event);
 | 
			
		||||
			});
 | 
			
		||||
			handle.instance.start();
 | 
			
		||||
			handle.active = true;
 | 
			
		||||
		} catch (e: any) {
 | 
			
		||||
			handle.error = e?.message ?? String(e);
 | 
			
		||||
			handle.active = false;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function stopSensor(handle: SensorHandle) {
 | 
			
		||||
		try {
 | 
			
		||||
			handle.instance?.stop?.();
 | 
			
		||||
		} catch {}
 | 
			
		||||
		handle.active = false;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// UI helpers
 | 
			
		||||
	function toFixed(n: number | undefined, digits = 2) {
 | 
			
		||||
		return typeof n === 'number' && Number.isFinite(n) ? n.toFixed(digits) : '—';
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	async function copyJSON(data: unknown) {
 | 
			
		||||
		try {
 | 
			
		||||
			await navigator.clipboard.writeText(JSON.stringify(data, null, 2));
 | 
			
		||||
			alert(m.sensors_copied());
 | 
			
		||||
		} catch {}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	async function requestMotionOrientation() {
 | 
			
		||||
		try {
 | 
			
		||||
			if (motionPermissionAvailable) {
 | 
			
		||||
				const res = await (DeviceMotionEvent as any).requestPermission();
 | 
			
		||||
				motionPermission = res === 'granted' ? 'granted' : 'denied';
 | 
			
		||||
			}
 | 
			
		||||
			if (orientationPermissionAvailable) {
 | 
			
		||||
				const res2 = await (DeviceOrientationEvent as any).requestPermission();
 | 
			
		||||
				orientationPermission = res2 === 'granted' ? 'granted' : 'denied';
 | 
			
		||||
			}
 | 
			
		||||
		} catch (_) {
 | 
			
		||||
			if (motionPermissionAvailable && motionPermission === 'unknown') motionPermission = 'denied';
 | 
			
		||||
			if (orientationPermissionAvailable && orientationPermission === 'unknown')
 | 
			
		||||
				orientationPermission = 'denied';
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Kick off light permission checks lazily in UI; starting sensors will request permissions where needed.
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<h2><i class="ti ti-cpu-2"></i> {m.sensors_title()}</h2>
 | 
			
		||||
 | 
			
		||||
<div class="sections">
 | 
			
		||||
	{#if motionPermissionAvailable || orientationPermissionAvailable}
 | 
			
		||||
		<section>
 | 
			
		||||
			<h3>{m.sensors_permissions()}</h3>
 | 
			
		||||
			<div class="row">
 | 
			
		||||
				<button onclick={requestMotionOrientation}
 | 
			
		||||
					><i class="ti ti-key"></i> {m.sensors_enableMotionOrientation()}</button
 | 
			
		||||
				>
 | 
			
		||||
			</div>
 | 
			
		||||
			<ul class="kv">
 | 
			
		||||
				{#if motionPermissionAvailable}
 | 
			
		||||
					<li>
 | 
			
		||||
						<span class="key">{m.sensors_motion()}</span>
 | 
			
		||||
						<span>
 | 
			
		||||
							{#if motionPermission === 'granted'}{m.sensors_status_granted()}
 | 
			
		||||
							{:else if motionPermission === 'denied'}{m.sensors_status_denied()}
 | 
			
		||||
							{:else}{m.sensors_status_unknown()}{/if}
 | 
			
		||||
						</span>
 | 
			
		||||
					</li>
 | 
			
		||||
				{/if}
 | 
			
		||||
				{#if orientationPermissionAvailable}
 | 
			
		||||
					<li>
 | 
			
		||||
						<span class="key">{m.sensors_orientation()}</span>
 | 
			
		||||
						<span>
 | 
			
		||||
							{#if orientationPermission === 'granted'}{m.sensors_status_granted()}
 | 
			
		||||
							{:else if orientationPermission === 'denied'}{m.sensors_status_denied()}
 | 
			
		||||
							{:else}{m.sensors_status_unknown()}{/if}
 | 
			
		||||
						</span>
 | 
			
		||||
					</li>
 | 
			
		||||
				{/if}
 | 
			
		||||
			</ul>
 | 
			
		||||
		</section>
 | 
			
		||||
	{/if}
 | 
			
		||||
	<section>
 | 
			
		||||
		<h3>{m.sensors_geolocation()}</h3>
 | 
			
		||||
		{#if geoSupported}
 | 
			
		||||
			<div class="row">
 | 
			
		||||
				<button onclick={startGeolocation} disabled={geoWatchId !== null}>
 | 
			
		||||
					<i class="ti ti-player-play"></i>
 | 
			
		||||
					{m.sensors_start()}
 | 
			
		||||
				</button>
 | 
			
		||||
				<button onclick={stopGeolocation} disabled={geoWatchId === null}>
 | 
			
		||||
					<i class="ti ti-player-stop"></i>
 | 
			
		||||
					{m.sensors_stop()}
 | 
			
		||||
				</button>
 | 
			
		||||
			</div>
 | 
			
		||||
			{#if geoError}
 | 
			
		||||
				<div class="error">{geoError}</div>
 | 
			
		||||
			{/if}
 | 
			
		||||
			{#if geolocation}
 | 
			
		||||
				<ul class="kv">
 | 
			
		||||
					<li><span class="key">lat</span><span>{geolocation.coords.latitude}</span></li>
 | 
			
		||||
					<li><span class="key">lon</span><span>{geolocation.coords.longitude}</span></li>
 | 
			
		||||
					<li>
 | 
			
		||||
						<span class="key">{m.sensors_accuracy()}</span><span
 | 
			
		||||
							>{toFixed(geolocation.coords.accuracy)}</span
 | 
			
		||||
						>
 | 
			
		||||
					</li>
 | 
			
		||||
					<li>
 | 
			
		||||
						<span class="key">{m.sensors_altitude()}</span><span
 | 
			
		||||
							>{geolocation.coords.altitude ?? '—'}</span
 | 
			
		||||
						>
 | 
			
		||||
					</li>
 | 
			
		||||
					<li>
 | 
			
		||||
						<span class="key">{m.sensors_heading()}</span><span
 | 
			
		||||
							>{toFixed(geolocation.coords.heading ?? undefined)}</span
 | 
			
		||||
						>
 | 
			
		||||
					</li>
 | 
			
		||||
					<li>
 | 
			
		||||
						<span class="key">{m.sensors_speed()}</span><span
 | 
			
		||||
							>{toFixed(geolocation.coords.speed ?? undefined)}</span
 | 
			
		||||
						>
 | 
			
		||||
					</li>
 | 
			
		||||
					<li>
 | 
			
		||||
						<span class="key">{m.sensors_timestamp()}</span><span
 | 
			
		||||
							>{new Date(geolocation.timestamp).toLocaleString()}</span
 | 
			
		||||
						>
 | 
			
		||||
					</li>
 | 
			
		||||
				</ul>
 | 
			
		||||
				<div class="row">
 | 
			
		||||
					<button onclick={() => copyJSON(geolocation)}
 | 
			
		||||
						><i class="ti ti-copy"></i> {m.sensors_copy()}</button
 | 
			
		||||
					>
 | 
			
		||||
				</div>
 | 
			
		||||
			{/if}
 | 
			
		||||
		{:else}
 | 
			
		||||
			<div class="subdued">{m.sensors_notSupported()}</div>
 | 
			
		||||
		{/if}
 | 
			
		||||
	</section>
 | 
			
		||||
 | 
			
		||||
	<section>
 | 
			
		||||
		<h3>{m.sensors_deviceMotion()}</h3>
 | 
			
		||||
		{#if motionSupported}
 | 
			
		||||
			<ul class="kv">
 | 
			
		||||
				<li><span class="key">ax</span><span>{toFixed(deviceMotion?.ax)}</span></li>
 | 
			
		||||
				<li><span class="key">ay</span><span>{toFixed(deviceMotion?.ay)}</span></li>
 | 
			
		||||
				<li><span class="key">az</span><span>{toFixed(deviceMotion?.az)}</span></li>
 | 
			
		||||
				<li><span class="key">α</span><span>{toFixed(deviceMotion?.gx)}</span></li>
 | 
			
		||||
				<li><span class="key">β</span><span>{toFixed(deviceMotion?.gy)}</span></li>
 | 
			
		||||
				<li><span class="key">γ</span><span>{toFixed(deviceMotion?.gz)}</span></li>
 | 
			
		||||
			</ul>
 | 
			
		||||
			<div class="row">
 | 
			
		||||
				<button onclick={() => copyJSON(deviceMotion)}
 | 
			
		||||
					><i class="ti ti-copy"></i> {m.sensors_copy()}</button
 | 
			
		||||
				>
 | 
			
		||||
			</div>
 | 
			
		||||
		{:else}
 | 
			
		||||
			<div class="subdued">{m.sensors_notSupported()}</div>
 | 
			
		||||
		{/if}
 | 
			
		||||
	</section>
 | 
			
		||||
 | 
			
		||||
	<section>
 | 
			
		||||
		<h3>{m.sensors_deviceOrientation()}</h3>
 | 
			
		||||
		{#if orientationSupported}
 | 
			
		||||
			<ul class="kv">
 | 
			
		||||
				<li><span class="key">alpha</span><span>{toFixed(deviceOrientation?.alpha)}</span></li>
 | 
			
		||||
				<li><span class="key">beta</span><span>{toFixed(deviceOrientation?.beta)}</span></li>
 | 
			
		||||
				<li><span class="key">gamma</span><span>{toFixed(deviceOrientation?.gamma)}</span></li>
 | 
			
		||||
			</ul>
 | 
			
		||||
			<div class="row">
 | 
			
		||||
				<button onclick={() => copyJSON(deviceOrientation)}
 | 
			
		||||
					><i class="ti ti-copy"></i> {m.sensors_copy()}</button
 | 
			
		||||
				>
 | 
			
		||||
			</div>
 | 
			
		||||
		{:else}
 | 
			
		||||
			<div class="subdued">{m.sensors_notSupported()}</div>
 | 
			
		||||
		{/if}
 | 
			
		||||
	</section>
 | 
			
		||||
 | 
			
		||||
	<section>
 | 
			
		||||
		<h3>{m.sensors_accelerometer()}</h3>
 | 
			
		||||
		{#if accelerometer.supported}
 | 
			
		||||
			<div class="row">
 | 
			
		||||
				{#if !accelerometer.active}
 | 
			
		||||
					<button onclick={() => startSensor(accelerometer, 'Accelerometer')}
 | 
			
		||||
						><i class="ti ti-player-play"></i> {m.sensors_start()}</button
 | 
			
		||||
					>
 | 
			
		||||
				{:else}
 | 
			
		||||
					<button onclick={() => stopSensor(accelerometer)}
 | 
			
		||||
						><i class="ti ti-player-stop"></i> {m.sensors_stop()}</button
 | 
			
		||||
					>
 | 
			
		||||
				{/if}
 | 
			
		||||
			</div>
 | 
			
		||||
			{#if accelerometer.error}
 | 
			
		||||
				<div class="error">{accelerometer.error}</div>
 | 
			
		||||
			{/if}
 | 
			
		||||
			<ul class="kv">
 | 
			
		||||
				<li><span class="key">x</span><span>{toFixed(accelerometer.data.x as number)}</span></li>
 | 
			
		||||
				<li><span class="key">y</span><span>{toFixed(accelerometer.data.y as number)}</span></li>
 | 
			
		||||
				<li><span class="key">z</span><span>{toFixed(accelerometer.data.z as number)}</span></li>
 | 
			
		||||
			</ul>
 | 
			
		||||
		{:else}
 | 
			
		||||
			<div class="subdued">{m.sensors_notSupported()}</div>
 | 
			
		||||
		{/if}
 | 
			
		||||
	</section>
 | 
			
		||||
 | 
			
		||||
	<section>
 | 
			
		||||
		<h3>{m.sensors_gyroscope()}</h3>
 | 
			
		||||
		{#if gyroscope.supported}
 | 
			
		||||
			<div class="row">
 | 
			
		||||
				{#if !gyroscope.active}
 | 
			
		||||
					<button onclick={() => startSensor(gyroscope, 'Gyroscope')}
 | 
			
		||||
						><i class="ti ti-player-play"></i> {m.sensors_start()}</button
 | 
			
		||||
					>
 | 
			
		||||
				{:else}
 | 
			
		||||
					<button onclick={() => stopSensor(gyroscope)}
 | 
			
		||||
						><i class="ti ti-player-stop"></i> {m.sensors_stop()}</button
 | 
			
		||||
					>
 | 
			
		||||
				{/if}
 | 
			
		||||
			</div>
 | 
			
		||||
			{#if gyroscope.error}
 | 
			
		||||
				<div class="error">{gyroscope.error}</div>
 | 
			
		||||
			{/if}
 | 
			
		||||
			<ul class="kv">
 | 
			
		||||
				<li><span class="key">x</span><span>{toFixed(gyroscope.data.x as number)}</span></li>
 | 
			
		||||
				<li><span class="key">y</span><span>{toFixed(gyroscope.data.y as number)}</span></li>
 | 
			
		||||
				<li><span class="key">z</span><span>{toFixed(gyroscope.data.z as number)}</span></li>
 | 
			
		||||
			</ul>
 | 
			
		||||
		{:else}
 | 
			
		||||
			<div class="subdued">{m.sensors_notSupported()}</div>
 | 
			
		||||
		{/if}
 | 
			
		||||
	</section>
 | 
			
		||||
 | 
			
		||||
	<section>
 | 
			
		||||
		<h3>{m.sensors_magnetometer()}</h3>
 | 
			
		||||
		{#if magnetometer.supported}
 | 
			
		||||
			<div class="row">
 | 
			
		||||
				{#if !magnetometer.active}
 | 
			
		||||
					<button onclick={() => startSensor(magnetometer, 'Magnetometer', { frequency: 10 })}
 | 
			
		||||
						><i class="ti ti-player-play"></i> {m.sensors_start()}</button
 | 
			
		||||
					>
 | 
			
		||||
				{:else}
 | 
			
		||||
					<button onclick={() => stopSensor(magnetometer)}
 | 
			
		||||
						><i class="ti ti-player-stop"></i> {m.sensors_stop()}</button
 | 
			
		||||
					>
 | 
			
		||||
				{/if}
 | 
			
		||||
			</div>
 | 
			
		||||
			{#if magnetometer.error}
 | 
			
		||||
				<div class="error">{magnetometer.error}</div>
 | 
			
		||||
			{/if}
 | 
			
		||||
			<ul class="kv">
 | 
			
		||||
				<li><span class="key">x</span><span>{toFixed(magnetometer.data.x as number)}</span></li>
 | 
			
		||||
				<li><span class="key">y</span><span>{toFixed(magnetometer.data.y as number)}</span></li>
 | 
			
		||||
				<li><span class="key">z</span><span>{toFixed(magnetometer.data.z as number)}</span></li>
 | 
			
		||||
			</ul>
 | 
			
		||||
		{:else}
 | 
			
		||||
			<div class="subdued">{m.sensors_notSupported()}</div>
 | 
			
		||||
		{/if}
 | 
			
		||||
	</section>
 | 
			
		||||
 | 
			
		||||
	<section>
 | 
			
		||||
		<h3>{m.sensors_ambientLight()}</h3>
 | 
			
		||||
		{#if ambientLight.supported}
 | 
			
		||||
			<div class="row">
 | 
			
		||||
				{#if !ambientLight.active}
 | 
			
		||||
					<button onclick={() => startSensor(ambientLight, 'AmbientLightSensor')}
 | 
			
		||||
						><i class="ti ti-player-play"></i> {m.sensors_start()}</button
 | 
			
		||||
					>
 | 
			
		||||
				{:else}
 | 
			
		||||
					<button onclick={() => stopSensor(ambientLight)}
 | 
			
		||||
						><i class="ti ti-player-stop"></i> {m.sensors_stop()}</button
 | 
			
		||||
					>
 | 
			
		||||
				{/if}
 | 
			
		||||
			</div>
 | 
			
		||||
			{#if ambientLight.error}
 | 
			
		||||
				<div class="error">{ambientLight.error}</div>
 | 
			
		||||
			{/if}
 | 
			
		||||
			<ul class="kv">
 | 
			
		||||
				<li>
 | 
			
		||||
					<span class="key">{m.sensors_illuminance()}</span><span
 | 
			
		||||
						>{toFixed(ambientLight.data.illuminance as number)}</span
 | 
			
		||||
					>
 | 
			
		||||
				</li>
 | 
			
		||||
			</ul>
 | 
			
		||||
		{:else}
 | 
			
		||||
			<div class="subdued">{m.sensors_notSupported()}</div>
 | 
			
		||||
		{/if}
 | 
			
		||||
	</section>
 | 
			
		||||
 | 
			
		||||
	<section>
 | 
			
		||||
		<h3>{m.sensors_barometer()}</h3>
 | 
			
		||||
		{#if barometer.supported}
 | 
			
		||||
			<div class="row">
 | 
			
		||||
				{#if !barometer.active}
 | 
			
		||||
					<button onclick={() => startSensor(barometer, 'Barometer')}
 | 
			
		||||
						><i class="ti ti-player-play"></i> {m.sensors_start()}</button
 | 
			
		||||
					>
 | 
			
		||||
				{:else}
 | 
			
		||||
					<button onclick={() => stopSensor(barometer)}
 | 
			
		||||
						><i class="ti ti-player-stop"></i> {m.sensors_stop()}</button
 | 
			
		||||
					>
 | 
			
		||||
				{/if}
 | 
			
		||||
			</div>
 | 
			
		||||
			{#if barometer.error}
 | 
			
		||||
				<div class="error">{barometer.error}</div>
 | 
			
		||||
			{/if}
 | 
			
		||||
			<ul class="kv">
 | 
			
		||||
				<li>
 | 
			
		||||
					<span class="key">{m.sensors_pressure()}</span><span
 | 
			
		||||
						>{toFixed(barometer.data.pressure as number)}</span
 | 
			
		||||
					>
 | 
			
		||||
				</li>
 | 
			
		||||
				<li>
 | 
			
		||||
					<span class="key">{m.sensors_temperature()}</span><span
 | 
			
		||||
						>{barometer.data.temperature ?? '—'}</span
 | 
			
		||||
					>
 | 
			
		||||
				</li>
 | 
			
		||||
			</ul>
 | 
			
		||||
		{:else}
 | 
			
		||||
			<div class="subdued">{m.sensors_notSupported()}</div>
 | 
			
		||||
		{/if}
 | 
			
		||||
	</section>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
	.sections {
 | 
			
		||||
		display: grid;
 | 
			
		||||
		grid-template-columns: 1fr 1fr;
 | 
			
		||||
		gap: 1rem;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	section {
 | 
			
		||||
		border: 1px solid currentColor;
 | 
			
		||||
		border-radius: 0.5rem;
 | 
			
		||||
		padding: 0.75rem;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	h3 {
 | 
			
		||||
		margin: 0 0 0.5rem 0;
 | 
			
		||||
		font-size: 1.1rem;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.row {
 | 
			
		||||
		display: flex;
 | 
			
		||||
		gap: 0.5rem;
 | 
			
		||||
		align-items: center;
 | 
			
		||||
		flex-wrap: wrap;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.kv {
 | 
			
		||||
		list-style: none;
 | 
			
		||||
		padding: 0;
 | 
			
		||||
		margin: 0.5rem 0 0 0;
 | 
			
		||||
		display: grid;
 | 
			
		||||
		grid-template-columns: auto 1fr;
 | 
			
		||||
		gap: 0.25rem 0.75rem;
 | 
			
		||||
		align-items: baseline;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.kv .key {
 | 
			
		||||
		opacity: 0.8;
 | 
			
		||||
		margin-right: 0.25em;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.error {
 | 
			
		||||
		color: #ff6b6b;
 | 
			
		||||
		margin: 0.25rem 0;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.subdued {
 | 
			
		||||
		opacity: 0.7;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	button {
 | 
			
		||||
		background: none;
 | 
			
		||||
		color: inherit;
 | 
			
		||||
		border: 1px solid currentColor;
 | 
			
		||||
		border-radius: 0.25rem;
 | 
			
		||||
		padding: 0.25rem 0.5rem;
 | 
			
		||||
		display: inline-flex;
 | 
			
		||||
		align-items: center;
 | 
			
		||||
		gap: 0.4rem;
 | 
			
		||||
	}
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			@ -1,354 +0,0 @@
 | 
			
		|||
<script lang="ts">
 | 
			
		||||
	import { onMount, onDestroy } from 'svelte';
 | 
			
		||||
	import { m } from '$lib/paraglide/messages';
 | 
			
		||||
	import Oscilloscope from '$lib/audio/Oscilloscope.svelte';
 | 
			
		||||
	import Spectrum from '$lib/audio/Spectrum.svelte';
 | 
			
		||||
 | 
			
		||||
	type GenType = 'off' | 'sine' | 'sweep' | 'white' | 'pink' | 'brown';
 | 
			
		||||
 | 
			
		||||
	let ctx: AudioContext | null = null;
 | 
			
		||||
	let gainNode: GainNode | null = null;
 | 
			
		||||
	let analyser = $state<AnalyserNode | null>(null);
 | 
			
		||||
	let source: AudioNode | null = null; // OscillatorNode or AudioBufferSourceNode
 | 
			
		||||
	let started = $state(false);
 | 
			
		||||
 | 
			
		||||
	let genType = $state<GenType>('sine');
 | 
			
		||||
	let frequency = $state(440);
 | 
			
		||||
	let volume = $state(0.2);
 | 
			
		||||
 | 
			
		||||
	let sweepFrom = $state(20);
 | 
			
		||||
	let sweepTo = $state(20000);
 | 
			
		||||
	let sweepDuration = $state(10); // seconds
 | 
			
		||||
	let sweepLoop = $state(false);
 | 
			
		||||
 | 
			
		||||
	function ensureAudio() {
 | 
			
		||||
		if (!ctx) {
 | 
			
		||||
			ctx = new AudioContext();
 | 
			
		||||
			gainNode = ctx.createGain();
 | 
			
		||||
			gainNode.gain.value = volume;
 | 
			
		||||
			analyser = ctx.createAnalyser();
 | 
			
		||||
			analyser.fftSize = 2048;
 | 
			
		||||
			analyser.smoothingTimeConstant = 0.7;
 | 
			
		||||
			gainNode.connect(analyser);
 | 
			
		||||
			analyser.connect(ctx.destination);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function connectSource(node: AudioNode) {
 | 
			
		||||
		ensureAudio();
 | 
			
		||||
		stopSource();
 | 
			
		||||
		source = node;
 | 
			
		||||
		node.connect(gainNode!);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function stopSource() {
 | 
			
		||||
		if (!source) return;
 | 
			
		||||
		try {
 | 
			
		||||
			// Try to stop if it has a stop() method
 | 
			
		||||
			(source as any).stop?.();
 | 
			
		||||
			(source as any).disconnect?.();
 | 
			
		||||
		} catch {}
 | 
			
		||||
		source = null;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function start() {
 | 
			
		||||
		ensureAudio();
 | 
			
		||||
		ctx!.resume();
 | 
			
		||||
		started = true;
 | 
			
		||||
		if (genType === 'sine') startSine();
 | 
			
		||||
		else if (genType === 'sweep') startSweep();
 | 
			
		||||
		else if (genType === 'white') startNoise('white');
 | 
			
		||||
		else if (genType === 'pink') startNoise('pink');
 | 
			
		||||
		else if (genType === 'brown') startNoise('brown');
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function stop() {
 | 
			
		||||
		stopSource();
 | 
			
		||||
		started = false;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function onTypeChange(t: GenType) {
 | 
			
		||||
		genType = t;
 | 
			
		||||
		if (!started) return;
 | 
			
		||||
		// Restart with new type
 | 
			
		||||
		start();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function onFrequencyChange(v: number) {
 | 
			
		||||
		frequency = v;
 | 
			
		||||
		if (source && (source as OscillatorNode).frequency) {
 | 
			
		||||
			(source as OscillatorNode).frequency.setValueAtTime(frequency, ctx!.currentTime);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function onVolumeChange(v: number) {
 | 
			
		||||
		volume = v;
 | 
			
		||||
		if (gainNode) gainNode.gain.setValueAtTime(volume, ctx!.currentTime);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function startSine() {
 | 
			
		||||
		const osc = ctx!.createOscillator();
 | 
			
		||||
		osc.type = 'sine';
 | 
			
		||||
		osc.frequency.setValueAtTime(frequency, ctx!.currentTime);
 | 
			
		||||
		connectSource(osc);
 | 
			
		||||
		osc.start();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function startSweep() {
 | 
			
		||||
		const osc = ctx!.createOscillator();
 | 
			
		||||
		osc.type = 'sine';
 | 
			
		||||
		const startF = Math.max(0.1, sweepFrom);
 | 
			
		||||
		const endF = Math.max(0.1, sweepTo);
 | 
			
		||||
		const now = ctx!.currentTime;
 | 
			
		||||
		osc.frequency.cancelScheduledValues(now);
 | 
			
		||||
		osc.frequency.setValueAtTime(startF, now);
 | 
			
		||||
		// Use exponential ramp if both positive and not equal; fall back to linear otherwise
 | 
			
		||||
		if (startF > 0 && endF > 0 && startF !== endF) {
 | 
			
		||||
			osc.frequency.exponentialRampToValueAtTime(endF, now + sweepDuration);
 | 
			
		||||
		} else {
 | 
			
		||||
			osc.frequency.linearRampToValueAtTime(endF, now + sweepDuration);
 | 
			
		||||
		}
 | 
			
		||||
		connectSource(osc);
 | 
			
		||||
		osc.start();
 | 
			
		||||
		// auto stop or loop after sweep
 | 
			
		||||
		const timeoutId = setTimeout(() => {
 | 
			
		||||
			try {
 | 
			
		||||
				osc.stop();
 | 
			
		||||
			} catch {}
 | 
			
		||||
			if (started && genType === 'sweep') {
 | 
			
		||||
				if (sweepLoop) {
 | 
			
		||||
					startSweep(); // restart for loop
 | 
			
		||||
				} else {
 | 
			
		||||
					started = false;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}, sweepDuration * 1000);
 | 
			
		||||
 | 
			
		||||
		// When the source is replaced, clear this timeout
 | 
			
		||||
		source?.addEventListener('disconnect', () => clearTimeout(timeoutId), { once: true });
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function makeNoiseBuffer(kind: 'white' | 'pink' | 'brown') {
 | 
			
		||||
		const sr = ctx!.sampleRate;
 | 
			
		||||
		const seconds = 2;
 | 
			
		||||
		const frameCount = sr * seconds;
 | 
			
		||||
		const buffer = ctx!.createBuffer(2, frameCount, sr);
 | 
			
		||||
		for (let ch = 0; ch < buffer.numberOfChannels; ch++) {
 | 
			
		||||
			const data = buffer.getChannelData(ch);
 | 
			
		||||
			if (kind === 'white') {
 | 
			
		||||
				for (let i = 0; i < frameCount; i++) data[i] = Math.random() * 2 - 1;
 | 
			
		||||
			} else if (kind === 'pink') {
 | 
			
		||||
				// Voss-McCartney algorithm approximation
 | 
			
		||||
				let b0 = 0,
 | 
			
		||||
					b1 = 0,
 | 
			
		||||
					b2 = 0,
 | 
			
		||||
					b3 = 0,
 | 
			
		||||
					b4 = 0,
 | 
			
		||||
					b5 = 0,
 | 
			
		||||
					b6 = 0;
 | 
			
		||||
				for (let i = 0; i < frameCount; i++) {
 | 
			
		||||
					const white = Math.random() * 2 - 1;
 | 
			
		||||
					b0 = 0.99886 * b0 + white * 0.0555179;
 | 
			
		||||
					b1 = 0.99332 * b1 + white * 0.0750759;
 | 
			
		||||
					b2 = 0.969 * b2 + white * 0.153852;
 | 
			
		||||
					b3 = 0.8665 * b3 + white * 0.3104856;
 | 
			
		||||
					b4 = 0.55 * b4 + white * 0.5329522;
 | 
			
		||||
					b5 = -0.7616 * b5 - white * 0.016898;
 | 
			
		||||
					const pink = b0 + b1 + b2 + b3 + b4 + b5 + b6 + white * 0.5362;
 | 
			
		||||
					b6 = white * 0.115926;
 | 
			
		||||
					data[i] = pink * 0.11; // gain normalize
 | 
			
		||||
				}
 | 
			
		||||
			} else if (kind === 'brown') {
 | 
			
		||||
				let lastOut = 0;
 | 
			
		||||
				for (let i = 0; i < frameCount; i++) {
 | 
			
		||||
					const white = Math.random() * 2 - 1;
 | 
			
		||||
					const brown = (lastOut + 0.02 * white) / 1.02;
 | 
			
		||||
					lastOut = brown;
 | 
			
		||||
					data[i] = brown * 3.5; // gain normalize
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		return buffer;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function startNoise(kind: 'white' | 'pink' | 'brown') {
 | 
			
		||||
		const buf = makeNoiseBuffer(kind);
 | 
			
		||||
		const src = ctx!.createBufferSource();
 | 
			
		||||
		src.buffer = buf;
 | 
			
		||||
		src.loop = true;
 | 
			
		||||
		connectSource(src);
 | 
			
		||||
		src.start();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	onMount(() => {
 | 
			
		||||
		// prepare audio on mount but start only on user gesture
 | 
			
		||||
		ensureAudio();
 | 
			
		||||
 | 
			
		||||
		return () => {
 | 
			
		||||
			stop();
 | 
			
		||||
		};
 | 
			
		||||
	});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<h2><i class="ti ti-wave-sine"></i> {m.signalGen_title()}</h2>
 | 
			
		||||
 | 
			
		||||
<article class="layout">
 | 
			
		||||
	<section class="controls">
 | 
			
		||||
		<div class="row">
 | 
			
		||||
			<span class="label">{m.signalGen_type()}:</span>
 | 
			
		||||
			<div class="buttons">
 | 
			
		||||
				<button class:active={genType === 'sine'} onclick={() => onTypeChange('sine')}
 | 
			
		||||
					>{m.signalGen_sine()}</button
 | 
			
		||||
				>
 | 
			
		||||
				<button class:active={genType === 'sweep'} onclick={() => onTypeChange('sweep')}
 | 
			
		||||
					>{m.signalGen_sweep()}</button
 | 
			
		||||
				>
 | 
			
		||||
				<button class:active={genType === 'white'} onclick={() => onTypeChange('white')}
 | 
			
		||||
					>{m.signalGen_noiseWhite()}</button
 | 
			
		||||
				>
 | 
			
		||||
				<button class:active={genType === 'pink'} onclick={() => onTypeChange('pink')}
 | 
			
		||||
					>{m.signalGen_noisePink()}</button
 | 
			
		||||
				>
 | 
			
		||||
				<button class:active={genType === 'brown'} onclick={() => onTypeChange('brown')}
 | 
			
		||||
					>{m.signalGen_noiseBrown()}</button
 | 
			
		||||
				>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
 | 
			
		||||
		{#if genType === 'sine'}
 | 
			
		||||
			<div class="row">
 | 
			
		||||
				<label for="freq">{m.signalGen_frequency()}:</label>
 | 
			
		||||
				<input
 | 
			
		||||
					id="freq"
 | 
			
		||||
					type="range"
 | 
			
		||||
					min="20"
 | 
			
		||||
					max="20000"
 | 
			
		||||
					step="1"
 | 
			
		||||
					bind:value={frequency}
 | 
			
		||||
					oninput={(e) => onFrequencyChange(+e.currentTarget.value)}
 | 
			
		||||
				/>
 | 
			
		||||
				<input
 | 
			
		||||
					type="number"
 | 
			
		||||
					min="20"
 | 
			
		||||
					max="20000"
 | 
			
		||||
					class="exact-freq"
 | 
			
		||||
					bind:value={frequency}
 | 
			
		||||
					oninput={(e) => onFrequencyChange(+e.currentTarget.value)}
 | 
			
		||||
				/>
 | 
			
		||||
				<span class="value">Hz</span>
 | 
			
		||||
			</div>
 | 
			
		||||
		{/if}
 | 
			
		||||
 | 
			
		||||
		{#if genType === 'sweep'}
 | 
			
		||||
			<div class="row">
 | 
			
		||||
				<label for="sweep-from">{m.signalGen_from()}:</label>
 | 
			
		||||
				<input id="sweep-from" type="number" min="1" max="20000" bind:value={sweepFrom} />
 | 
			
		||||
				<label for="sweep-to">{m.signalGen_to()}:</label>
 | 
			
		||||
				<input id="sweep-to" type="number" min="1" max="20000" bind:value={sweepTo} />
 | 
			
		||||
			</div>
 | 
			
		||||
			<div class="row">
 | 
			
		||||
				<label for="sweep-duration">{m.signalGen_duration()}:</label>
 | 
			
		||||
				<input id="sweep-duration" type="number" min="1" max="120" bind:value={sweepDuration} />
 | 
			
		||||
				<span class="value">s</span>
 | 
			
		||||
				<label class="checkbox-label"
 | 
			
		||||
					><input type="checkbox" bind:checked={sweepLoop} />{m.signalGen_loop()}</label
 | 
			
		||||
				>
 | 
			
		||||
			</div>
 | 
			
		||||
		{/if}
 | 
			
		||||
 | 
			
		||||
		<div class="row">
 | 
			
		||||
			<label for="gain">{m.signalGen_gain()}:</label>
 | 
			
		||||
			<input
 | 
			
		||||
				id="gain"
 | 
			
		||||
				type="range"
 | 
			
		||||
				min="0"
 | 
			
		||||
				max="1"
 | 
			
		||||
				step="0.01"
 | 
			
		||||
				bind:value={volume}
 | 
			
		||||
				oninput={(e) => onVolumeChange(+e.currentTarget.value)}
 | 
			
		||||
			/>
 | 
			
		||||
			<span class="value">{(volume * 100) | 0}%</span>
 | 
			
		||||
		</div>
 | 
			
		||||
 | 
			
		||||
		<div class="row">
 | 
			
		||||
			{#if !started}
 | 
			
		||||
				<button class="primary" onclick={start}
 | 
			
		||||
					><i class="ti ti-player-play"></i> {m.signalGen_start()}</button
 | 
			
		||||
				>
 | 
			
		||||
			{:else}
 | 
			
		||||
				<button onclick={stop}><i class="ti ti-player-stop"></i> {m.signalGen_stop()}</button>
 | 
			
		||||
			{/if}
 | 
			
		||||
		</div>
 | 
			
		||||
	</section>
 | 
			
		||||
 | 
			
		||||
	<section class="displays" aria-hidden={!started}>
 | 
			
		||||
		{#if analyser}
 | 
			
		||||
			<Oscilloscope {analyser} title={m.signalGen_scope()} />
 | 
			
		||||
			<Spectrum {analyser} title={m.signalGen_spectrum()} />
 | 
			
		||||
		{/if}
 | 
			
		||||
	</section>
 | 
			
		||||
</article>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
	.layout {
 | 
			
		||||
		display: grid;
 | 
			
		||||
		grid-template-columns: 1fr 2fr;
 | 
			
		||||
		gap: 1rem;
 | 
			
		||||
	}
 | 
			
		||||
	.controls {
 | 
			
		||||
		display: flex;
 | 
			
		||||
		flex-direction: column;
 | 
			
		||||
		gap: 0.75rem;
 | 
			
		||||
	}
 | 
			
		||||
	.controls .row {
 | 
			
		||||
		display: flex;
 | 
			
		||||
		align-items: center;
 | 
			
		||||
		gap: 0.5rem;
 | 
			
		||||
		flex-wrap: wrap;
 | 
			
		||||
	}
 | 
			
		||||
	.controls label {
 | 
			
		||||
		opacity: 0.8;
 | 
			
		||||
		min-width: 6rem;
 | 
			
		||||
	}
 | 
			
		||||
	.controls .buttons {
 | 
			
		||||
		display: flex;
 | 
			
		||||
		gap: 0.5rem;
 | 
			
		||||
		flex-wrap: wrap;
 | 
			
		||||
	}
 | 
			
		||||
	.controls button {
 | 
			
		||||
		border: 1px solid rgba(255, 255, 255, 0.25);
 | 
			
		||||
		background: rgba(255, 255, 255, 0.07);
 | 
			
		||||
		padding: 0.25rem 0.5rem;
 | 
			
		||||
		border-radius: 4px;
 | 
			
		||||
		cursor: pointer;
 | 
			
		||||
	}
 | 
			
		||||
	.controls button.active,
 | 
			
		||||
	.controls button.primary {
 | 
			
		||||
		background: rgba(92, 184, 92, 0.2);
 | 
			
		||||
		border-color: #5cb85c;
 | 
			
		||||
	}
 | 
			
		||||
	.controls input[type='range'] {
 | 
			
		||||
		flex: 1 1 auto;
 | 
			
		||||
	}
 | 
			
		||||
	.controls .value {
 | 
			
		||||
		min-width: 4rem;
 | 
			
		||||
		text-align: right;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.exact-freq {
 | 
			
		||||
		width: 5em;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.checkbox-label {
 | 
			
		||||
		display: inline-flex;
 | 
			
		||||
		align-items: center;
 | 
			
		||||
		gap: 0.5em;
 | 
			
		||||
		user-select: none;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.displays {
 | 
			
		||||
		display: grid;
 | 
			
		||||
		grid-template-rows: auto auto;
 | 
			
		||||
		gap: 1rem;
 | 
			
		||||
	}
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			@ -1,75 +0,0 @@
 | 
			
		|||
<script lang="ts">
 | 
			
		||||
	import { onMount } from 'svelte';
 | 
			
		||||
	import { m } from '$lib/paraglide/messages';
 | 
			
		||||
 | 
			
		||||
	let time = $state(0);
 | 
			
		||||
	let fps = 0;
 | 
			
		||||
	let start = 0;
 | 
			
		||||
	let displayFps = $state('?');
 | 
			
		||||
	let fpsInterval: NodeJS.Timeout | undefined;
 | 
			
		||||
 | 
			
		||||
	const times: number[] = [];
 | 
			
		||||
	function refreshLoop() {
 | 
			
		||||
		const now = performance.now();
 | 
			
		||||
		time = Math.floor(now - start);
 | 
			
		||||
		while (times.length > 0 && times[0] <= now - 1000) {
 | 
			
		||||
			times.shift();
 | 
			
		||||
		}
 | 
			
		||||
		times.push(now);
 | 
			
		||||
		fps = times.length;
 | 
			
		||||
		window.requestAnimationFrame(refreshLoop);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function restart() {
 | 
			
		||||
		displayFps = '?';
 | 
			
		||||
		times.length = 0;
 | 
			
		||||
		start = performance.now();
 | 
			
		||||
		clearInterval(fpsInterval);
 | 
			
		||||
		fpsInterval = setInterval(() => {
 | 
			
		||||
			displayFps = fps.toString();
 | 
			
		||||
		}, 1000);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	onMount(() => {
 | 
			
		||||
		refreshLoop();
 | 
			
		||||
		restart();
 | 
			
		||||
	});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<h2><i class="ti ti-alarm"></i> {m.timer_title()}</h2>
 | 
			
		||||
<div class="display">
 | 
			
		||||
	<div class="time">{time}</div>
 | 
			
		||||
	<div class="fps">{displayFps} {m.timer_fps()}</div>
 | 
			
		||||
</div>
 | 
			
		||||
<button onclick={restart}>{m.timer_restart()}</button>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
	div,
 | 
			
		||||
	button {
 | 
			
		||||
		text-align: center;
 | 
			
		||||
		margin-bottom: 1rem;
 | 
			
		||||
		user-select: none;
 | 
			
		||||
		line-height: 1;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.display {
 | 
			
		||||
		flex-grow: 1;
 | 
			
		||||
		display: flex;
 | 
			
		||||
		flex-direction: column;
 | 
			
		||||
		justify-content: center;
 | 
			
		||||
		font-variant-numeric: tabular-nums;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.time {
 | 
			
		||||
		font-size: 12rem;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.fps {
 | 
			
		||||
		font-size: 4rem;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	button {
 | 
			
		||||
		align-self: center;
 | 
			
		||||
		font-size: 2rem;
 | 
			
		||||
	}
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			@ -1,15 +1,13 @@
 | 
			
		|||
<script lang="ts">
 | 
			
		||||
	import 'normalize.css/normalize.css';
 | 
			
		||||
	import '@fontsource/atkinson-hyperlegible';
 | 
			
		||||
	import '@fontsource/atkinson-hyperlegible/700.css';
 | 
			
		||||
	import '@fontsource/b612';
 | 
			
		||||
	import '@fontsource/b612/700.css';
 | 
			
		||||
	import '@tabler/icons-webfont/tabler-icons.css';
 | 
			
		||||
	import '../index.css';
 | 
			
		||||
	import TestCard from '$lib/TestCard.svelte';
 | 
			
		||||
	import { page } from '$app/stores';
 | 
			
		||||
	import { onMount } from 'svelte';
 | 
			
		||||
	interface Props {
 | 
			
		||||
		children?: import('svelte').Snippet;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	let { children }: Props = $props();
 | 
			
		||||
	import { goto } from '$app/navigation';
 | 
			
		||||
 | 
			
		||||
	let idleTimeout: NodeJS.Timeout | undefined;
 | 
			
		||||
	onMount(() => {
 | 
			
		||||
| 
						 | 
				
			
			@ -21,11 +19,53 @@
 | 
			
		|||
			}, 3000);
 | 
			
		||||
		});
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	$: onlyCard = $page.data.card;
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
{@render children?.()}
 | 
			
		||||
<TestCard full={onlyCard} on:focus={() => goto('/card')} />
 | 
			
		||||
<main class:content={!onlyCard} class:sub={!$page.data.root && !onlyCard}>
 | 
			
		||||
	<a href=".." class="button button-back"><i class="ti ti-arrow-back" />Back</a>
 | 
			
		||||
	<slot />
 | 
			
		||||
</main>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
	main.content {
 | 
			
		||||
		position: absolute;
 | 
			
		||||
		top: 50%;
 | 
			
		||||
		left: 50%;
 | 
			
		||||
		transform: translate(-50%, -50%);
 | 
			
		||||
		background: rgba(0, 0, 0, 0.8);
 | 
			
		||||
		border-radius: 0.5rem;
 | 
			
		||||
		border: 1px solid white;
 | 
			
		||||
 | 
			
		||||
		padding: 1rem;
 | 
			
		||||
 | 
			
		||||
		display: flex;
 | 
			
		||||
		flex-direction: column;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	main.sub {
 | 
			
		||||
		height: 90vh;
 | 
			
		||||
		width: 90vw;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.button-back {
 | 
			
		||||
		position: absolute;
 | 
			
		||||
		top: 1rem;
 | 
			
		||||
		right: 1rem;
 | 
			
		||||
 | 
			
		||||
		opacity: 0.66;
 | 
			
		||||
		transition: opacity 0.3s;
 | 
			
		||||
		&:hover {
 | 
			
		||||
			opacity: 1;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	main:not(.sub) .button-back {
 | 
			
		||||
		display: none;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	:global(.hide-idle) {
 | 
			
		||||
		transition: opacity 1s;
 | 
			
		||||
		opacity: 1;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,2 +1 @@
 | 
			
		|||
export const prerender = true;
 | 
			
		||||
export const trailingSlash = 'always';
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										87
									
								
								src/routes/+page.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								src/routes/+page.svelte
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,87 @@
 | 
			
		|||
<script>
 | 
			
		||||
	import { version } from '../../package.json';
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<nav>
 | 
			
		||||
	<h1>Universal Test Card</h1>
 | 
			
		||||
 | 
			
		||||
	<div class="options">
 | 
			
		||||
		<a href="card">
 | 
			
		||||
			<i class="ti ti-device-desktop"></i>
 | 
			
		||||
			Screen
 | 
			
		||||
		</a>
 | 
			
		||||
		<a href="audio">
 | 
			
		||||
			<i class="ti ti-volume"></i>
 | 
			
		||||
			Audio
 | 
			
		||||
		</a>
 | 
			
		||||
		<a href="av-sync">
 | 
			
		||||
			<i class="ti ti-time-duration-off"></i>
 | 
			
		||||
			AV Sync
 | 
			
		||||
		</a>
 | 
			
		||||
		<a href="keyboard">
 | 
			
		||||
			<i class="ti ti-keyboard"></i>
 | 
			
		||||
			Keyboard
 | 
			
		||||
		</a>
 | 
			
		||||
		<a href="mouse" class="disabled">
 | 
			
		||||
			<i class="ti ti-mouse"></i>
 | 
			
		||||
			Mouse
 | 
			
		||||
		</a>
 | 
			
		||||
		<a href="gamepad">
 | 
			
		||||
			<i class="ti ti-device-gamepad"></i>
 | 
			
		||||
			Gamepad
 | 
			
		||||
		</a>
 | 
			
		||||
		<a href="camera">
 | 
			
		||||
			<i class="ti ti-camera"></i>
 | 
			
		||||
			Camera
 | 
			
		||||
		</a>
 | 
			
		||||
		<a href="microphone" class="disabled">
 | 
			
		||||
			<i class="ti ti-microphone"></i>
 | 
			
		||||
			Microphone
 | 
			
		||||
		</a>
 | 
			
		||||
		<a href="sensors" class="disabled">
 | 
			
		||||
			<i class="ti ti-cpu-2"></i>
 | 
			
		||||
			Sensors
 | 
			
		||||
		</a>
 | 
			
		||||
	</div>
 | 
			
		||||
</nav>
 | 
			
		||||
<footer><a href="https://git.thm.place/thm/test-card">testcard v{version}</a></footer>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
	h1 {
 | 
			
		||||
		text-align: center;
 | 
			
		||||
		font-size: 3rem;
 | 
			
		||||
		margin: 1rem;
 | 
			
		||||
		text-transform: uppercase;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.options {
 | 
			
		||||
		display: flex;
 | 
			
		||||
		justify-content: space-evenly;
 | 
			
		||||
		align-items: center;
 | 
			
		||||
		gap: 2em;
 | 
			
		||||
 | 
			
		||||
		& a {
 | 
			
		||||
			text-align: center;
 | 
			
		||||
			text-decoration: none;
 | 
			
		||||
 | 
			
		||||
			&.disabled {
 | 
			
		||||
				pointer-events: none;
 | 
			
		||||
				opacity: 0.5;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		& .ti {
 | 
			
		||||
			display: block;
 | 
			
		||||
			font-size: 3rem;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	footer {
 | 
			
		||||
		text-align: center;
 | 
			
		||||
		opacity: 0.6;
 | 
			
		||||
		margin-top: 1rem;
 | 
			
		||||
		& a {
 | 
			
		||||
			text-decoration: none;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			@ -3,5 +3,5 @@ import type { PageLoad } from './$types';
 | 
			
		|||
export const load: PageLoad = () => {
 | 
			
		||||
	return {
 | 
			
		||||
		root: true
 | 
			
		||||
	};
 | 
			
		||||
};
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,16 +1,11 @@
 | 
			
		|||
<script lang="ts">
 | 
			
		||||
	import CycleButton from './cycle-button.svelte';
 | 
			
		||||
	interface Props {
 | 
			
		||||
		children?: import('svelte').Snippet;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	let { children }: Props = $props();
 | 
			
		||||
 | 
			
		||||
	let channelsEl: HTMLDivElement | undefined = $state();
 | 
			
		||||
	let channelsEl: HTMLDivElement;
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<div class="channels" bind:this={channelsEl}>
 | 
			
		||||
	{@render children?.()}
 | 
			
		||||
	<slot />
 | 
			
		||||
</div>
 | 
			
		||||
<div class="controls">
 | 
			
		||||
	<CycleButton element={channelsEl} />
 | 
			
		||||
| 
						 | 
				
			
			@ -6,20 +6,19 @@
 | 
			
		|||
	import rearLeftUrl from '@assets/audio/5.1/Rear_Left.mp3';
 | 
			
		||||
	import rearRightUrl from '@assets/audio/5.1/Rear_Right.mp3';
 | 
			
		||||
	import LfeUrl from '@assets/audio/5.1/LFE_Noise.mp3';
 | 
			
		||||
	import { m } from '$lib/paraglide/messages';
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<div class="row">
 | 
			
		||||
	<Speaker src={frontLeftUrl} left>{m.audio_channel_frontLeft()}</Speaker>
 | 
			
		||||
	<Speaker src={frontLeftUrl} left>Front Left</Speaker>
 | 
			
		||||
	<div class="center">
 | 
			
		||||
		<Speaker src={frontCenterUrl} center>{m.audio_channel_frontCenter()}</Speaker>
 | 
			
		||||
		<Speaker src={frontCenterUrl} center>Front Center</Speaker>
 | 
			
		||||
	</div>
 | 
			
		||||
	<Speaker src={frontRightUrl} right>{m.audio_channel_frontRight()}</Speaker>
 | 
			
		||||
	<Speaker src={frontRightUrl} right>Front Right</Speaker>
 | 
			
		||||
</div>
 | 
			
		||||
<div class="row">
 | 
			
		||||
	<Speaker src={rearLeftUrl} left>{m.audio_channel_rearLeft()}</Speaker>
 | 
			
		||||
	<Speaker src={rearRightUrl} right>{m.audio_channel_rearRight()}</Speaker>
 | 
			
		||||
	<Speaker src={rearLeftUrl} left>Rear Left</Speaker>
 | 
			
		||||
	<Speaker src={rearRightUrl} right>Rear Right</Speaker>
 | 
			
		||||
</div>
 | 
			
		||||
<Speaker src={LfeUrl} lfe>{m.audio_channel_lfe()}</Speaker>
 | 
			
		||||
<Speaker src={LfeUrl} lfe>LFE</Speaker>
 | 
			
		||||
 | 
			
		||||
<div class="label">5.1</div>
 | 
			
		||||
| 
						 | 
				
			
			@ -8,25 +8,24 @@
 | 
			
		|||
	import rearLeftUrl from '@assets/audio/7.1/Rear_Left.mp3';
 | 
			
		||||
	import rearRightUrl from '@assets/audio/7.1/Rear_Right.mp3';
 | 
			
		||||
	import LfeUrl from '@assets/audio/7.1/LFE_Noise.mp3';
 | 
			
		||||
	import { m } from '$lib/paraglide/messages';
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<div class="row">
 | 
			
		||||
	<Speaker src={frontLeftUrl} left>{m.audio_channel_frontLeft()}</Speaker>
 | 
			
		||||
	<Speaker src={frontLeftUrl} left>Front Left</Speaker>
 | 
			
		||||
	<div class="center">
 | 
			
		||||
		<Speaker src={frontCenterUrl} center>{m.audio_channel_frontCenter()}</Speaker>
 | 
			
		||||
		<Speaker src={frontCenterUrl} center>Front Center</Speaker>
 | 
			
		||||
	</div>
 | 
			
		||||
	<Speaker src={frontRightUrl} right>{m.audio_channel_frontRight()}</Speaker>
 | 
			
		||||
	<Speaker src={frontRightUrl} right>Front Right</Speaker>
 | 
			
		||||
</div>
 | 
			
		||||
<div class="row">
 | 
			
		||||
	<Speaker src={sideLeftUrl} left>{m.audio_channel_sideLeft()}</Speaker>
 | 
			
		||||
	<Speaker src={sideRightUrl} right>{m.audio_channel_sideRight()}</Speaker>
 | 
			
		||||
	<Speaker src={sideLeftUrl} left>Side Left</Speaker>
 | 
			
		||||
	<Speaker src={sideRightUrl} right>Side Right</Speaker>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<div class="row">
 | 
			
		||||
	<Speaker src={rearLeftUrl} left>{m.audio_channel_rearLeft()}</Speaker>
 | 
			
		||||
	<Speaker src={rearRightUrl} right>{m.audio_channel_rearRight()}</Speaker>
 | 
			
		||||
	<Speaker src={rearLeftUrl} left>Rear Left</Speaker>
 | 
			
		||||
	<Speaker src={rearRightUrl} right>Rear Right</Speaker>
 | 
			
		||||
</div>
 | 
			
		||||
<Speaker src={LfeUrl} lfe>{m.audio_channel_lfe()}</Speaker>
 | 
			
		||||
<Speaker src={LfeUrl} lfe>LFE</Speaker>
 | 
			
		||||
 | 
			
		||||
<div class="label">7.1</div>
 | 
			
		||||
| 
						 | 
				
			
			@ -1,14 +1,9 @@
 | 
			
		|||
<script lang="ts">
 | 
			
		||||
	import { onDestroy } from 'svelte';
 | 
			
		||||
	import { m } from '$lib/paraglide/messages';
 | 
			
		||||
 | 
			
		||||
	interface Props {
 | 
			
		||||
		element: HTMLElement;
 | 
			
		||||
	}
 | 
			
		||||
	export let element: HTMLElement;
 | 
			
		||||
 | 
			
		||||
	let { element }: Props = $props();
 | 
			
		||||
 | 
			
		||||
	let cycling = $state(false);
 | 
			
		||||
	let cycling = false;
 | 
			
		||||
	let currentChannel: HTMLAudioElement | undefined;
 | 
			
		||||
	async function cycleChannels() {
 | 
			
		||||
		cycling = true;
 | 
			
		||||
| 
						 | 
				
			
			@ -48,11 +43,11 @@
 | 
			
		|||
	});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<button onclick={onClick}>
 | 
			
		||||
<button on:click={onClick}>
 | 
			
		||||
	<i class="ti ti-refresh"></i>
 | 
			
		||||
	{#if cycling}
 | 
			
		||||
		{m.audio_stopCycling()}
 | 
			
		||||
		Stop Cycling
 | 
			
		||||
	{:else}
 | 
			
		||||
		{m.audio_cycleThrough()}
 | 
			
		||||
		Cycle through
 | 
			
		||||
	{/if}
 | 
			
		||||
</button>
 | 
			
		||||
| 
						 | 
				
			
			@ -1,26 +1,13 @@
 | 
			
		|||
<script lang="ts">
 | 
			
		||||
	interface Props {
 | 
			
		||||
		src: string;
 | 
			
		||||
		left?: boolean;
 | 
			
		||||
		center?: boolean;
 | 
			
		||||
		right?: boolean;
 | 
			
		||||
		lfe?: boolean;
 | 
			
		||||
		inline?: boolean;
 | 
			
		||||
		children?: import('svelte').Snippet;
 | 
			
		||||
	}
 | 
			
		||||
	export let src: string;
 | 
			
		||||
	export let left = false;
 | 
			
		||||
	export let center = false;
 | 
			
		||||
	export let right = false;
 | 
			
		||||
	export let lfe = false;
 | 
			
		||||
	export let inline = false;
 | 
			
		||||
 | 
			
		||||
	let {
 | 
			
		||||
		src,
 | 
			
		||||
		left = false,
 | 
			
		||||
		center = false,
 | 
			
		||||
		right = false,
 | 
			
		||||
		lfe = false,
 | 
			
		||||
		inline = false,
 | 
			
		||||
		children
 | 
			
		||||
	}: Props = $props();
 | 
			
		||||
 | 
			
		||||
	let currentTime = $state(0);
 | 
			
		||||
	let paused = $state(true);
 | 
			
		||||
	let currentTime = 0;
 | 
			
		||||
	let paused = true;
 | 
			
		||||
	function play() {
 | 
			
		||||
		currentTime = 0;
 | 
			
		||||
		paused = false;
 | 
			
		||||
| 
						 | 
				
			
			@ -35,14 +22,14 @@
 | 
			
		|||
	class:lfe
 | 
			
		||||
	class:inline
 | 
			
		||||
	class:playing={!paused}
 | 
			
		||||
	onclick={play}
 | 
			
		||||
	on:click={play}
 | 
			
		||||
>
 | 
			
		||||
	{#if !lfe}
 | 
			
		||||
		<i class="ti ti-volume"></i>
 | 
			
		||||
	{:else}
 | 
			
		||||
		<i class="ti ti-wave-sine"></i>
 | 
			
		||||
	{/if}
 | 
			
		||||
	<span>{@render children?.()}</span>
 | 
			
		||||
	<span><slot /></span>
 | 
			
		||||
	<audio bind:currentTime bind:paused {src}></audio>
 | 
			
		||||
</button>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -4,16 +4,15 @@
 | 
			
		|||
	import rightUrl from '@assets/audio/stereo/Right.mp3';
 | 
			
		||||
	import Speaker from './speaker.svelte';
 | 
			
		||||
	import CycleButton from './cycle-button.svelte';
 | 
			
		||||
	import { m } from '$lib/paraglide/messages';
 | 
			
		||||
 | 
			
		||||
	let speakersEl: HTMLElement | undefined = $state();
 | 
			
		||||
	let speakersEl: HTMLElement;
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<div class="test">
 | 
			
		||||
	<div class="speakers" bind:this={speakersEl}>
 | 
			
		||||
		<Speaker src={leftUrl} left inline>{m.audio_channel_left()}</Speaker>
 | 
			
		||||
		<Speaker src={centerUrl} center inline>{m.audio_channel_center()}</Speaker>
 | 
			
		||||
		<Speaker src={rightUrl} right inline>{m.audio_channel_right()}</Speaker>
 | 
			
		||||
		<Speaker src={leftUrl} left inline>Left</Speaker>
 | 
			
		||||
		<Speaker src={centerUrl} center inline>Center</Speaker>
 | 
			
		||||
		<Speaker src={rightUrl} right inline>Right</Speaker>
 | 
			
		||||
	</div>
 | 
			
		||||
	<CycleButton element={speakersEl} />
 | 
			
		||||
</div>
 | 
			
		||||
							
								
								
									
										5
									
								
								src/routes/audio/+layout.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								src/routes/audio/+layout.svelte
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,5 @@
 | 
			
		|||
<script lang="ts">
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<h2><i class="ti ti-volume"></i> Audio test</h2>
 | 
			
		||||
<slot />
 | 
			
		||||
							
								
								
									
										1
									
								
								src/routes/audio/+layout.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/routes/audio/+layout.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1 @@
 | 
			
		|||
export const trailingSlash = 'always';
 | 
			
		||||
							
								
								
									
										37
									
								
								src/routes/audio/+page.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								src/routes/audio/+page.svelte
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,37 @@
 | 
			
		|||
<script lang="ts">
 | 
			
		||||
	import StereoTest from './(channels)/stereo-test.svelte';
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<article>
 | 
			
		||||
	<h3>Channel tests</h3>
 | 
			
		||||
	<h4>Stereo</h4>
 | 
			
		||||
	<section>
 | 
			
		||||
		<StereoTest />
 | 
			
		||||
	</section>
 | 
			
		||||
	<h4>Surround audio</h4>
 | 
			
		||||
	<section>
 | 
			
		||||
		<ul>
 | 
			
		||||
			<li><a class="button" href="channels-5.1">5.1 Surround</a></li>
 | 
			
		||||
			<li><a class="button" href="channels-7.1">7.1 Surround</a></li>
 | 
			
		||||
		</ul>
 | 
			
		||||
	</section>
 | 
			
		||||
</article>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
	h4 {
 | 
			
		||||
		margin-bottom: 0;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ul {
 | 
			
		||||
		list-style-type: none;
 | 
			
		||||
		padding: 0;
 | 
			
		||||
		margin: 0;
 | 
			
		||||
 | 
			
		||||
		display: inline-flex;
 | 
			
		||||
		gap: 1em;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	section {
 | 
			
		||||
		margin: 1em 0;
 | 
			
		||||
	}
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			@ -1,24 +1,22 @@
 | 
			
		|||
<script lang="ts">
 | 
			
		||||
	import videoUrl from '@assets/avsync.webm';
 | 
			
		||||
	import { m } from '$lib/paraglide/messages';
 | 
			
		||||
	let paused = $state(true);
 | 
			
		||||
	let paused = true;
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<h2><i class="ti ti-time-duration-off"></i> {m.avSync_title()}</h2>
 | 
			
		||||
<!-- svelte-ignore a11y_media_has_caption -->
 | 
			
		||||
<h2><i class="ti ti-time-duration-off"></i> Audio/Video Synchronization</h2>
 | 
			
		||||
<!-- svelte-ignore a11y-media-has-caption -->
 | 
			
		||||
<video
 | 
			
		||||
	class:playing={!paused}
 | 
			
		||||
	autoplay
 | 
			
		||||
	loop
 | 
			
		||||
	bind:paused
 | 
			
		||||
	src={videoUrl}
 | 
			
		||||
	onclick={() => (paused = false)}
 | 
			
		||||
	on:click={() => (paused = false)}
 | 
			
		||||
></video>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
	video {
 | 
			
		||||
		flex-grow: 1;
 | 
			
		||||
		min-height: 0;
 | 
			
		||||
 | 
			
		||||
		&:not(.playing) {
 | 
			
		||||
			opacity: 0.5;
 | 
			
		||||
| 
						 | 
				
			
			@ -2,32 +2,27 @@
 | 
			
		|||
	import { onDestroy, onMount } from 'svelte';
 | 
			
		||||
	import { browser } from '$app/environment';
 | 
			
		||||
	import debug from 'debug';
 | 
			
		||||
	import { m } from '$lib/paraglide/messages';
 | 
			
		||||
	const dbg = debug('app:camera');
 | 
			
		||||
 | 
			
		||||
	let video: HTMLVideoElement | undefined = $state();
 | 
			
		||||
	let devices: MediaDeviceInfo[] = $state([]);
 | 
			
		||||
	let currentDevice: string | undefined = $state();
 | 
			
		||||
	let video: HTMLVideoElement;
 | 
			
		||||
	let devices: MediaDeviceInfo[] = [];
 | 
			
		||||
	let currentDevice: string | undefined;
 | 
			
		||||
 | 
			
		||||
	let requestResolution: [number, number] | 'auto' = $state('auto');
 | 
			
		||||
	let requestFramerate: number | 'auto' = $state('auto');
 | 
			
		||||
	let requestResolution: [number, number] | 'auto' = 'auto';
 | 
			
		||||
	let requestFramerate: number | 'auto' = 'auto';
 | 
			
		||||
	let deviceInfo: {
 | 
			
		||||
		resolution?: string;
 | 
			
		||||
		frameRate?: number;
 | 
			
		||||
	} = $state({});
 | 
			
		||||
	let snapshot: string | undefined = $state();
 | 
			
		||||
	let flipped = $state(false);
 | 
			
		||||
	} = {};
 | 
			
		||||
	let snapshot: string | undefined;
 | 
			
		||||
	let flipped = false;
 | 
			
		||||
 | 
			
		||||
	$effect(() => {
 | 
			
		||||
		dbg('devices %O', devices);
 | 
			
		||||
	});
 | 
			
		||||
	$effect(() => {
 | 
			
		||||
		dbg('currentDevice %s', currentDevice);
 | 
			
		||||
	});
 | 
			
		||||
	$: dbg('devices %O', devices);
 | 
			
		||||
	$: dbg('currentDevice %s', currentDevice);
 | 
			
		||||
 | 
			
		||||
	onMount(() => {
 | 
			
		||||
		refreshDevices();
 | 
			
		||||
		video?.addEventListener('playing', () => {
 | 
			
		||||
		video.addEventListener('playing', () => {
 | 
			
		||||
			if (browser && video?.srcObject instanceof MediaStream) {
 | 
			
		||||
				deviceInfo = {
 | 
			
		||||
					resolution: `${video.videoWidth}x${video.videoHeight}`,
 | 
			
		||||
| 
						 | 
				
			
			@ -52,27 +47,23 @@
 | 
			
		|||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	$effect(() => {
 | 
			
		||||
		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) => {
 | 
			
		||||
					if (!video) return;
 | 
			
		||||
					video.srcObject = stream;
 | 
			
		||||
					refreshDevices();
 | 
			
		||||
				});
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
	$: 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() {
 | 
			
		||||
		if (!video) return;
 | 
			
		||||
		const canvas = document.createElement('canvas');
 | 
			
		||||
		canvas.width = video.videoWidth;
 | 
			
		||||
		canvas.height = video.videoHeight;
 | 
			
		||||
| 
						 | 
				
			
			@ -87,26 +78,26 @@
 | 
			
		|||
	}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<h2><i class="ti ti-camera"></i> {m.camera_title()}</h2>
 | 
			
		||||
<h2><i class="ti ti-camera"></i> Camera test</h2>
 | 
			
		||||
 | 
			
		||||
<div class="controls">
 | 
			
		||||
	<label>
 | 
			
		||||
		{m.camera_device()}
 | 
			
		||||
		Device
 | 
			
		||||
		<select bind:value={currentDevice} disabled={!devices.length}>
 | 
			
		||||
			{#each devices as device}
 | 
			
		||||
				<option value={device.deviceId}>{device.label || '???'}</option>
 | 
			
		||||
			{:else}
 | 
			
		||||
				<option>{m.camera_noCameraFound()}</option>
 | 
			
		||||
				<option>No camera found</option>
 | 
			
		||||
			{/each}
 | 
			
		||||
		</select>
 | 
			
		||||
	</label>
 | 
			
		||||
	<button onclick={refreshDevices}>
 | 
			
		||||
	<button on:click={refreshDevices}>
 | 
			
		||||
		<i class="ti ti-refresh"></i>
 | 
			
		||||
		{m.camera_refresh()}
 | 
			
		||||
		Refresh
 | 
			
		||||
	</button>
 | 
			
		||||
	<div class="separator"></div>
 | 
			
		||||
	<label>
 | 
			
		||||
		{m.camera_resolution()}
 | 
			
		||||
		Resolution
 | 
			
		||||
		<select bind:value={requestResolution}>
 | 
			
		||||
			<option value="auto">Auto</option>
 | 
			
		||||
			<option value={[4096, 2160]}>4096x2160</option>
 | 
			
		||||
| 
						 | 
				
			
			@ -118,7 +109,7 @@
 | 
			
		|||
		</select>
 | 
			
		||||
	</label>
 | 
			
		||||
	<label>
 | 
			
		||||
		{m.camera_frameRate()}
 | 
			
		||||
		Frame rate
 | 
			
		||||
		<select bind:value={requestFramerate}>
 | 
			
		||||
			<option value="auto">Auto</option>
 | 
			
		||||
			<option value={120}>120 fps</option>
 | 
			
		||||
| 
						 | 
				
			
			@ -132,43 +123,41 @@
 | 
			
		|||
</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 onclick={() => (snapshot = undefined)} aria-label={m.camera_closeSnapshot()}
 | 
			
		||||
			><i class="ti ti-x"></i></button
 | 
			
		||||
		>
 | 
			
		||||
		<button on:click={() => (snapshot = undefined)}><i class="ti ti-x"></i></button>
 | 
			
		||||
	{/if}
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<footer>
 | 
			
		||||
	{#if !currentDevice}
 | 
			
		||||
		<span class="subdued">{m.camera_noCameraSelected()}</span>
 | 
			
		||||
		<span class="subdued">No camera selected</span>
 | 
			
		||||
	{:else}
 | 
			
		||||
		<ul>
 | 
			
		||||
			{#key currentDevice}
 | 
			
		||||
				<li>
 | 
			
		||||
					{m.camera_resolution()}: <strong>{deviceInfo.resolution || '???'}</strong>
 | 
			
		||||
					Resolution: <strong>{deviceInfo.resolution || '???'}</strong>
 | 
			
		||||
				</li>
 | 
			
		||||
				<li>
 | 
			
		||||
					{m.camera_frameRate()}: <strong>{deviceInfo.frameRate || '???'}</strong>
 | 
			
		||||
					Frame rate: <strong>{deviceInfo.frameRate || '???'}</strong>
 | 
			
		||||
				</li>
 | 
			
		||||
			{/key}
 | 
			
		||||
		</ul>
 | 
			
		||||
		<div class="controls">
 | 
			
		||||
			<button onclick={takeSnapshot}>
 | 
			
		||||
			<button on:click={takeSnapshot}>
 | 
			
		||||
				<i class="ti ti-camera"></i>
 | 
			
		||||
				{m.camera_takePicture()}
 | 
			
		||||
				Take picture
 | 
			
		||||
			</button>
 | 
			
		||||
			<button onclick={() => (flipped = !flipped)}>
 | 
			
		||||
			<button on:click={() => (flipped = !flipped)}>
 | 
			
		||||
				<i class="ti ti-flip-vertical"></i>
 | 
			
		||||
				{#if flipped}
 | 
			
		||||
					{m.camera_unflipImage()}
 | 
			
		||||
					Unflip image
 | 
			
		||||
				{:else}
 | 
			
		||||
					{m.camera_flipImage()}
 | 
			
		||||
					Flip image
 | 
			
		||||
				{/if}
 | 
			
		||||
			</button>
 | 
			
		||||
		</div>
 | 
			
		||||
| 
						 | 
				
			
			@ -1,14 +1,7 @@
 | 
			
		|||
<script lang="ts">
 | 
			
		||||
	import { m } from '$lib/paraglide/messages';
 | 
			
		||||
	interface Props {
 | 
			
		||||
		children?: import('svelte').Snippet;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	let { children }: Props = $props();
 | 
			
		||||
<script>
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<a href=".." class="hide-idle"><i class="ti ti-arrow-back"></i> {m.common_back()}</a>
 | 
			
		||||
{@render children?.()}
 | 
			
		||||
<a href="/" class="hide-idle"><i class="ti ti-arrow-back"></i> Back</a>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
	a {
 | 
			
		||||
| 
						 | 
				
			
			@ -28,7 +21,5 @@
 | 
			
		|||
		align-items: center;
 | 
			
		||||
 | 
			
		||||
		text-decoration: none;
 | 
			
		||||
 | 
			
		||||
		z-index: 99;
 | 
			
		||||
	}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										7
									
								
								src/routes/card/+page.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/routes/card/+page.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,7 @@
 | 
			
		|||
import type { PageLoad } from './$types';
 | 
			
		||||
 | 
			
		||||
export const load: PageLoad = () => {
 | 
			
		||||
	return {
 | 
			
		||||
		card: true
 | 
			
		||||
	};
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -2,79 +2,29 @@
 | 
			
		|||
	import { onMount } from 'svelte';
 | 
			
		||||
	import { browser } from '$app/environment';
 | 
			
		||||
	import debug from 'debug';
 | 
			
		||||
	import { m } from '$lib/paraglide/messages';
 | 
			
		||||
 | 
			
		||||
	const dbg = debug('app:camera');
 | 
			
		||||
 | 
			
		||||
	let gamepads: Gamepad[] = $state([]);
 | 
			
		||||
	let currentGamepad: Gamepad | undefined = $state();
 | 
			
		||||
	let buttons: GamepadButton[] = $state([]);
 | 
			
		||||
	let axes: number[] = $state([]);
 | 
			
		||||
	let gamepads: Gamepad[] = [];
 | 
			
		||||
	let currentGamepad: Gamepad | undefined;
 | 
			
		||||
	let buttons: GamepadButton[] = [];
 | 
			
		||||
	let axes: number[] = [];
 | 
			
		||||
 | 
			
		||||
	const axisHistory: number[][] = [];
 | 
			
		||||
	const sizes: [number, number][] = [];
 | 
			
		||||
	const contexts: CanvasRenderingContext2D[] = [];
 | 
			
		||||
 | 
			
		||||
	function update() {
 | 
			
		||||
		buttons = currentGamepad?.buttons.concat() || [];
 | 
			
		||||
		axes = currentGamepad?.axes.concat() || [];
 | 
			
		||||
 | 
			
		||||
		axisHistory.push(axes);
 | 
			
		||||
		if (axisHistory.length > 1024) {
 | 
			
		||||
			axisHistory.shift();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		for (let i = 0; i < axes.length; i++) {
 | 
			
		||||
			if (!contexts[i]) {
 | 
			
		||||
				const canvas = document.querySelector(`canvas[data-axis="${i}"]`) as HTMLCanvasElement;
 | 
			
		||||
				if (!canvas) continue;
 | 
			
		||||
				if (!canvas.checkVisibility()) continue;
 | 
			
		||||
				contexts[i] = canvas.getContext('2d') as CanvasRenderingContext2D;
 | 
			
		||||
				sizes[i] = [canvas.width, canvas.height];
 | 
			
		||||
			}
 | 
			
		||||
			const ctx = contexts[i];
 | 
			
		||||
			if (!ctx) continue;
 | 
			
		||||
 | 
			
		||||
			const [width, height] = sizes[i];
 | 
			
		||||
 | 
			
		||||
			ctx.clearRect(0, 0, width, height);
 | 
			
		||||
			ctx.strokeStyle = `rgba(255, 0, 0, 0.5)`;
 | 
			
		||||
			ctx.beginPath();
 | 
			
		||||
			ctx.moveTo(0, height / 2);
 | 
			
		||||
			ctx.lineTo(width, height / 2);
 | 
			
		||||
			ctx.stroke();
 | 
			
		||||
 | 
			
		||||
			ctx.strokeStyle = 'white';
 | 
			
		||||
			ctx.beginPath();
 | 
			
		||||
			ctx.moveTo(width - axisHistory.length, height / 2);
 | 
			
		||||
			for (let j = 0; j < axisHistory.length; j++) {
 | 
			
		||||
				const x = width - axisHistory.length + j;
 | 
			
		||||
				const y = ((axisHistory[j][i] + 1) * (height - 2)) / 2 + 1;
 | 
			
		||||
				ctx.lineTo(x, y);
 | 
			
		||||
			}
 | 
			
		||||
			ctx.stroke();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		requestAnimationFrame(update);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	$effect(() => {
 | 
			
		||||
	$: {
 | 
			
		||||
		if (currentGamepad) {
 | 
			
		||||
			function update() {
 | 
			
		||||
				buttons = currentGamepad?.buttons.concat() || [];
 | 
			
		||||
				axes = currentGamepad?.axes.concat() || [];
 | 
			
		||||
				requestAnimationFrame(update);
 | 
			
		||||
			}
 | 
			
		||||
			update();
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	$effect(() => {
 | 
			
		||||
		dbg('Gamepads %O', gamepads);
 | 
			
		||||
	});
 | 
			
		||||
	$effect(() => {
 | 
			
		||||
		dbg('Current gamepad %s', currentGamepad);
 | 
			
		||||
	});
 | 
			
		||||
	$: dbg('Gamepads %O', gamepads);
 | 
			
		||||
	$: dbg('Current gamepad %s', currentGamepad);
 | 
			
		||||
 | 
			
		||||
	$effect(() => {
 | 
			
		||||
		currentGamepad?.vibrationActuator?.playEffect('dual-rumble', {
 | 
			
		||||
			duration: 1000
 | 
			
		||||
		});
 | 
			
		||||
	$: currentGamepad?.vibrationActuator?.playEffect('dual-rumble', {
 | 
			
		||||
		duration: 1000
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	onMount(() => {
 | 
			
		||||
| 
						 | 
				
			
			@ -99,27 +49,27 @@
 | 
			
		|||
	});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<h2><i class="ti ti-device-gamepad"></i> {m.gamepad_title()}</h2>
 | 
			
		||||
<h2><i class="ti ti-device-gamepad"></i> Gamepad & Joystick Tests</h2>
 | 
			
		||||
<div class="controls">
 | 
			
		||||
	<label>
 | 
			
		||||
		{m.gamepad_device()}
 | 
			
		||||
		Device
 | 
			
		||||
		<select disabled={!gamepads.length}>
 | 
			
		||||
			{#each gamepads as gamepad}
 | 
			
		||||
				<option value={gamepad.index}>{gamepad.id}</option>
 | 
			
		||||
			{:else}
 | 
			
		||||
				<option>{m.gamepad_noGamepadsDetected()}</option>
 | 
			
		||||
				<option>No gamepads detected. (Try pressing a button)</option>
 | 
			
		||||
			{/each}
 | 
			
		||||
		</select>
 | 
			
		||||
	</label>
 | 
			
		||||
	<button onclick={refreshGamepads}>
 | 
			
		||||
	<button on:click={refreshGamepads}>
 | 
			
		||||
		<i class="ti ti-refresh"></i>
 | 
			
		||||
		{m.gamepad_refresh()}
 | 
			
		||||
		Refresh
 | 
			
		||||
	</button>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
{#if currentGamepad}
 | 
			
		||||
	<section>
 | 
			
		||||
		<h3>{m.gamepad_buttons()}</h3>
 | 
			
		||||
		<h3>Buttons</h3>
 | 
			
		||||
		<ul class="buttons">
 | 
			
		||||
			{#each buttons as button, i}
 | 
			
		||||
				<li class:pressed={button.pressed}>{i}</li>
 | 
			
		||||
| 
						 | 
				
			
			@ -127,7 +77,7 @@
 | 
			
		|||
		</ul>
 | 
			
		||||
	</section>
 | 
			
		||||
	<section>
 | 
			
		||||
		<h3>{m.gamepad_axes()}</h3>
 | 
			
		||||
		<h3>Axes</h3>
 | 
			
		||||
		<div class="axes">
 | 
			
		||||
			{#each axes as axis, i (i)}
 | 
			
		||||
				<div class="axis">
 | 
			
		||||
| 
						 | 
				
			
			@ -136,10 +86,6 @@
 | 
			
		|||
						<progress value={axis + 1} max="2"></progress>
 | 
			
		||||
						<span>{axis.toFixed(2)}</span>
 | 
			
		||||
					</div>
 | 
			
		||||
					<details>
 | 
			
		||||
						<summary>{m.gamepad_history()}</summary>
 | 
			
		||||
						<canvas width="512" height="128" data-axis={i}></canvas>
 | 
			
		||||
					</details>
 | 
			
		||||
				</div>
 | 
			
		||||
			{/each}
 | 
			
		||||
		</div>
 | 
			
		||||
| 
						 | 
				
			
			@ -214,9 +160,4 @@
 | 
			
		|||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	canvas {
 | 
			
		||||
		background: black;
 | 
			
		||||
		width: 100%;
 | 
			
		||||
	}
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			@ -1,10 +1,9 @@
 | 
			
		|||
<script lang="ts">
 | 
			
		||||
	import { onMount } from 'svelte';
 | 
			
		||||
	import { m } from '$lib/paraglide/messages';
 | 
			
		||||
 | 
			
		||||
	let key: string | undefined = $state();
 | 
			
		||||
	let code: string | undefined = $state();
 | 
			
		||||
	let pressedKeys: string[] = $state([]);
 | 
			
		||||
	let key: string;
 | 
			
		||||
	let code: string;
 | 
			
		||||
	let pressedKeys: string[] = [];
 | 
			
		||||
	onMount(() => {
 | 
			
		||||
		document.addEventListener('keydown', (event) => {
 | 
			
		||||
			key = event.key;
 | 
			
		||||
| 
						 | 
				
			
			@ -15,8 +14,8 @@
 | 
			
		|||
	});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<h2>{m.keyboard_title()}</h2>
 | 
			
		||||
<p>{m.keyboard_instruction()}</p>
 | 
			
		||||
<h2>Keyboard testing</h2>
 | 
			
		||||
<p>Press a key on the keyboard to see the event object and the key code.</p>
 | 
			
		||||
<div class="current">
 | 
			
		||||
	{#if key}
 | 
			
		||||
		<span>{key}</span>
 | 
			
		||||
| 
						 | 
				
			
			@ -26,7 +25,7 @@
 | 
			
		|||
	{/if}
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<p>{m.keyboard_pressedKeys()}</p>
 | 
			
		||||
<p>Pressed keys:</p>
 | 
			
		||||
<ul>
 | 
			
		||||
	{#each pressedKeys as key}
 | 
			
		||||
		<li>{key}</li>
 | 
			
		||||
							
								
								
									
										0
									
								
								src/routes/microphone/+page.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								src/routes/microphone/+page.svelte
									
										
									
									
									
										Normal file
									
								
							
							
								
								
									
										0
									
								
								src/routes/mouse/+page.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								src/routes/mouse/+page.svelte
									
										
									
									
									
										Normal file
									
								
							
							
								
								
									
										0
									
								
								src/routes/sensors/+page.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								src/routes/sensors/+page.svelte
									
										
									
									
									
										Normal file
									
								
							
							
								
								
									
										0
									
								
								src/routes/video/+page.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								src/routes/video/+page.svelte
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 Before Width: | Height: | Size: 316 KiB  | 
							
								
								
									
										0
									
								
								style.css
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								style.css
									
										
									
									
									
										Normal file
									
								
							
							
								
								
									
										
											BIN
										
									
								
								tests/output/testcard-baseline.png
									 (Stored with Git LFS)
										
									
									
									
								
							
							
						
						
									
										
											BIN
										
									
								
								tests/output/testcard-baseline.png
									 (Stored with Git LFS)
										
									
									
									
								
							
										
											Binary file not shown.
										
									
								
							| 
						 | 
				
			
			@ -1,86 +0,0 @@
 | 
			
		|||
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
 | 
			
		||||
import puppeteer, { type Browser, type Page } from 'puppeteer';
 | 
			
		||||
import fs from 'fs';
 | 
			
		||||
import { PNG } from 'pngjs';
 | 
			
		||||
import pixelmatch from 'pixelmatch';
 | 
			
		||||
import { startDevServer, stopDevServer } from './utils';
 | 
			
		||||
 | 
			
		||||
let browser: Browser;
 | 
			
		||||
let page: Page;
 | 
			
		||||
 | 
			
		||||
const baseTestPath = 'tests/output';
 | 
			
		||||
const screenshotPath = `${baseTestPath}/testcard-current.png`;
 | 
			
		||||
const baselinePath = `${baseTestPath}/testcard-baseline.png`;
 | 
			
		||||
const diffPath = `${baseTestPath}/testcard-diff.png`;
 | 
			
		||||
 | 
			
		||||
describe('Test Card', () => {
 | 
			
		||||
	beforeAll(async () => {
 | 
			
		||||
		await startDevServer(); // boot SvelteKit dev server
 | 
			
		||||
		browser = await puppeteer.launch();
 | 
			
		||||
		page = await browser.newPage();
 | 
			
		||||
		await page.goto('http://localhost:5888/card');
 | 
			
		||||
		await page.waitForNetworkIdle();
 | 
			
		||||
		await page.addStyleTag({
 | 
			
		||||
			content: '.clock { opacity: 0; } * { transition: none !important; }'
 | 
			
		||||
		});
 | 
			
		||||
		await page.setViewport({ width: 1920, height: 1080 });
 | 
			
		||||
		await page.evaluate(() => {
 | 
			
		||||
			return new Promise((resolve) => {
 | 
			
		||||
				requestAnimationFrame(() => {
 | 
			
		||||
					requestAnimationFrame(resolve);
 | 
			
		||||
				});
 | 
			
		||||
			});
 | 
			
		||||
		});
 | 
			
		||||
		await fs.promises.mkdir(baseTestPath, { recursive: true });
 | 
			
		||||
		await page.screenshot({ path: screenshotPath });
 | 
			
		||||
	}, 60000);
 | 
			
		||||
 | 
			
		||||
	afterAll(async () => {
 | 
			
		||||
		await browser.close();
 | 
			
		||||
		await stopDevServer();
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	it('matches baseline (visual regression)', () => {
 | 
			
		||||
		if (!fs.existsSync(baselinePath)) {
 | 
			
		||||
			fs.copyFileSync(screenshotPath, baselinePath);
 | 
			
		||||
			console.log('Baseline image created. Re-run tests.');
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const img1 = PNG.sync.read(fs.readFileSync(baselinePath));
 | 
			
		||||
		const img2 = PNG.sync.read(fs.readFileSync(screenshotPath));
 | 
			
		||||
		const { width, height } = img1;
 | 
			
		||||
 | 
			
		||||
		expect(img2.width).toBe(width);
 | 
			
		||||
		expect(img2.height).toBe(height);
 | 
			
		||||
 | 
			
		||||
		const diff = new PNG({ width, height });
 | 
			
		||||
		const mismatches = pixelmatch(img1.data, img2.data, diff.data, width, height, {
 | 
			
		||||
			threshold: 0.1
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		if (mismatches > 0) {
 | 
			
		||||
			fs.writeFileSync(diffPath, PNG.sync.write(diff));
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		expect(mismatches).toBe(0);
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	// it('has a black vertical line exactly in the center', () => {
 | 
			
		||||
	// 	const img = PNG.sync.read(fs.readFileSync(screenshotPath));
 | 
			
		||||
	// 	const { width, height, data } = img;
 | 
			
		||||
	// 	const midX = Math.floor(width / 2);
 | 
			
		||||
 | 
			
		||||
	// 	function getPixel(x: number, y: number) {
 | 
			
		||||
	// 		const idx = (width * y + x) << 2;
 | 
			
		||||
	// 		return { r: data[idx], g: data[idx + 1], b: data[idx + 2] };
 | 
			
		||||
	// 	}
 | 
			
		||||
 | 
			
		||||
	// 	for (let y = 0; y < height; y++) {
 | 
			
		||||
	// 		const { r, g, b } = getPixel(midX, y);
 | 
			
		||||
	// 		expect(r).toBeLessThan(20);
 | 
			
		||||
	// 		expect(g).toBeLessThan(20);
 | 
			
		||||
	// 		expect(b).toBeLessThan(20);
 | 
			
		||||
	// 	}
 | 
			
		||||
	// });
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -1,42 +0,0 @@
 | 
			
		|||
import { spawn, type ChildProcess } from 'child_process';
 | 
			
		||||
import waitOn from 'wait-on';
 | 
			
		||||
import debug from 'debug';
 | 
			
		||||
const dbg = debug('test-card:utils');
 | 
			
		||||
 | 
			
		||||
const SERVER_PORT = 5888;
 | 
			
		||||
 | 
			
		||||
let devServer: ChildProcess;
 | 
			
		||||
export async function startDevServer(): Promise<void> {
 | 
			
		||||
	return new Promise((resolve, reject) => {
 | 
			
		||||
		dbg('Starting dev server on http://localhost:%d...', SERVER_PORT);
 | 
			
		||||
		devServer = spawn(
 | 
			
		||||
			'npm',
 | 
			
		||||
			['run', 'dev', '--', '--host', 'localhost', '--port', String(SERVER_PORT)],
 | 
			
		||||
			{
 | 
			
		||||
				stdio: 'pipe',
 | 
			
		||||
				shell: true
 | 
			
		||||
			}
 | 
			
		||||
		);
 | 
			
		||||
 | 
			
		||||
		// Wait until SvelteKit dev server responds
 | 
			
		||||
		waitOn(
 | 
			
		||||
			{
 | 
			
		||||
				resources: [`http://localhost:${SERVER_PORT}`],
 | 
			
		||||
				timeout: 30000
 | 
			
		||||
			},
 | 
			
		||||
			(err) => {
 | 
			
		||||
				if (err) reject(err);
 | 
			
		||||
				else resolve();
 | 
			
		||||
			}
 | 
			
		||||
		);
 | 
			
		||||
 | 
			
		||||
		dbg('Dev server started');
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function stopDevServer(): Promise<void> {
 | 
			
		||||
	if (devServer) {
 | 
			
		||||
		dbg('Stopping dev server...');
 | 
			
		||||
		devServer.kill('SIGTERM');
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,16 +1,9 @@
 | 
			
		|||
import { paraglideVitePlugin } from '@inlang/paraglide-js';
 | 
			
		||||
import { sveltekit } from '@sveltejs/kit/vite';
 | 
			
		||||
import { defineConfig } from 'vite';
 | 
			
		||||
import * as path from 'path';
 | 
			
		||||
 | 
			
		||||
export default defineConfig({
 | 
			
		||||
	plugins: [
 | 
			
		||||
		sveltekit(),
 | 
			
		||||
		paraglideVitePlugin({
 | 
			
		||||
			project: './project.inlang',
 | 
			
		||||
			outdir: './src/lib/paraglide'
 | 
			
		||||
		})
 | 
			
		||||
	],
 | 
			
		||||
	plugins: [sveltekit()],
 | 
			
		||||
	resolve: {
 | 
			
		||||
		alias: {
 | 
			
		||||
			'@assets': path.join(__dirname, 'assets/generated')
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		
		Reference in a new issue