Skip to content

Commit e293ba1

Browse files
committed
Initial public release: curveforge UI playground
Browser playground for the curveforge Python library. Runs the canonical build_curve() in the page via Pyodide; designs Dirac Live target curves with live audio preview, A/B comparison, YAML/.targetcurve I/O, and localStorage persistence. No backend. Stack: Preact + @preact/signals + htm, Vite, TypeScript strict, stylelint + eslint mirroring the llm-pipelines lint contract.
0 parents  commit e293ba1

42 files changed

Lines changed: 7813 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/deploy.yml

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
name: Deploy
2+
3+
on:
4+
push:
5+
branches: [main]
6+
workflow_dispatch:
7+
8+
permissions:
9+
contents: read
10+
pages: write
11+
id-token: write
12+
13+
concurrency:
14+
group: pages
15+
cancel-in-progress: false
16+
17+
jobs:
18+
check:
19+
runs-on: ubuntu-latest
20+
steps:
21+
- uses: actions/checkout@v4
22+
- uses: actions/setup-node@v4
23+
with:
24+
node-version: "20"
25+
cache: npm
26+
- run: npm ci
27+
- run: npm run lint:all
28+
29+
build:
30+
needs: check
31+
runs-on: ubuntu-latest
32+
steps:
33+
- uses: actions/checkout@v4
34+
- uses: actions/setup-node@v4
35+
with:
36+
node-version: "20"
37+
cache: npm
38+
- run: npm ci
39+
- run: npm run build
40+
- uses: actions/upload-pages-artifact@v3
41+
with:
42+
path: ./dist
43+
44+
deploy:
45+
needs: build
46+
runs-on: ubuntu-latest
47+
environment:
48+
name: github-pages
49+
url: ${{ steps.deployment.outputs.page_url }}
50+
steps:
51+
- id: deployment
52+
uses: actions/deploy-pages@v4

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
node_modules
2+
dist
3+
.DS_Store
4+
*.log
5+
.vite

.prettierrc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"semi": false,
3+
"singleQuote": true,
4+
"trailingComma": "all",
5+
"printWidth": 100
6+
}

