Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .specify/memory/constitution.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ but not the primary supported path.

### II. Vanilla Frontend — No Frameworks

The frontend (`static/app.js`, `static/highway.js`, `static/index.html`,
The frontend (`static/app.js`, `static/highway.js`, `static/v3/index.html`,
`static/style.css`) is plain JavaScript with the `fetch` API, direct DOM
manipulation, and the Canvas 2D / WebGL2 APIs. The only style framework
is Tailwind CSS, served as a prebuilt static stylesheet
Expand Down Expand Up @@ -284,4 +284,4 @@ no `..`, no absolute paths).
higher-numbered principle's escape hatch is to live in a plugin
with its own bundled assets.

**Version**: 1.2.0 | **Ratified**: 2026-05-09 | **Last Amended**: 2026-07-08
**Version**: 1.3.0 | **Ratified**: 2026-05-09 | **Last Amended**: 2026-07-11
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Removed
- **The classic v2 UI shell is gone — v3 is the only UI (R3a).** `static/index.html`, the
`/v2` route, and the `FEEDBACK_UI` v2/legacy opt-out are deleted; `/` and `/v3` both serve
`static/v3/index.html`, which has been the default since 0.3.0. This is the first step of
the core-frontend ES-module migration (R3a): both shells load the same `static/app.js`, so
every subsequent step of that migration would otherwise have to be made, and verified,
twice. Removing the fallback now halves that surface before any of it is touched.
Incidentally fixes a latent bug in the old `index()` route — its guard read
`if getenv_compat("FEEDBACK_UI") or getenv_compat("FEEDBACK_UI") in ("v2", "legacy")`,
whose left operand is truthy for *any* non-empty value, so `FEEDBACK_UI=v3` actually served
the **v2** shell. `static/tailwind.min.css` is regenerated (the content globs scanned the
deleted file, so v2-only utility classes are now purged). Constitution amended to 1.3.0:
Principle II's frontend file list now names `static/v3/index.html`.
**Migration notes:** if you set `FEEDBACK_UI=v2` (or `=legacy`), or bookmarked `/v2`, there
is no longer a classic shell to fall back to — unset the variable and use `/`. The env var
itself is no longer read; the `SLOPSMITH_*`→`FEEDBACK_*` compat shim is unaffected. No
chart, settings, or plugin data changes, and no plugin API changes: v3 reuses the same
engine (`app.js`, `highway.js`, `playSong`, `showScreen`, the capability registry).

### Fixed
- **The packaged desktop app could not start (`ModuleNotFoundError: No module named
'appstate'`).** feedback-desktop's `scripts/bundle-slopsmith.sh` copies a *hardcoded
Expand Down
12 changes: 6 additions & 6 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,13 +125,13 @@ Notes:

### v3 UI (fee[dB]ack v0.3.0) — player-chrome contract

v0.3.0 ships a redesigned UI behind a flag (`FEEDBACK_UI=v3` or the `/v3` route);
the classic UI (v2) stays the default until 0.3.0 ships, so **plugins must work in
both**. v3 reuses the same engine (`server.py`, `app.js`, `highway.js`, `playSong`,
v0.3.0's redesigned UI is **the only UI** — the classic v2 shell and its
`FEEDBACK_UI` / `/v2` opt-outs are gone, so there is no second shell to support.
v3 reuses the same engine (`server.py`, `app.js`, `highway.js`, `playSong`,
`showScreen`, capabilities, library providers, the `window.feedBackViz_<id>` /
`setRenderer` contract), so a plugin's **backend, capabilities, `nav`/`screen`,
visualization renderers, diagnostics, and settings export work unchanged** — v3
surfaces `nav` in its sidebar and mounts screens exactly as v2 does.
surfaces `nav` in its sidebar and mounts screens as before.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

**The only thing that changed is the player chrome.** If your plugin injects a
control into it, you must adapt:
Expand All @@ -157,7 +157,7 @@ control into it, you must adapt:
popovers 40).

Full guide + the canonical snippet: **[docs/plugin-v3-ui.md](docs/plugin-v3-ui.md)**.
Verify any player-injecting plugin in **both** `/` (v2) and `/v3`.
Verify any player-injecting plugin at `/` — it and `/v3` serve the same v3 shell.

### Performance — never run DOM queries on a per-frame path

Expand Down Expand Up @@ -566,7 +566,7 @@ a local pointer + code map.
- **Storage** — `localStorage` for all user preferences
- **Styling** — Tailwind CSS utility classes, dark theme (`bg-dark-600`, `text-gray-300`, accent `#4080e0`, gold `#e8c040`). Tailwind is served as a **prebuilt** stylesheet (`static/tailwind.min.css`, regenerated by `bash scripts/build-tailwind.sh`), **never** the runtime Play CDN — the CDN's on-the-fly JIT rescanned the DOM on the main thread and dropped ~26% of frames with the 3D highway (feedBack-desktop#110). The committed CSS only contains classes the build scanner saw, so CI (`tailwind-fresh`) rebuilds and diffs it; run the build script and commit when you add new classes. A plugin that uses classes not guaranteed in core (notably arbitrary values like `w-[37px]`) MUST ship its own compiled stylesheet via the `styles` manifest key, built with `corePlugins.preflight = false` (utilities only — core ships the one base reset). Plugins MUST NOT load the Tailwind Play CDN or any runtime CSS JIT. See constitution Principle II.
- **Naming** — camelCase for JS functions, kebab-case for CSS classes, snake_case for plugin IDs
- **Text selection (v3)** — the v3 UI defaults to `user-select: none` on `html` (in `static/v3/v3.css`) so accidental drag/double-click selection of chrome never looks broken. Form fields are always re-enabled, and a **plugin's mounted screen subtree (`.screen[id^="plugin-"]`) stays selectable by default**, so a plugin's copy-worthy text (lyrics, chord names, results, diagnostics) is unaffected — *unless your plugin renders copyable content OUTSIDE its `plugin-<id>` screen* (e.g. injected into the player chrome / a HUD overlay), which inherits the non-select default. Opt such content back in with the core-served **`.fb-selectable`** class (it sets `user-select: text` on the element + descendants; works for runtime-installed plugins since it's hand-authored in core CSS, not a scanned Tailwind utility). Never use a `* { user-select: none }` rule (breaks input carets/IME), and never use `user-select: none` to "lock" text — keep errors, IDs, paths, versions, and metadata selectable. (v2 is unchanged.)
- **Text selection (v3)** — the v3 UI defaults to `user-select: none` on `html` (in `static/v3/v3.css`) so accidental drag/double-click selection of chrome never looks broken. Form fields are always re-enabled, and a **plugin's mounted screen subtree (`.screen[id^="plugin-"]`) stays selectable by default**, so a plugin's copy-worthy text (lyrics, chord names, results, diagnostics) is unaffected — *unless your plugin renders copyable content OUTSIDE its `plugin-<id>` screen* (e.g. injected into the player chrome / a HUD overlay), which inherits the non-select default. Opt such content back in with the core-served **`.fb-selectable`** class (it sets `user-select: text` on the element + descendants; works for runtime-installed plugins since it's hand-authored in core CSS, not a scanned Tailwind utility). Never use a `* { user-select: none }` rule (breaks input carets/IME), and never use `user-select: none` to "lock" text — keep errors, IDs, paths, versions, and metadata selectable.
- **Player layout** — `#player` is `display:flex; flex-direction:column; position:fixed; inset:0`. `#highway` is `flex:1`. `#player-controls` sits at the bottom. Hiding the highway collapses the layout — use `margin-top: auto` on controls if you need to hide it.

## Backend Conventions
Expand Down
8 changes: 4 additions & 4 deletions docs/plugin-styles.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# Plugin styling — the `styles` capability

> Building for the redesigned **v3 UI** (`FEEDBACK_UI=v3` / `/v3`)? v3 uses `fb-*`
> design tokens and a restructured player chrome with a dedicated plugin-control
> slot. See **[plugin-v3-ui.md](plugin-v3-ui.md)** for the player-chrome contract
> plugins must follow in v3.
> The **v3 UI** is the only UI — it uses `fb-*` design tokens and a restructured
> player chrome with a dedicated plugin-control slot. See
> **[plugin-v3-ui.md](plugin-v3-ui.md)** for the player-chrome contract plugins
> must follow.

FeedBack serves Tailwind as a **prebuilt** stylesheet
(`static/tailwind.min.css`), never the runtime Play CDN. The CDN's on-the-fly
Expand Down
24 changes: 12 additions & 12 deletions docs/plugin-v3-ui.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
# Building plugins for the v3 UI (fee[dB]ack v0.3.0)

v0.3.0 ("fee[dB]ack") ships a redesigned UI **behind a flag** — `FEEDBACK_UI=v3`
or the `/v3` route. The classic UI (v2) remains the default until 0.3.0 ships, so
plugins must work in **both**.

The good news: v3 **reuses the same engine** as v2 — same `server.py`, `app.js`,
`highway.js`, `playSong`, `showScreen`, capability registry, library providers,
and the `window.feedBackViz_<id>` / `setRenderer` visualization contract. So your
plugin's **backend, capabilities, library providers, `nav`/`screen`, visualization
renderers, diagnostics, and settings export all work unchanged in v3.** v3 surfaces
your `nav` entry in the new sidebar (via `shell.js` `renderPluginNav`) and your
screen mounts exactly as before.
v0.3.0 ("fee[dB]ack") ships a redesigned UI. It is **the only UI** — the classic v2
shell and its `FEEDBACK_UI` / `/v2` opt-outs have been removed, so there is no
longer a second shell to support.

The good news: v3 **reuses the same engine** the classic UI did — same `server.py`,
`app.js`, `highway.js`, `playSong`, `showScreen`, capability registry, library
providers, and the `window.feedBackViz_<id>` / `setRenderer` visualization contract.
So your plugin's **backend, capabilities, library providers, `nav`/`screen`,
visualization renderers, diagnostics, and settings export all work unchanged.** v3
surfaces your `nav` entry in the new sidebar (via `shell.js` `renderPluginNav`) and
your screen mounts exactly as before.

**The one thing that changed is the player chrome** — and only if your plugin
injects controls into it.
Expand Down Expand Up @@ -188,4 +188,4 @@ out of the capability graph.
- [ ] Dropdowns positioned via `getBoundingClientRect()`, not `#player-controls`.
- [ ] `#player` overlays keep `z-index` ≤ the chrome layers (transport/HUD 20,
rail 30, popovers 40).
- [ ] Verify in **both** `/` (v2) and `/v3`.
- [ ] Verify at `/` — it and `/v3` serve the same (and only) v3 shell.
16 changes: 1 addition & 15 deletions server.py
Original file line number Diff line number Diff line change
Expand Up @@ -2391,24 +2391,10 @@ def _running_version() -> str:

@app.get("/")
def index():
# fee[dB]ack v0.3.0: the v3 shell is now the DEFAULT at `/`. The classic v2
# UI remains fully available as a fallback — opt back in with
# FEEDBACK_UI=v2 (or =legacy), or hit the dedicated /v2 route below (which
# serves it regardless of the env var).
if getenv_compat("FEEDBACK_UI") or getenv_compat("FEEDBACK_UI") in ("v2", "legacy"):
return FileResponse(str(STATIC_DIR / "index.html"))
return FileResponse(str(STATIC_DIR / "v3" / "index.html"))


@app.get("/v3")
def index_v3():
# Always serve the v0.3.0 shell, independent of the env var (kept for
# explicit/back-compat links even though `/` now defaults to v3).
# Retained as a back-compat alias for links minted while v3 was opt-in.
return FileResponse(str(STATIC_DIR / "v3" / "index.html"))


@app.get("/v2")
def index_v2():
# Always serve the classic v2 UI, independent of the env var, so the
# fallback is reachable without flipping FEEDBACK_UI.
return FileResponse(str(STATIC_DIR / "index.html"))
2 changes: 1 addition & 1 deletion static/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -1498,7 +1498,7 @@
// Normalize any stale 'Combo' tokens left from legacy-mode sessions.
if (_getArrangementNamingMode() === 'smart') {
filters.arrHas = _toSmartArrs(filters.arrHas);
filters.arrLacks = _toSmartArrs(filters.arrLacks);

Check warning on line 1501 in static/app.js

View workflow job for this annotation

GitHub Actions / ci / lint

File has too many lines (12165). Maximum allowed is 1500
}
return filters;
} catch {
Expand Down Expand Up @@ -9181,7 +9181,7 @@
ctrl.appendChild(bar);
// Mount OUTSIDE #player-controls (in #player-footer, or as its sibling) so
// the popover's chip <button>s can't be matched by a plugin injector that
// anchors on `#player-controls > button:last-of-type` (see static/index.html).
// anchors on `#player-controls > button:last-of-type` (see static/v3/index.html).
_mountSectionPracticeControlSafe(ctrl);
_placeSectionPracticeControlForChrome();
return bar;
Expand Down
2 changes: 1 addition & 1 deletion static/highway.js
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@
// it's only "full resolution" when Quality is HD; otherwise it pins at the
// chosen Quality. Read once here; live changes come through
// api.setMinRenderScale(), surfaced as the "Min res" control next to Quality
// in the player controls (static/index.html).
// in the player controls (static/v3/index.html).
hwState._autoScaleMin = (function () {
const v = parseFloat(localStorage.getItem('highwayMinRenderScale'));
return Number.isFinite(v) ? Math.max(_AUTO_SCALE_MIN, Math.min(1, v)) : _AUTO_SCALE_MIN;
Expand Down Expand Up @@ -1498,7 +1498,7 @@
} catch (e) {
hwState._rendererDrawFailures += 1;
console.error('renderer draw:', e);
// Self-heal: a plugin whose draw() throws every frame

Check warning on line 1501 in static/highway.js

View workflow job for this annotation

GitHub Actions / ci / lint

File has too many lines (4224). Maximum allowed is 1500
// would otherwise spam the console and leave the canvas
// blank indefinitely. After a short streak of failures,
// revert to the built-in renderer so the user at least
Expand Down
Loading
Loading