fix: linux display main page

This commit is contained in:
2026-09-08 21:57:38 +02:00
parent ac23f98fcd
commit 8f5a7c22cc
14 changed files with 442 additions and 195 deletions
+34 -88
View File
@@ -1097,9 +1097,6 @@
"arm" "arm"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1114,9 +1111,6 @@
"arm" "arm"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1131,9 +1125,6 @@
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1148,9 +1139,6 @@
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1165,9 +1153,6 @@
"loong64" "loong64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1182,9 +1167,6 @@
"loong64" "loong64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1199,9 +1181,6 @@
"ppc64" "ppc64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1216,9 +1195,6 @@
"ppc64" "ppc64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1233,9 +1209,6 @@
"riscv64" "riscv64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1250,9 +1223,6 @@
"riscv64" "riscv64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1267,9 +1237,6 @@
"s390x" "s390x"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1284,9 +1251,6 @@
"x64" "x64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1301,9 +1265,6 @@
"x64" "x64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1493,9 +1454,6 @@
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "Apache-2.0 OR MIT", "license": "Apache-2.0 OR MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1513,9 +1471,6 @@
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "Apache-2.0 OR MIT", "license": "Apache-2.0 OR MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1533,9 +1488,6 @@
"riscv64" "riscv64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "Apache-2.0 OR MIT", "license": "Apache-2.0 OR MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1553,9 +1505,6 @@
"x64" "x64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "Apache-2.0 OR MIT", "license": "Apache-2.0 OR MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1573,9 +1522,6 @@
"x64" "x64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "Apache-2.0 OR MIT", "license": "Apache-2.0 OR MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1809,9 +1755,9 @@
} }
}, },
"node_modules/baseline-browser-mapping": { "node_modules/baseline-browser-mapping": {
"version": "2.10.44", "version": "2.11.21",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.44.tgz", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.21.tgz",
"integrity": "sha512-T3ghW+sl/ZJ8w1v/yQx3qvJ9040DWoLBz8JT/CILbAKcFyG9b2MRe75v6W5uXjv6uH1lumK2Kv46y2zSkcej0Q==", "integrity": "sha512-uh8vpY/1/YyFkunIDFH/12p7/7VdPKA1hejMVEbdkEaWnUz0Hesvx5EbiU6XxjyHZIOju+ZMbQJkRh+es3/spQ==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"bin": { "bin": {
@@ -1822,9 +1768,9 @@
} }
}, },
"node_modules/brace-expansion": { "node_modules/brace-expansion": {
"version": "5.0.7", "version": "5.0.9",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz",
"integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true, "peer": true,
@@ -1832,13 +1778,13 @@
"balanced-match": "^4.0.2" "balanced-match": "^4.0.2"
}, },
"engines": { "engines": {
"node": "18 || 20 || >=22" "node": "20 || >=22"
} }
}, },
"node_modules/browserslist": { "node_modules/browserslist": {
"version": "4.28.6", "version": "4.28.9",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.6.tgz", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.9.tgz",
"integrity": "sha512-FQBYNK15VMslhLHpA7+n+n1GOlF1kId2xcCg7/j95f24AOF6VDYMNH4mFxF7KuaTdv627faazpOAjFzMrfJOUw==", "integrity": "sha512-EWazOblFYUvlGZcfGhPUPmYh3nikUxBVb+y9MJun5f3hBi812X+8MSQTujLBtgK3cf51fJWbWfOjyeO954d+Eg==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@@ -1856,11 +1802,11 @@
], ],
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"baseline-browser-mapping": "^2.10.42", "baseline-browser-mapping": "^2.11.20",
"caniuse-lite": "^1.0.30001803", "caniuse-lite": "^1.0.30001810",
"electron-to-chromium": "^1.5.389", "electron-to-chromium": "^1.5.420",
"node-releases": "^2.0.51", "node-releases": "^2.0.54",
"update-browserslist-db": "^1.2.3" "update-browserslist-db": "^1.3.2"
}, },
"bin": { "bin": {
"browserslist": "cli.js" "browserslist": "cli.js"
@@ -1870,9 +1816,9 @@
} }
}, },
"node_modules/caniuse-lite": { "node_modules/caniuse-lite": {
"version": "1.0.30001806", "version": "1.0.30001810",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz",
"integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==", "integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@@ -1947,9 +1893,9 @@
"peer": true "peer": true
}, },
"node_modules/electron-to-chromium": { "node_modules/electron-to-chromium": {
"version": "1.5.394", "version": "1.5.422",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.394.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.422.tgz",
"integrity": "sha512-Wmt2Gm0o8JWBuGgmc4XZ0u9s1RaCRqhxP47phplmfg04+qypTUurpeJGP45A7Fhv7jdrrVH44PLlR9qXo37cVQ==", "integrity": "sha512-UvA/32XqrLDdZSn7Jllo1AYNcWji/G0d5M0GTViE7KoGBiMunw3a34Sb2KO4ZZyrSEhqsxFoVhWWJshdyfKqJA==",
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
@@ -2562,9 +2508,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/nanoid": { "node_modules/nanoid": {
"version": "3.3.16", "version": "3.3.18",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",
"integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@@ -2589,9 +2535,9 @@
"peer": true "peer": true
}, },
"node_modules/node-releases": { "node_modules/node-releases": {
"version": "2.0.51", "version": "2.0.54",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.54.tgz",
"integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==", "integrity": "sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -2694,9 +2640,9 @@
} }
}, },
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.5.21", "version": "8.5.28",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.21.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz",
"integrity": "sha512-v4sDNP3fdNiWMfabO7OwOQdOX8TiQSztKyT1Wj0w+j7LDallJThJRBBBmzVGyYj0crMh7jlV4zepPkiNu9UwDQ==", "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@@ -2714,7 +2660,7 @@
], ],
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"nanoid": "^3.3.16", "nanoid": "^3.3.18",
"picocolors": "^1.1.1", "picocolors": "^1.1.1",
"source-map-js": "^1.2.1" "source-map-js": "^1.2.1"
}, },
@@ -2962,9 +2908,9 @@
} }
}, },
"node_modules/update-browserslist-db": { "node_modules/update-browserslist-db": {
"version": "1.2.3", "version": "1.3.2",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.2.tgz",
"integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", "integrity": "sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
+1 -1
View File
@@ -22,4 +22,4 @@ tauri = { version = "2", features = [] }
tauri-plugin-opener = "2" tauri-plugin-opener = "2"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
dsp_thomann = { path = "../../dsp_thomann", features = ["serialization"] } dsp_thomann = { path = "../../dsp-thomann", features = ["serialization"] }
+2 -2
View File
@@ -2,7 +2,7 @@ export function TriangleUpIcon(props: React.SVGProps<SVGSVGElement>) {
return ( return (
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}> <svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<path <path
d="M8 3L13 10H3L8 3Z" d="M8 5L4 10H12L8 5Z"
stroke="currentColor" stroke="currentColor"
strokeWidth="1.5" strokeWidth="1.5"
fill="transparent" fill="transparent"
@@ -16,7 +16,7 @@ export function TriangleDownIcon(props: React.SVGProps<SVGSVGElement>) {
return ( return (
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}> <svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<path <path
d="M8 13L3 6H13L8 13Z" d="M8 11L4 6H12L8 11Z"
stroke="currentColor" stroke="currentColor"
strokeWidth="1.5" strokeWidth="1.5"
fill="transparent" fill="transparent"
@@ -0,0 +1,55 @@
/* ChoiceSelect.css */
.choice-select {
position: relative;
display: flex;
align-items: stretch;
}
.choice-select[data-disabled] {
opacity: 0.5;
}
.choice-select__input {
flex: 1 1 auto;
height: var(--card-control-height);
padding: 0 var(--space-6) 0 var(--space-3);
background: var(--bg-panel);
color: var(--text-primary);
border: var(--border-width) solid var(--border-hairline);
border-radius: var(--radius-sm);
font-family: var(--font-ui);
font-size: var(--font-sm);
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
outline: none;
cursor: pointer;
transition: border-color var(--dur-base) var(--ease-standard);
}
.choice-select__input:focus {
border-color: var(--accent-brand);
}
.choice-select__input:disabled {
cursor: not-allowed;
}
.choice-select__chevron {
position: absolute;
right: var(--space-2);
top: 50%;
height: 70%;
aspect-ratio: 1;
transform: translateY(-50%);
pointer-events: none;
color: var(--accent-brand);
font-size: var(--font-sm);
}
@@ -0,0 +1,44 @@
// ChoiceSelect.tsx
import { TriangleDownIcon } from '../../assets/icons/Triangle';
import './ChoiceSelect.css';
type ChoiceSelectProps<T extends string> = {
value: T;
options: readonly T[];
formatLabel?: (value: T) => string;
onChange: (value: T) => void;
id?: string;
disabled?: boolean;
};
function ChoiceSelect<T extends string>({
value,
options,
formatLabel = (v) => v,
onChange,
id,
disabled,
}: ChoiceSelectProps<T>) {
return (
<div className="choice-select" data-disabled={disabled || undefined}>
<select
id={id}
className="choice-select__input"
value={value}
disabled={disabled}
onChange={(e) => onChange(e.target.value as T)}
>
{options.map((option) => (
<option key={option} value={option}>
{formatLabel(option)}
</option>
))}
</select>
<span className="choice-select__chevron" aria-hidden="true">
<TriangleDownIcon />
</span>
</div>
);
}
export default ChoiceSelect;
+10 -3
View File
@@ -20,14 +20,13 @@
font-family: var(--font-ui); font-family: var(--font-ui);
font-size: 3cqh; font-size: 3cqh;
font-weight: var(--font-weight-semibold); font-weight: var(--font-weight-semibold);
text-align: center; text-align: left;
color: var(--accent-brand); color: var(--accent-brand);
background: transparent; background: transparent;
border: none; border: none;
border-radius: var(--radius-sm); /* padding: 0.5ch; */
padding: 0.5ch;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
@@ -78,3 +77,11 @@
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
.choice-stepper[data-disabled] {
opacity: 0.5;
}
.choice-stepper__input:disabled {
cursor: not-allowed;
}
+151 -43
View File
@@ -1,60 +1,97 @@
import { useEffect, useState } from 'react'; import { JSX, useEffect, useState } from 'react';
import Button from '../Button/Button'; import Button from '../Button/Button';
import { TriangleUpIcon, TriangleDownIcon } from '../../assets/icons/Triangle'; import { TriangleUpIcon, TriangleDownIcon } from '../../assets/icons/Triangle';
import './ChoiceStepper.css'; import './ChoiceStepper.css';
type ChoiceStepperProps<T> = { function StepperArrows({
onUp,
onDown,
canUp,
canDown,
disabled,
}: {
onUp: () => void;
onDown: () => void;
canUp: boolean;
canDown: boolean;
disabled?: boolean;
}) {
return (
<div className="choice-stepper__arrows">
<Button
variant="toggle"
className="choice-stepper__arrow-btn"
onClick={onUp}
disabled={disabled || !canUp}
>
<TriangleUpIcon />
</Button>
<Button
variant="toggle"
className="choice-stepper__arrow-btn"
onClick={onDown}
disabled={disabled || !canDown}
>
<TriangleDownIcon />
</Button>
</div>
);
}
// ---- List variant ----
type ChoiceStepperListProps<T> = {
value: T; value: T;
values: readonly T[]; values: readonly T[];
formatLabel: (value: T) => string; formatLabel: (value: T) => string;
onChange: (value: T) => void; onChange: (value: T) => void;
id?: string;
disabled?: boolean;
className?: string;
}; };
function ChoiceStepper<T>({ value, values, formatLabel, onChange }: ChoiceStepperProps<T>) { function ChoiceStepperList<T>({
value,
values,
formatLabel,
onChange,
id,
disabled,
className,
}: ChoiceStepperListProps<T>) {
const index = values.indexOf(value); const index = values.indexOf(value);
const canUp = index === -1 || index < values.length - 1; const canUp = index === -1 || index < values.length - 1;
const canDown = index > 0; const canDown = index > 0;
const [text, setText] = useState(formatLabel(value)); const [text, setText] = useState(formatLabel(value));
useEffect(() => setText(formatLabel(value)), [value, formatLabel]);
useEffect(() => {
setText(formatLabel(value));
}, [value, formatLabel]);
const step = (direction: 1 | -1) => { const step = (direction: 1 | -1) => {
const currentIndex = index === -1 ? 0 : index; const currentIndex = index === -1 ? 0 : index;
const nextIndex = Math.min(values.length - 1, Math.max(0, currentIndex + direction)); const nextIndex = Math.min(values.length - 1, Math.max(0, currentIndex + direction));
if (nextIndex !== currentIndex) onChange(values[nextIndex]);
if (nextIndex !== currentIndex) {
onChange(values[nextIndex]);
}
}; };
const commitText = () => { const commitText = () => {
const match = values.find( const match = values.find(
(v) => formatLabel(v).trim().toLowerCase() === text.trim().toLowerCase() (v) => formatLabel(v).trim().toLowerCase() === text.trim().toLowerCase()
); );
if (match !== undefined) onChange(match);
if (match !== undefined) { else setText(formatLabel(value));
onChange(match);
} else {
setText(formatLabel(value));
}
}; };
return ( return (
<div className="choice-stepper"> <div className={`choice-stepper ${className ?? ''}`} data-disabled={disabled || undefined}>
<input <input
id={id}
className="choice-stepper__input" className="choice-stepper__input"
value={text} value={text}
disabled={disabled}
onChange={(e) => setText(e.target.value)} onChange={(e) => setText(e.target.value)}
onBlur={commitText} onBlur={commitText}
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === 'Enter') { if (e.key === 'Enter') e.currentTarget.blur();
e.currentTarget.blur(); else if (e.key === 'ArrowUp') {
} else if (e.key === 'ArrowUp') {
e.preventDefault(); e.preventDefault();
step(1); step(1);
} else if (e.key === 'ArrowDown') { } else if (e.key === 'ArrowDown') {
@@ -63,28 +100,99 @@ function ChoiceStepper<T>({ value, values, formatLabel, onChange }: ChoiceSteppe
} }
}} }}
/> />
<StepperArrows
<div className="choice-stepper__arrows"> onUp={() => step(1)}
<Button onDown={() => step(-1)}
variant="toggle" canUp={canUp}
className="choice-stepper__arrow-btn" canDown={canDown}
onClick={() => step(1)} disabled={disabled}
disabled={!canUp} />
>
<TriangleUpIcon />
</Button>
<Button
variant="toggle"
className="choice-stepper__arrow-btn"
onClick={() => step(-1)}
disabled={!canDown}
>
<TriangleDownIcon />
</Button>
</div>
</div> </div>
); );
} }
// ---- Range variant (concrete, no generic) ----
type ChoiceStepperRangeProps = {
value: number;
min: number;
max: number;
step?: number;
formatLabel: (value: number) => string;
onChange: (value: number) => void;
id?: string;
disabled?: boolean;
className?: string;
};
function ChoiceStepperRange({
value,
min,
max,
step: stepSize = 1,
formatLabel,
onChange,
id,
disabled,
className,
}: ChoiceStepperRangeProps) {
const [text, setText] = useState(formatLabel(value));
useEffect(() => setText(formatLabel(value)), [value, formatLabel]);
const clamp = (n: number) => Math.min(max, Math.max(min, n));
const canUp = value < max;
const canDown = value > min;
const step = (direction: 1 | -1) => {
const next = clamp(value + direction * stepSize);
if (next !== value) onChange(next);
};
const commitText = () => {
const parsed = Number(text);
if (Number.isFinite(parsed)) onChange(clamp(parsed));
else setText(formatLabel(value));
};
return (
<div className={`choice-stepper ${className ?? ''}`} data-disabled={disabled || undefined}>
<input
id={id}
className="choice-stepper__input"
value={text}
disabled={disabled}
onChange={(e) => setText(e.target.value)}
onBlur={commitText}
onKeyDown={(e) => {
if (e.key === 'Enter') e.currentTarget.blur();
else if (e.key === 'ArrowUp') {
e.preventDefault();
step(1);
} else if (e.key === 'ArrowDown') {
e.preventDefault();
step(-1);
}
}}
/>
<StepperArrows
onUp={() => step(1)}
onDown={() => step(-1)}
canUp={canUp}
canDown={canDown}
disabled={disabled}
/>
</div>
);
}
function ChoiceStepper<T>(props: ChoiceStepperListProps<T>): JSX.Element;
function ChoiceStepper(props: ChoiceStepperRangeProps): JSX.Element;
function ChoiceStepper(props: ChoiceStepperListProps<unknown> | ChoiceStepperRangeProps) {
if ('values' in props) {
return <ChoiceStepperList {...props} />;
}
return <ChoiceStepperRange {...props} />;
}
export default ChoiceStepper; export default ChoiceStepper;
@@ -68,8 +68,14 @@ function getPeqCrossoverPoints(
getPeqCurvePoints({ freqs, bands: peqChain.bands as any }), getPeqCurvePoints({ freqs, bands: peqChain.bands as any }),
getCrossoverCurvePoints({ getCrossoverCurvePoints({
freqs, freqs,
highPass: { frequency: crossover.high_pass.frequency, filter_type: crossover.high_pass.slope as any }, highPass: {
lowPass: { frequency: crossover.low_pass.frequency, filter_type: crossover.low_pass.slope as any }, frequency: crossover.high_pass.frequency,
filter_type: crossover.high_pass.slope as any,
},
lowPass: {
frequency: crossover.low_pass.frequency,
filter_type: crossover.low_pass.slope as any,
},
}), }),
]); ]);
} }
@@ -317,7 +323,9 @@ function GainSection({
const [showOutputStages, setShowOutputStages] = useState(false); const [showOutputStages, setShowOutputStages] = useState(false);
const [liveInputGains, setLiveInputGains] = useState<Partial<Record<InputChannel, number>>>({}); const [liveInputGains, setLiveInputGains] = useState<Partial<Record<InputChannel, number>>>({});
const [liveOutputGains, setLiveOutputGains] = useState<Partial<Record<OutputChannel, number>>>({}); const [liveOutputGains, setLiveOutputGains] = useState<Partial<Record<OutputChannel, number>>>(
{}
);
const setLiveInputGain = (channel: InputChannel, value: number | null) => { const setLiveInputGain = (channel: InputChannel, value: number | null) => {
setLiveInputGains((prev) => { setLiveInputGains((prev) => {
@@ -381,7 +389,11 @@ function GainSection({
inputColors, inputColors,
showInputStages, showInputStages,
(channel) => (channel) =>
getInputChannelCurves(dsp.state.current_config.input_states[channel], freqs, liveInputGains[channel]) getInputChannelCurves(
dsp.state.current_config.input_states[channel],
freqs,
liveInputGains[channel]
)
); );
const outputCurves = buildCurvesForChannels( const outputCurves = buildCurvesForChannels(
@@ -390,7 +402,11 @@ function GainSection({
outputColors, outputColors,
showOutputStages, showOutputStages,
(channel) => (channel) =>
getOutputChannelCurves(dsp.state.current_config.output_states[channel], freqs, liveOutputGains[channel]) getOutputChannelCurves(
dsp.state.current_config.output_states[channel],
freqs,
liveOutputGains[channel]
)
); );
const { yMin: inputYMin, yMax: inputYMax } = computeYAxisBounds( const { yMin: inputYMin, yMax: inputYMax } = computeYAxisBounds(
@@ -13,7 +13,7 @@ import {
} from '../../constants'; } from '../../constants';
import { SidebarId } from '../../constants'; import { SidebarId } from '../../constants';
import SignalFlowMatrix from '../../../../components/SignalFlowMatrix/SignalFlowMatrix'; import SignalFlowMatrix from '../../../../components/SignalFlowMatrix/SignalFlowMatrix';
import './MatrixSection.css'; import './Matrixsection.css';
const GAIN_VALUES: number[] = (() => { const GAIN_VALUES: number[] = (() => {
const values: number[] = []; const values: number[] = [];
+2 -5
View File
@@ -58,9 +58,7 @@ export function sumCurvePoints(pointArrays: CurvePoint[][]): CurvePoint[] {
return result; return result;
} }
export function sumPhaseCurvePoints( export function sumPhaseCurvePoints(pointArrays: CurvePoint[][]): CurvePoint[] {
pointArrays: CurvePoint[][]
): CurvePoint[] {
if (pointArrays.length === 0) return []; if (pointArrays.length === 0) return [];
const length = pointArrays[0].length; const length = pointArrays[0].length;
@@ -85,8 +83,7 @@ export function sumPhaseCurvePoints(
} }
// Wrap to [-180, 180]. // Wrap to [-180, 180].
const wrapped = const wrapped = ((((phase + 180) % 360) + 360) % 360) - 180;
((((phase + 180) % 360) + 360) % 360) - 180;
result[i] = { result[i] = {
x: pointArrays[0][i].x, x: pointArrays[0][i].x,
+1 -7
View File
@@ -1,10 +1,4 @@
import { import { CurvePoint, CurveStyle, Curve, DEFAULT_CURVE_COLORS, sumCurvePoints } from './curve';
CurvePoint,
CurveStyle,
Curve,
DEFAULT_CURVE_COLORS,
sumCurvePoints,
} from './curve';
export { sumCurvePoints }; export { sumCurvePoints };
+96 -5
View File
@@ -34,21 +34,26 @@
color: var(--text-muted); color: var(--text-muted);
} }
.add-dsp-form__field input, /* Input */
.add-dsp-form__field select {
.add-dsp-form__field input {
height: var(--card-control-height); height: var(--card-control-height);
padding: 0 var(--space-3); padding: 0 var(--space-3);
background: var(--bg-panel); background: var(--bg-panel);
color: var(--text-primary);
border: var(--border-width) solid var(--border-hairline); border: var(--border-width) solid var(--border-hairline);
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
color: var(--text-primary);
font-family: var(--font-ui); font-family: var(--font-ui);
font-size: var(--font-sm); font-size: var(--font-sm);
outline: none; outline: none;
appearance: none;
-webkit-appearance: none;
transition: border-color var(--dur-base) var(--ease-standard); transition: border-color var(--dur-base) var(--ease-standard);
} }
@@ -56,11 +61,15 @@
font-family: var(--font-mono); font-family: var(--font-mono);
} }
.add-dsp-form__field input:focus, .add-dsp-form__field input:focus {
.add-dsp-form__field select:focus {
border-color: var(--accent-brand); border-color: var(--accent-brand);
} }
.add-dsp-form__field input::placeholder {
color: var(--text-muted);
opacity: 0.5;
}
.add-dsp-form__actions { .add-dsp-form__actions {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
@@ -68,7 +77,89 @@
margin-top: var(--space-2); margin-top: var(--space-2);
} }
.add-dsp-form__field input:disabled {
color: var(--text-muted);
background: var(--bg-panel);
border-color: var(--border-hairline);
opacity: 0.5;
cursor: not-allowed;
}
/* CheckBox */
.add-dsp-form__checkbox-label { .add-dsp-form__checkbox-label {
display: flex; display: flex;
align-items: center; align-items: center;
gap: var(--space-2);
cursor: pointer;
}
.add-dsp-form__checkbox-label input[type='checkbox'] {
appearance: none;
-webkit-appearance: none;
box-sizing: border-box;
flex: 0 0 auto;
display: block;
width: 18px;
height: 18px;
aspect-ratio: 1 / 1;
padding: 0;
margin: 0;
background: var(--bg-panel);
border: var(--border-width) solid var(--border-hairline);
border-radius: var(--radius-sm);
position: relative;
cursor: pointer;
transition:
border-color var(--dur-base) var(--ease-standard),
background var(--dur-base) var(--ease-standard);
}
.add-dsp-form__checkbox-label input[type='checkbox']:checked {
background: var(--accent-brand);
border-color: var(--accent-brand);
}
.add-dsp-form__checkbox-label input[type='checkbox']:checked::after {
content: '';
position: absolute;
left: 50%;
top: 50%;
width: 5px;
height: 9px;
border: solid var(--bg-panel);
border-width: 0 2px 2px 0;
transform: translate(-50%, -60%) rotate(45deg);
}
.add-dsp-form__checkbox-label input[type='checkbox']:focus-visible {
outline: 2px solid var(--accent-brand);
outline-offset: 2px;
}
/* Stepper */
.add-dsp-form__stepper {
width: auto;
flex: 0 0 auto;
min-width: 120px;
height: var(--card-control-height);
}
.add-dsp-form__stepper .choice-stepper__arrows {
padding: 4px 4px 4px 0;
gap: 2px;
}
.add-dsp-form__field .choice-stepper__input {
border: none;
background: transparent;
} }
+20 -31
View File
@@ -2,6 +2,8 @@ import { useState } from 'react';
import { DSP, DSP_DEFAULT_PARAMETERS, DSPTypes } from '../../types'; import { DSP, DSP_DEFAULT_PARAMETERS, DSPTypes } from '../../types';
import Button from '../../components/Button/Button'; import Button from '../../components/Button/Button';
import Card from '../../components/Card/Card'; import Card from '../../components/Card/Card';
import ChoiceStepper from '../../components/ChoiceStepper/ChoiceStepper';
import ChoiceSelect from '../../components/ChoiceSelect/ChoiceSelect';
import './AddDspPage.css'; import './AddDspPage.css';
@@ -14,6 +16,8 @@ type DSPForm = {
demo: boolean; demo: boolean;
}; };
const DSP_TYPES = Object.keys(DSP_DEFAULT_PARAMETERS) as DSPTypes[];
function AddDSPForm({ function AddDSPForm({
onCancel, onCancel,
onAdd, onAdd,
@@ -39,9 +43,7 @@ function AddDSPForm({
function submit() { function submit() {
if (!canSubmit) return; if (!canSubmit) return;
onAdd({ onAdd({ ...form });
...form,
});
} }
return ( return (
@@ -64,38 +66,24 @@ function AddDSPForm({
<div className="add-dsp-form__row"> <div className="add-dsp-form__row">
<div className="add-dsp-form__field"> <div className="add-dsp-form__field">
<label htmlFor="f-type">Model</label> <label htmlFor="f-type">Model</label>
<select <ChoiceSelect
id="f-type" id="f-type"
value={form.type} value={form.type}
onChange={(e) => { options={DSP_TYPES}
const type = e.target.value as DSPTypes; onChange={(type) => setForm({ ...form, type })}
/>
setForm({
...form,
type,
});
}}
>
{Object.keys(DSP_DEFAULT_PARAMETERS).map((type) => (
<option key={type} value={type}>
{type}
</option>
))}
</select>
</div> </div>
<div className="add-dsp-form__field"> <div className="add-dsp-form__field">
<label htmlFor="f-deviceid">Device ID</label> <label htmlFor="f-deviceid">Device ID</label>
<input <ChoiceStepper
id="f-deviceid" id="f-deviceid"
className="mono" className="add-dsp-form__stepper"
type="number" value={form.deviceId}
min={1} min={1}
max={255} max={255}
step={1} formatLabel={(v) => String(v)}
placeholder="1" onChange={(deviceId) => setForm({ ...form, deviceId })}
value={form.deviceId}
onChange={(e) => setForm({ ...form, deviceId: Number(e.target.value) })}
/> />
</div> </div>
</div> </div>
@@ -114,13 +102,15 @@ function AddDSPForm({
<div className="add-dsp-form__field"> <div className="add-dsp-form__field">
<label htmlFor="f-port">Port</label> <label htmlFor="f-port">Port</label>
<input <ChoiceStepper
id="f-port" id="f-port"
className="mono" className="add-dsp-form__stepper"
type="number"
value={form.port} value={form.port}
min={1}
max={65535}
disabled={form.demo} disabled={form.demo}
onChange={(e) => setForm({ ...form, port: Number(e.target.value) })} formatLabel={(v) => String(v)}
onChange={(port) => setForm({ ...form, port })}
/> />
</div> </div>
</div> </div>
@@ -141,7 +131,6 @@ function AddDSPForm({
<Button variant="ghost" onClick={onCancel}> <Button variant="ghost" onClick={onCancel}>
Cancel Cancel
</Button> </Button>
<Button variant="primary" onClick={submit} disabled={!canSubmit}> <Button variant="primary" onClick={submit} disabled={!canSubmit}>
Add DSP Add DSP
</Button> </Button>