.stylelintrc.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"extends": "stylelint-config-standard",
3+
"rules": {
4+
"max-nesting-depth": 2,
5+
"selector-max-class": 4,
6+
"selector-max-compound-selectors": 3,
7+
"no-descending-specificity": null,
8+
"declaration-no-important": true,
9+
"selector-pseudo-class-no-unknown": [true, { "ignorePseudoClasses": ["where", "is"] }],
10+
11+
"property-no-vendor-prefix": null,
12+
"selector-no-vendor-prefix": null,
13+
"value-no-vendor-prefix": null
14+
}
15+
}

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 curveforge-ui contributors
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# curveforge-ui
2+
3+
Browser playground for [curveforge](https://pypi.org/project/curveforge/),
4+
the Python library that builds Dirac Live target curves. The Python core
5+
runs in the page via Pyodide — the UI calls the exact same `build_curve()`
6+
function the CLI uses, so the math is the canonical reference.
7+
8+
**Live:** https://teolbb.github.io/curveforge-ui/
9+
10+
## What it does
11+
12+
- **Eight base curves** (Harman, Olive–Welti, Brüel & Kjær, Toole, sub
13+
shelves, flat, fully-custom breakpoints) with schema-driven param
14+
sliders rendered from the Python library's metadata.
15+
- **Composable transforms** — peaking EQ, shelves, tilt, gain, roll-off
16+
— stacked on top of any base curve. Reorder, remove, tweak live.
17+
- **A/B compare** two recipes side-by-side; <kbd>A</kbd>/<kbd>B</kbd>
18+
to switch, <kbd>S</kbd> to swap.
19+
- **Live audio preview** through a 31-band parametric-EQ cascade. Drive
20+
it with pink noise, a log sweep, or an uploaded file. <kbd>Space</kbd>
21+
bypasses for instant before/after.
22+
- **YAML import/export** in the same format the CLI consumes — design
23+
here, ship the recipe straight to a pipeline.
24+
- **`.targetcurve` export** ready for Dirac Live's calibration tool.
25+
- All state persists in `localStorage`; no backend, no accounts.
26+
27+
## Develop
28+
29+
```sh
30+
npm install
31+
npm run dev # http://localhost:5173/
32+
npm run lint:all # eslint + stylelint + tsc --noEmit
33+
npm run build # static output to dist/
34+
```
35+
36+
First load downloads Pyodide (~10 MB) and installs `curveforge` from
37+
PyPI via `micropip` — takes a few seconds, then cached.
38+
39+
## Layout
40+
41+
```
42+
src/
43+
main.ts entry — listeners + bootstrap
44+
engine/ domain logic, no Preact
45+
state.ts signals + persistence + mutations
46+
audio.ts Web Audio EQ cascade
47+
bridge.ts Pyodide + YAML I/O
48+
ui/ view layer
49+
App.ts Preact components, grouped by panel
50+
plot.ts SVG magnitude-response renderer
51+
actions.ts UI → side-effect handlers
52+
widgets.ts reusable widgets
53+
{icons,labels,sparklines,types}.ts
54+
styles/ themed CSS files
55+
public/schemas.json curve + transform metadata, regenerated from curveforge
56+
```
57+
58+
The Pyodide bridge is intentionally thin: each curve build writes a
59+
small JSON payload into Python globals, runs `build_curve()`, reads
60+
back `{freqs, gains_db}`. Plot rendering is plain SVG — no charting
61+
library.
62+
63+
## License
64+
65+
MIT.

eslint.config.js

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import tsParser from '@typescript-eslint/parser'
2+
import tsPlugin from '@typescript-eslint/eslint-plugin'
3+
import sonarjs from 'eslint-plugin-sonarjs'
4+
import jsdoc from 'eslint-plugin-jsdoc'
5+
6+
const STRICT_RULES = {
7+
'@typescript-eslint/no-explicit-any': 'error',
8+
'@typescript-eslint/no-non-null-assertion': 'error',
9+
'@typescript-eslint/explicit-function-return-type': ['error', {
10+
allowExpressions: true,
11+
allowTypedFunctionExpressions: true,
12+
}],
13+
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
14+
15+
// Structural metrics — all hard caps. High values signal a function doing too much.
16+
// - Function length: 50 lines (≤40 is the working norm).
17+
// - Indent depth: 3 levels. Non-negotiable.
18+
// - Cyclomatic complexity: 12. Genuine cases (e.g. dispatch over a finite enum)
19+
// can use `// eslint-disable-next-line complexity` with a one-line justification.
20+
'max-lines-per-function': ['error', { max: 50, skipBlankLines: true, skipComments: true, IIFEs: true }],
21+
'max-lines': ['error', { max: 400, skipBlankLines: true, skipComments: true }],
22+
'max-depth': ['error', 3],
23+
'complexity': ['error', 12],
24+
25+
'no-restricted-syntax': ['error',
26+
{
27+
selector: "TSAsExpression > TSTypeReference > Identifier[name='never']",
28+
message: '`as never` is forbidden. Use proper types or `// eslint-disable-next-line` with a specific reason.',
29+
},
30+
{
31+
selector: 'TSAsExpression > TSAsExpression > TSUnknownKeyword',
32+
message: '`as unknown as X` double-cast is forbidden. Use proper types or `// eslint-disable-next-line` with a specific reason.',
33+
},
34+
],
35+
36+
'@typescript-eslint/consistent-type-assertions': ['error', {
37+
assertionStyle: 'as',
38+
objectLiteralTypeAssertions: 'never',
39+
}],
40+
'@typescript-eslint/no-unnecessary-type-assertion': 'error',
41+
42+
'no-console': 'error',
43+
'prefer-const': 'error',
44+
45+
// Cognitive complexity (sonarjs) — readability metric
46+
'sonarjs/cognitive-complexity': ['error', 15],
47+
48+
// Async correctness
49+
'@typescript-eslint/no-floating-promises': 'error',
50+
'@typescript-eslint/no-misused-promises': 'error',
51+
'@typescript-eslint/require-await': 'error',
52+
'@typescript-eslint/await-thenable': 'error',
53+
54+
// Boolean correctness — catches `if (someObj)` style bugs
55+
'@typescript-eslint/strict-boolean-expressions': ['error', {
56+
allowNullableBoolean: true,
57+
allowNullableObject: false,
58+
allowAny: false,
59+
}],
60+
61+
// Type-level correctness
62+
'@typescript-eslint/no-unnecessary-condition': 'error',
63+
'@typescript-eslint/switch-exhaustiveness-check': 'error',
64+
'@typescript-eslint/no-base-to-string': 'error',
65+
'@typescript-eslint/no-confusing-void-expression': 'error',
66+
67+
'@typescript-eslint/only-throw-error': 'error',
68+
69+
'@typescript-eslint/ban-ts-comment': ['error', {
70+
'ts-expect-error': 'allow-with-description',
71+
'ts-ignore': true,
72+
'ts-nocheck': true,
73+
'ts-check': false,
74+
}],
75+
76+
'@typescript-eslint/consistent-type-imports': ['error', {
77+
prefer: 'type-imports',
78+
fixStyle: 'inline-type-imports',
79+
}],
80+
81+
'eqeqeq': ['error', 'always', { null: 'ignore' }],
82+
83+
'@typescript-eslint/prefer-readonly': 'error',
84+
85+
'max-params': ['error', 4],
86+
87+
'sonarjs/no-identical-functions': 'error',
88+
89+
'@typescript-eslint/naming-convention': ['error',
90+
{ selector: 'variableLike', format: ['camelCase', 'UPPER_CASE', 'PascalCase'], leadingUnderscore: 'allow' },
91+
{ selector: 'typeLike', format: ['PascalCase'] },
92+
{ selector: 'enumMember', format: ['camelCase', 'PascalCase'] },
93+
],
94+
95+
// JSDoc-on-public-exports is required in libraries (so IDE hover docs stay in
96+
// sync). curveforge-ui is an end-user app, not an SDK consumers integrate
97+
// against, so this rule is disabled — the rest of the type discipline stands.
98+
'jsdoc/require-jsdoc': 'off',
99+
'jsdoc/check-tag-names': ['error', { definedTags: ['internal'] }],
100+
'jsdoc/check-param-names': 'error',
101+
'jsdoc/no-undefined-types': 'off',
102+
}
103+
104+
export default [
105+
{
106+
ignores: ['dist/**', 'node_modules/**', 'public/**', 'scripts/**'],
107+
},
108+
{
109+
files: ['src/**/*.ts'],
110+
languageOptions: {
111+
parser: tsParser,
112+
parserOptions: { project: ['./tsconfig.json'] },
113+
},
114+
plugins: { '@typescript-eslint': tsPlugin, sonarjs, jsdoc },
115+
rules: STRICT_RULES,
116+
},
117+
]

index.html

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<title>curveforge — playground</title>
7+
<link rel="stylesheet" href="/src/style.css" />
8+
</head>
9+
<body>
10+
<script type="module" src="/src/main.ts"></script>
11+
</body>
12+
</html>

0 commit comments

Comments
 (0)