Commit e2b701ea by PLN (Algolia)

docs(ui): the README described a template, not the bridge — and fix what the screenshot showed

`armada/ui/README.md` was still the stock `npm create vite` text after four pages had
been built in it, so the one file a cold reader opens first said nothing true. It now
lists the five pages and what question each answers, the commands, and the two things
that are easy to get wrong: audio is *mounted* through `audio-mounts.json` (read by
both vite dev and serve.py, so a URL works in both — a UI that only works under `npm
run dev` is a demo), and `kits.json` is generated, uncommitted, and written to `dist/`
as well as `public/`.

The `packs/` README now says where a pack's audio is staged, since it is no longer one
place: kits cut from someone else's stems live beside their source
(`Samples/Fred/output/<kit>/`, `--samples-root`) so the pack stays one movable,
deletable thing, while ParVagues' own kits sit directly under `Samples/`. The bank name
is the directory name either way, so `s "<kit>"` cannot tell the difference.

Then I took a headless screenshot of the page instead of assuming it rendered, which
found three things a typecheck cannot: a legacy kit's filenames are long enough to
truncate with nothing to recover them from (now `title`), the flag filter rendered as a
dead `! 0` on every kit with no flags (now hidden), and the empty-rack text ran the
full width of the screen.
parent bb404419
# React + TypeScript + Vite
# armada/ui — the bridge
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
The instruments PLN actually looks at. One Vite project, several pages, one design
language (**the Ship's Bridge**`../PRODUCT.md` for the register, `../DESIGN.md` for
the visuals, managed with the `impeccable` skill from `armada/`).
Currently, two official plugins are available:
Stack: Vite + React 19 + TS + Tailwind 4 + shadcn conventions, Geist / Geist Mono.
Node ≥ 18 — `nvm use 22`.
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
## The pages
## React Compiler
| URL | Name | What it answers |
|---|---|---|
| `/` | Take Judge | A/B two masters of the same take — which one ships |
| `/judge.html` | Set Judge | one pass down a whole gig, a verdict + comment per track → `decisions.json` |
| `/bounds.html` | Boundary Lab | where exactly does one track become the next |
| `/sextant.html` | Le Sextant | distribution cost/revenue simulation |
| `/kits.html` | **La Cale** | is this sample kit any good — audition, loop, rack up, export Tidal |
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
Each page is its own Rollup input in `vite.config.ts` and its own `src/<page>.tsx`
entry; shared bits live in `src/components/` and `src/lib/`.
## Expanding the ESLint configuration
## Running it
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
```js
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
```bash
nvm use 22
npm ci
npm run dev # http://localhost:5173 — also on the LAN (server.host)
npm run build # tsc -b && vite build → dist/
npm test # vitest
npm run lint
```
// Remove tseslint.configs.recommended and replace with this
tseslint.configs.recommendedTypeChecked,
// Alternatively, use this for stricter rules
tseslint.configs.strictTypeChecked,
// Optionally, add this for stylistic rules
tseslint.configs.stylisticTypeChecked,
For phone audition and anything that has to work outside dev, serve the build:
// Other configs...
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```bash
python3 ../serve.py --dir dist --port 8792
```
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
```js
// eslint.config.js
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Enable lint rules for React
reactX.configs['recommended-typescript'],
// Enable lint rules for React DOM
reactDom.configs.recommended,
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
`serve.py` is Range-capable, which stdlib `http.server` is not — without it, seeking a
90 MB master in `<audio>` does not work at all.
## Audio, and why it is mounted rather than bundled
The audio these pages play is gigabytes and lives outside the repo. `audio-mounts.json`
maps `/audio/<prefix>/...` to real directories, and it is read by **both**
`vite.config.ts` (dev) and `../serve.py` (built), so a URL that works in one works in
the other. A UI that only works under `npm run dev` is a demo, not a tool.
Longest prefix wins; `""` is the fallback mount. Add a prefix rather than a hardcoded
path — the masters move between gigs and the URLs should not.
## Data
Pages fetch generated JSON from `public/`. Some of it is committed (the gig-scoped
`judge-*.json`, `bounds-*.json`); `kits.json` is not — it is a 1.2 MB index of audio
that lives outside the repo, regenerated by
```bash
cd ../../tools/foundry && python3 kitindex.py --all
```
which writes `public/` **and** `dist/` if a build is present, because `npm run build`
copies `public/` exactly once and a page served from `dist/` would otherwise show an
index from before the last cut.
......@@ -292,7 +292,9 @@ export default function KitAuditioner() {
className={`w-full flex items-center gap-2 px-2 py-1 text-left
${k.kit === kitSel ? 'bg-overlay' : 'hover:bg-raised'}`}>
<span className="w-1.5 h-1.5 rounded-full shrink-0" style={{ background: familyColor(k.family) }} />
<span className="flex-1 truncate text-[12px]">{k.family ?? k.kit}</span>
<span className="flex-1 truncate text-[12px]" title={k.kit}>
{k.family ?? k.kit}
</span>
<span className="tnum text-[11px] text-ink-faint">{k.n_samples}</span>
</button>
))}
......@@ -330,12 +332,14 @@ export default function KitAuditioner() {
className="bg-raised border border-hairline rounded-sm px-1 py-0.5 text-[11px]">
{TIERS.map((t) => <option key={t} value={t}>{t}+</option>)}
</select>
<button onClick={() => setFlaggedOnly((v) => !v)} disabled={!flagged}
className={`px-1.5 py-0.5 text-[11px] rounded-sm border disabled:opacity-40
{flagged > 0 && (
<button onClick={() => setFlaggedOnly((v) => !v)}
className={`px-1.5 py-0.5 text-[11px] rounded-sm border
${flaggedOnly ? 'border-wip text-wip' : 'border-hairline text-ink-faint'}`}
title="samples the grader flagged — worth a second listen">
! {flagged}
</button>
)}
<div className="tnum text-[11px] text-ink-faint w-14 text-right">{samples.length} shown</div>
</div>
......
......@@ -45,7 +45,7 @@ export function Rack({ rows, bpm, playing, position, onChange, onRemove, onClear
if (!rows.length) {
return (
<div className="p-4 text-[12px] text-ink-faint">
<div className="p-4 text-[12px] text-ink-faint max-w-2xl">
The rack is empty. Add samples with <span className="font-mono">+</span> — or press
<span className="font-mono"> a </span>on a focused row — then hit play. Loops are
stretched to the deck tempo with <span className="font-mono">loopAt</span>, which is
......
......@@ -40,14 +40,14 @@ export function SampleRow({ s, playing, head, inRack, focused, onPlay, onStop, o
return (
<div
onMouseDown={onFocus}
className={`grid grid-cols-[2.2rem_11rem_3.2rem_1fr_5.5rem_4.6rem] items-center gap-2
className={`grid grid-cols-[2.2rem_14rem_3.2rem_1fr_5.5rem_4.6rem] items-center gap-2
px-2 py-1 border-b border-hairline/60 cursor-default
${focused ? 'bg-overlay' : 'hover:bg-raised'}`}
>
{/* the index you type. `# n 3` is the whole reason the kit is addressable */}
<div className="tnum font-mono text-[13px] text-ink-muted text-right pr-1">{s.n}</div>
<div className="min-w-0">
<div className="min-w-0" title={s.file}>
<div className="truncate text-[13px] leading-tight" style={{ color: col }}>
{s.stem_role ?? s.name}
</div>
......
......@@ -11,8 +11,12 @@ packs/<pack>/
kitcheck.md # the audit
```
The audio itself lives in `~/Work/Sound/Samples/<kit>/` (symlinked into Dirt-Samples),
never here — this is provenance, not a copy. The manifest is the record; `.md` and
The audio itself lives under a **staging root**, symlinked into Dirt-Samples, never
here — this is provenance, not a copy. Where that root is, is per-pack: kits cut from
someone else's stems are staged beside their source (`Samples/Fred/output/<kit>/`, via
`--samples-root`) so the pack stays one movable, deletable thing, while ParVagues' own
kits sit directly under `Samples/<kit>/`. Either way the bank name is the directory
name, so `s "<kit>"` does not care which. The manifest is the record; `.md` and
`.tidal` are views of it and can be rebuilt at any time with:
```bash
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment