feat(v3): tabbed, card-row settings page + per-plugin settings category (#584)

Replace the single long scrolling v3 settings screen with a horizontal tab
bar (Gameplay / Audio / Graphics / Keybinds / Progression / Mic / Plugins /
System) over card rows (icon + title + description, control on the right) with
a per-category Reset.

- static/v3/index.html: tab bar + card-row markup (ids keep hydrating through
  the unchanged app.js loadSettings()/persistSetting() path).
- static/v3/settings.js (new): tab switching + active-tab persistence
  (localStorage 'v3-settings-tab'), per-category reset, read-only Keybinds
  reference from window.getAllShortcuts().
- static/v3/v3.css: plain CSS, no Tailwind rebuild.
- Per-plugin settings tab: new optional settings.category in plugin.json →
  plugins/__init__.py surfaces settings_category; app.js mounts each plugin
  <details> into #plugin-settings-<category> (fallback: Plugins tab).
  highway_3d ships category: "graphics".
- New gameplay settings: countdown_before_song (wired end-to-end, default off);
  miss_penalty + fail_behavior (persist-only stubs); "Note highway speed"
  surfaces existing master_difficulty.
- New POST /api/settings/reset clears whitelisted keys back to defaults.

Tests: test_settings_api.py, test_plugins.py::test_settings_category_parsed_from_manifest,
tests/browser/settings-tabbed.spec.ts. 179 passed locally.

Ported from the pre-rename feat/v3-settings-tabbed WIP onto current main
(slopsmith→feedBack rename applied; settings-screen markup conflict resolved
in favour of the new tabbed layout — all prior setting ids preserved).

Closes #579

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Byron Gamatos
2026-06-23 18:06:46 +02:00
committed by GitHub
co-authored by Claude Opus 4.8
parent f3a5cb9ed3
commit 3b485fe62b
11 changed files with 1176 additions and 183 deletions
+29
View File
@@ -4006,3 +4006,32 @@ def test_loader_honors_persisted_disable_without_memory_flip(tmp_path, reset_plu
assert not (config_dir / "z_marker").exists()
assert plugins.PENDING_PLUGINS["z_target"]["status"] == "disabled"
assert plugins.PENDING_PLUGINS["z_target"]["enabled"] is False
def test_settings_category_parsed_from_manifest(tmp_path, reset_plugin_state):
"""A plugin manifest's settings.category is parsed into settings_category
on the loaded entry (drives the v3 settings-tab placement). Absent or a
bare-string `settings` value yields None → the frontend's fallback tab."""
plugins = reset_plugin_state
def _write(pid, settings_value):
d = tmp_path / pid
d.mkdir()
(d / "plugin.json").write_text(json.dumps({
"id": pid, "name": pid, "routes": "routes.py", "settings": settings_value,
}))
(d / "routes.py").write_text("def setup(app, ctx):\n pass\n")
_write("graphy", {"html": "settings.html", "category": "graphics"})
_write("plainset", {"html": "settings.html"}) # dict, no category
_write("noset", None) # no settings at all
_run_load_plugins(plugins, type("FakeApp", (), {})(), tmp_path)
rows = {p["id"]: p for p in plugins.LOADED_PLUGINS}
assert rows["graphy"]["settings_category"] == "graphics"
assert rows["graphy"]["has_settings"] is True
assert rows["plainset"]["settings_category"] is None
assert rows["plainset"]["has_settings"] is True
assert rows["noset"]["settings_category"] is None
assert rows["noset"]["has_settings"] is False