Skip to content

feat(player): unified player-bar layout panel + EQ presets + cover-click action#122

Merged
InstaZDLL merged 4 commits into
mainfrom
feat/playerbar-layout-customization
May 23, 2026
Merged

feat(player): unified player-bar layout panel + EQ presets + cover-click action#122
InstaZDLL merged 4 commits into
mainfrom
feat/playerbar-layout-customization

Conversation

@InstaZDLL
Copy link
Copy Markdown
Owner

@InstaZDLL InstaZDLL commented May 23, 2026

Closes #119, closes #120.

Summary

Per the user's follow-up on #119, this is a single PR that:

The user picked fixed order and icon grid preview in the design discussion, both of which this PR implements.

New / changed components

File Role
`src/hooks/usePlayerBarLayout.ts` NEW — single source of truth for every per-button visibility + cover-click action. Re-reads on `waveflow:playerbar-layout-changed` (new unified event) and on the legacy per-feature events for back-compat.
`src/components/player/EqPresetButton.tsx` NEW — icon button (primary slot) + popover. Exports a shared `EqPresetPanel` body.
`src/components/views/settings/PlayerBarLayoutCard.tsx` NEW — Settings panel with live icon-grid preview, 9 show/hide toggles, 3-option cover-action radio.
`src/components/player/PlayerBar.tsx` Each conditional now reads from the hook; cover-click switches on `layout.coverAction`.
`src/components/player/MoreActionsMenu.tsx` New `pinEqPreset` / `showEq` props; EQ section renders inline via the shared `EqPresetPanel` when the pin is off.
`src/components/views/SettingsView.tsx` Removed the 3 standalone toggles + their handlers + their hydration; replaced by a single `` mount.

Setting keys

All per-profile, all default to match the pre-customisation layout (zero visible change on upgrade):

Key Default Notes
`ui.show_lyrics` true NEW
`ui.show_queue` true NEW
`ui.show_device` true NEW
`ui.show_mini_player` true NEW
`ui.show_immersive` true NEW (#119)
`ui.show_eq_preset` false NEW (#120)
`ui.show_sleep_timer` false EXISTING — kept
`ui.show_ab_loop` false EXISTING — kept
`ui.show_audio_quality_footer` false EXISTING — kept
`ui.cover_action` `immersive` NEW — `immersive` / `now_playing` / `none`

i18n

12 new keys propagated to all 17 locales (real translations, not placeholders — verified the same way as previous PRs via a one-shot Node script). Brand tokens preserved (`Spotify`, `EQ`).

Validation

  • `bun run lint` ✅
  • `bun run typecheck` ✅
  • `cargo check --all-targets` ✅ (no Rust changes but ran for sanity)

Test plan

Defaults / migration

  • Fresh install or existing user upgrading: player bar looks exactly like before (Mini-player + Immersive icon visible, A-B / Sleep / EQ / Audio-quality footer hidden by default, cover click opens immersive view)
  • No regression in Spotify mode: mini-player / device / EQ / speed / cover-click still hide where they used to

Settings panel

  • Open Settings → Apparence → "Disposition de la barre de lecture"
  • Preview row shows the currently-visible icons in render order
  • Toggle each button → preview + player bar both update live; new state survives a profile switch and back
  • Toggle every button OFF → preview shows "empty" message; player bar shows only cover + center playback controls (and the volume slider since it's not in this grid)
  • Cover action radio: switch to `now_playing` → clicking the cover opens / closes the right Now Playing panel (Spotify behaviour); switch to `none` → cover click is disabled (cursor stays default, no popover); switch back to `immersive` → opens fullscreen overlay (original behaviour)

EQ pinning (#120)

  • EQ pin OFF (default): open the "⋯" overflow → see EQ section with bypass switch + preset list; pick a preset → backend EQ updates immediately; menu closes
  • EQ pin ON: primary EQ icon appears in the bar before Lyrics; click → popover above with the same bypass + presets
  • Change a preset from the full EqualizerCard in Settings while popover is closed; reopen popover → new active preset highlighted

Immersive dedup (#119)

  • With `show_immersive` OFF: icon button disappears from the bar; cover-click (when `cover_action = immersive`) still works as the single Immersive entry point
  • With `cover_action = none` AND `show_immersive` OFF: Immersive view is unreachable from the bar (only via Settings); regression risk acknowledged for users who hide both

Legacy compatibility

  • An external tool dispatching `waveflow:sleep-timer-visibility` (or A-B / audio-quality variants) still triggers a re-read in the player bar

Summary by CodeRabbit

  • New Features

    • Préréglages d’égaliseur accessibles depuis la barre de lecture (popover) avec activation/désactivation et sélection.
    • Personnalisation de la disposition de la player bar (tiers Primary/Overflow/Pinnable, auto-hide du menu overflow).
    • Action configurable au clic sur la pochette (immersive / panneau Now Playing / désactivé).
  • Refactor

    • Centralisation des contrôles de visibilité de la player bar dans un panneau unique "Apparence" et aperçu unifié.
  • Documentation

    • Mise à jour de la documentation UI pour la nouvelle disposition de la player bar.
  • Chores

    • Ajout étendu de traductions pour les nouvelles options (multi-langues).

Review Change Stack

…ick action

Closes #119 (duplicate Immersive button) and #120 (EQ access from
the player bar), plus introduces the customisation infrastructure
the user asked for in the #119 follow-up discussion.

## What changed

- New `usePlayerBarLayout` hook centralises every player-bar
  visibility setting + the cover-click action behind a single
  `waveflow:playerbar-layout-changed` window event. Legacy
  per-feature events (`waveflow:sleep-timer-visibility`,
  `waveflow:ab-loop-visibility`,
  `waveflow:audio-quality-footer-visibility`) are still observed
  so any external dispatcher keeps working.
- New `EqPresetButton` + shared `EqPresetPanel` body: bypass toggle
  + the 20 built-in presets. Rendered as a popover from a primary
  pin slot, or inline in the `MoreActionsMenu` overflow when not
  pinned. Hydrates from `playerGetEq` lazily so it reflects any
  edit made through the full EqualizerCard.
- New `PlayerBarLayoutCard` Settings panel replaces the three
  earlier per-feature toggles (sleep timer / A-B loop / audio
  quality footer) with a unified show/hide grid covering every
  button + a live icon-grid preview + a radio selector for the
  cover thumbnail action (immersive / now-playing panel / none).
- `PlayerBar` now reads every conditional from the hook. Defaults
  match the pre-customisation layout exactly so existing users see
  no visible change until they visit Settings → Apparence.
- The duplicate "Immersive" entry from #119 is resolved through
  the new visibility grid: the always-on icon button + the cover
  click are independent toggles now, both default to on, and users
  who want a single entry simply toggle the icon off.
- `MoreActionsMenu` gains an EQ section (rendered only when the
  EQ pin is OFF) and a `pinEqPreset` prop so the menu hides the
  section once the user promotes EQ to the primary bar.

## Backwards compatibility

- Setting keys reuse the existing `ui.show_*` pattern (the legacy
  `ui.show_sleep_timer` / `ui.show_ab_loop` /
  `ui.show_audio_quality_footer` stay live; new keys
  `ui.show_lyrics` / `ui.show_queue` / `ui.show_device` /
  `ui.show_mini_player` / `ui.show_immersive` / `ui.show_eq_preset`
  default to match historical behaviour, and `ui.cover_action`
  defaults to `immersive`).
- No DB migration needed: defaults are baked into the hook so
  unset keys behave exactly like before.

## i18n

12 new keys propagated to all 17 locales:
- `playerBar.eqPreset`, `playerBar.eq.bypass`,
  `playerBar.toggleNowPlayingPanel`
- `settings.playerBarLayout.{title, subtitle, preview, previewEmpty}`
- `settings.playerBarLayout.buttons.{abLoop, sleepTimer, eqPreset,
  lyrics, queue, device, miniPlayer, immersive, audioQualityFooter}`
- `settings.playerBarLayout.coverAction.{title, immersive,
  nowPlaying, none}` each with `label` + `description`

The legacy `settings.showSleepTimer` / `showAbLoop` /
`showAudioQualityFooter` keys are left in the JSONs (orphans) so
any external tooling that cached them still resolves; they can be
swept by a later cleanup PR.

## Docs

CLAUDE.md UI catalogue + `docs/features/ui.md` player-bar-layout
section rewritten to reflect the unified panel + the cover-click
action selector + the EQ-pin tier.
@InstaZDLL InstaZDLL added scope: frontend React/Vite frontend (src/) scope: i18n Translations (src/i18n/) scope: docs Docs, README, assets type: feat New feature size: xl > 500 lines labels May 23, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 23, 2026

Caution

Review failed

Pull request was closed or merged during review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: e46b9417-fcd1-42dd-87c4-263cf397de40

📥 Commits

Reviewing files that changed from the base of the PR and between 50046d6 and 4034b0c.

📒 Files selected for processing (1)
  • src/components/player/PlayerBar.tsx

📝 Walkthrough

Walkthrough

Centralise la configuration de la Player bar via usePlayerBarLayout, ajoute PlayerBarLayoutCard dans Settings, expose les presets EQ (EqPresetButton/Panel) et refactore PlayerBar / MoreActionsMenu pour consommer le layout centralisé ; i18n et docs mis à jour.

Changes

Gestion centralisée du layout et EQ

Layer / File(s) Summary
Hook et types centralisés
src/hooks/usePlayerBarLayout.ts
Types CoverAction et PlayerBarLayout, constantes PLAYER_BAR_LAYOUT_KEYS/COVER_ACTION_KEY/PLAYER_BAR_LAYOUT_EVENT, hook usePlayerBarLayout qui charge/normalise les settings de profil, gère rafraîchissement par événements et annulation au démontage.
Composant EQ Preset (button + panel)
src/components/player/EqPresetButton.tsx
EqPresetButton (popover) et EqPresetPanel : hydrate l'EQ depuis le backend, détecte le preset actif (tolérance ±0.01 dB), gère activation/basculage avec rollback UI et appelle onPick?() après sélection.
Adaptation MoreActionsMenu pour EQ
src/components/player/MoreActionsMenu.tsx
Ajout des props pinEqPreset/showEq, logique showEqInMenu, import d'icône SlidersHorizontal et rendu conditionnel d’un bloc EQ presets dans l’overflow.
Intégration PlayerBar
src/components/player/PlayerBar.tsx
PlayerBar utilise usePlayerBarLayout() au lieu d'états locaux, gère layout.coverAction, rend les contrôles conditionnellement (layout.show*) et intègre EqPresetButton; passe les props de pinning à MoreActionsMenu.
Panneau de configuration unifiée
src/components/views/settings/PlayerBarLayoutCard.tsx
PlayerBarLayoutCard rend l’aperçu des boutons visibles, une grille de toggles persistés via setProfileSetting (avec busyKey) et des radios pour coverAction (immersive/now_playing/none), puis dispatch PLAYER_BAR_LAYOUT_EVENT après écritures.
Nettoyage SettingsView
src/components/views/SettingsView.tsx
Suppression des useState/handlers locaux pour sleep timer / A-B loop / audio-quality-footer ; remplacement par l’import et le rendu de PlayerBarLayoutCard dans la section Playback.
Documentation et traductions
CLAUDE.md, docs/features/ui.md, src/i18n/locales/*.json
Mise à jour des docs pour documenter Settings → Playback et usePlayerBarLayout, et ajout/extension des clés i18n playerBar.* et settings.playerBarLayout dans les locales.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant PlayerBar
  participant usePlayerBarLayout
  participant PlayerBarLayoutCard
  participant ProfileAPI
  
  rect rgba(0, 200, 100, 0.5)
    Note over User,ProfileAPI: Première visite / montage
    PlayerBar->>usePlayerBarLayout: appel du hook
    usePlayerBarLayout->>ProfileAPI: getProfileSetting(PLAYER_BAR_LAYOUT_KEYS)
    ProfileAPI-->>usePlayerBarLayout: valeurs du profil
    usePlayerBarLayout->>usePlayerBarLayout: normalise + fallback DEFAULTS
    usePlayerBarLayout-->>PlayerBar: PlayerBarLayout{show*, coverAction}
    PlayerBar->>PlayerBar: render conditionnels basés sur layout
  end
  
  rect rgba(100, 150, 200, 0.5)
    Note over User,ProfileAPI: Utilisateur change paramètres
    User->>PlayerBarLayoutCard: toggle bouton ou coverAction
    PlayerBarLayoutCard->>ProfileAPI: setProfileSetting(key, value)
    ProfileAPI-->>PlayerBarLayoutCard: succès
    PlayerBarLayoutCard->>PlayerBarLayoutCard: dispatchEvent(PLAYER_BAR_LAYOUT_EVENT)
    usePlayerBarLayout->>usePlayerBarLayout: event listener déclenché
    usePlayerBarLayout->>ProfileAPI: refresh via getProfileSetting
    usePlayerBarLayout-->>PlayerBar: nouvel objet PlayerBarLayout
    PlayerBar->>PlayerBar: re-render avec nouvelle config
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • InstaZDLL/WaveFlow#85: chevauchements sur clés i18n chinoises liées au playerBar / now-playing / EQ.
  • InstaZDLL/WaveFlow#83: changements de structure de SettingsView susceptibles de chevaucher l'insertion de PlayerBarLayoutCard.
  • InstaZDLL/WaveFlow#18: modifie la logique de pin/overflow et touche PlayerBar / MoreActionsMenu — risque de conflit fonctionnel.

Poem

🎶 Un hook qui fédère, une carte qui ordonne,
Les presets apparaissent, le popover s’étonne.
Les toggles s’unissent, la barre reprend sens,
Un dispatch fait bouger l’état, le player reprend cadence.
Bravo, petit refactor — écoute en confiance.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 55.56% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed Le titre suit Conventional Commits avec un scope valide (player) et résume avec précision les trois changements majeurs : unification du layout, accès EQ, et action de clic sur la cover.
Description check ✅ Passed La description est complète : elle énumère les fichiers modifiés avec leurs rôles, liste les clés de paramètres avec valeurs par défaut, détaille le plan de test couvrant migration, panneau Settings, EQ, immersive et compatibilité rétro.
Linked Issues check ✅ Passed Les deux problèmes sont résolus : #119 (bouton Immersive dupliqué) → contrôle de visibilité via hook ; #120 (accès EQ rapide) → EqPresetButton pinnable + panneau EQ dans le menu débordant.
Out of Scope Changes check ✅ Passed Tous les changements sont centrés sur le layout unified du player bar, l'EQ, et la cover-action. Les 17 fichiers i18n sont justifiés (12 clés nouvelles propagées). Aucun changement hors scope détecté.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/playerbar-layout-customization

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@CLAUDE.md`:
- Line 96: Replace the French label "Settings → Apparence" with the correct
English "Settings → Appearance" in the CLAUDE.md text where the Player bar /
PlayerBarLayoutCard and usePlayerBarLayout description appears; ensure any other
occurrences in the nearby description (mentions of PlayerBarLayoutCard,
usePlayerBarLayout, EqPresetPanel, ProfileSelectorModal, rename_profile,
LibraryContext.loadedProfileId, lib.rs) are updated to use "Appearance" so the
English doc is consistent.

In `@docs/features/ui.md`:
- Line 119: Replace the French label "Settings → Apparence" with the correct
English UI label "Settings → Appearance" in the docs text string shown (the
table cell that currently reads 'Settings → Apparence'); also scan other
occurrences of the exact phrase "Settings → Apparence" (e.g., within
docs/features/ui.md content and any related UI instructions) and update them to
"Settings → Appearance" so the doc matches the English UI labels referenced by
usePlayerBarLayout.

In `@src/components/views/settings/PlayerBarLayoutCard.tsx`:
- Around line 52-66: The writeBool callback declares and invokes an unused
optimistic?: () => void parameter (in function writeBool) which is dead code;
remove the optimistic parameter from the writeBool signature and delete the
optimistic?.() invocation inside the function, and ensure any internal or future
call sites to setProfileSetting/PLAYER_BAR_LAYOUT_EVENT still call writeBool
with just (key, value) — update references to the writeBool symbol accordingly
so signatures match.

In `@src/i18n/locales/zh-TW.json`:
- Around line 1513-1521: The zh-TW translation under playerBarLayout.buttons
contains simplified Chinese characters; update each value for the keys "abLoop",
"sleepTimer", "eqPreset", "lyrics", "queue", "device", "miniPlayer", and
"immersive" to use Traditional Chinese orthography (e.g., convert simplified
variants to their traditional equivalents) so the zh-TW locale uses only
Traditional Chinese strings; keep keys unchanged and ensure punctuation/spacing
matches surrounding entries.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: d92d720f-305b-4a53-9f6d-71185dc8734d

📥 Commits

Reviewing files that changed from the base of the PR and between d444339 and 100fb45.

📒 Files selected for processing (25)
  • CLAUDE.md
  • docs/features/ui.md
  • src/components/player/EqPresetButton.tsx
  • src/components/player/MoreActionsMenu.tsx
  • src/components/player/PlayerBar.tsx
  • src/components/views/SettingsView.tsx
  • src/components/views/settings/PlayerBarLayoutCard.tsx
  • src/hooks/usePlayerBarLayout.ts
  • src/i18n/locales/ar.json
  • src/i18n/locales/de.json
  • src/i18n/locales/en.json
  • src/i18n/locales/es.json
  • src/i18n/locales/fr.json
  • src/i18n/locales/hi.json
  • src/i18n/locales/id.json
  • src/i18n/locales/it.json
  • src/i18n/locales/ja.json
  • src/i18n/locales/ko.json
  • src/i18n/locales/nl.json
  • src/i18n/locales/pt-BR.json
  • src/i18n/locales/pt.json
  • src/i18n/locales/ru.json
  • src/i18n/locales/tr.json
  • src/i18n/locales/zh-CN.json
  • src/i18n/locales/zh-TW.json

Comment thread CLAUDE.md Outdated
Comment thread docs/features/ui.md Outdated
Comment thread src/components/views/settings/PlayerBarLayoutCard.tsx Outdated
Comment thread src/i18n/locales/zh-TW.json
@InstaZDLL InstaZDLL self-assigned this May 23, 2026
…bels in docs (#122 review)

Three of the four review findings were valid:

- `PlayerBarLayoutCard.writeBool` declared an `optimistic?: () =>
  void` parameter and called `optimistic?.()` inside, but no call
  site ever passed one. Dead code. Removed both.
- CLAUDE.md + docs/features/ui.md referred to the Settings tab as
  "Settings → Apparence" (French label) in English documentation
  that elsewhere uses "Appearance" — replaced for consistency with
  the en.json label and the existing wording (`library /
  playback / integrations / appearance / …`). Also fixed the same
  drift in three code comments I introduced in this PR
  (PlayerBar.tsx, PlayerBarLayoutCard.tsx).

Skipped: the suggestion to rewrite the zh-TW `playerBarLayout
.buttons.*` strings to "use only Traditional Chinese". Side-by-
side comparison with the zh-CN equivalent shows the values ARE
already Traditional: 循環 (vs 循环), 計時器 (vs 定时), 預設 (vs
预设), 歌詞 (vs 歌词), 佇列 (vs 队列), 音訊輸出 (vs 音频输出),
沉浸式檢視 (vs 沉浸视图), 音質資訊列 (vs 音质信息条). Only
迷你播放器 matches both because none of its five characters
have simplified variants.
@InstaZDLL
Copy link
Copy Markdown
Owner Author

InstaZDLL commented May 23, 2026

@coderabbitai applied 3 of 4 findings in cabeb98:

  • Dead optimistic param removed from PlayerBarLayoutCard.writeBool — confirmed no call site ever passed one.
  • ApparenceAppearance in CLAUDE.md, docs/features/ui.md (2 occurrences in the table), and 3 code comments I introduced in this PR. The English docs were already using appearance elsewhere (e.g. CLAUDE.md catalogue list library / playback / integrations / appearance / data / shortcuts / diagnostics); my French label was the outlier.

Skipped: the suggestion to convert the zh-TW playerBarLayout.buttons.* strings to Traditional Chinese. Side-by-side check with zh-CN.json confirms the values are already Traditional:

Key zh-CN (Simplified) zh-TW (mine)
abLoop A-B 循 A-B 循
sleepTimer 睡眠定时 睡眠計時器
eqPreset 均衡器(预设 等化器(預設
lyrics
queue
device 频输 訊輸
immersive 沉浸视图 沉浸式檢視
audioQualityFooter 信息 質資訊

Only 迷你播放器 matches both scripts because its five characters (迷, 你, 播, 放, 器) have no simplified variants.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 23, 2026

Tip

For best results, initiate chat on the files or code changes.

@InstaZDLL Merci pour la vérification minutieuse. Les 3 fixes sont bien confirmés.

Sur le zh-TW : ta table est conclusive — les variantes traditionnelles (環, 計時器, 等化器/預設, 詞, 佇列, 訊輸出, 沉浸式檢視, 質資訊列) sont correctement distinctes du simplifié. Ma suggestion était une fausse alarme ; il n'y avait rien à corriger.

🐇 ✓


✏️ Learnings added
Learnt from: InstaZDLL
URL: https://github.com/InstaZDLL/WaveFlow/pull/122

Timestamp: 2026-05-23T18:39:27.628Z
Learning: In WaveFlow (InstaZDLL/WaveFlow), the zh-TW locale translations use proper Traditional Chinese characters distinct from zh-CN Simplified. For example: 環 (vs 环), 計時器 (vs 定时), 等化器/預設 (vs 均衡器/预设), 詞 (vs 词), 佇列 (vs 队列), 訊輸出 (vs 频输出), 沉浸式檢視 (vs 沉浸视图), 質資訊列 (vs 质信息条). Only characters with no simplified variants (e.g. 迷你播放器) are identical between the two locales. Do not flag zh-TW strings as "not Traditional Chinese" without verifying against zh-CN.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

…back (#122)

The card was mounted in the Library settings panel by accident (the
recon agent that scouted the original placement mis-identified the
category as Appearance, but the original toggles actually lived in
Library). The user reported the card was invisible because the
Appearance tab is still a placeholder ("Theme picker ships in v1.3")
and the actual placement was buried in Library, far from the related
playback controls.

Move `<PlayerBarLayoutCard />` to the Playback category, right after
`<ExclusiveModeCard />`, alongside `<EqualizerCard />` and the
ReplayGain / mono / WASAPI exclusive toggles — every setting it
exposes is about playback ergonomics, so this is the correct
semantic home.

Wrapping it in `px-4` matches the surrounding cards' padding pattern
(EqualizerCard uses the same wrapper).

Bonus: this avoids stepping on PR #82 (open, CONFLICTING), which
owns the upcoming Appearance theme picker that's replacing the
placeholder.

Docs and code comments updated to point at "Settings → Playback"
instead of "Settings → Appearance" everywhere (CLAUDE.md,
docs/features/ui.md, PlayerBar.tsx ×2, PlayerBarLayoutCard.tsx ×1).
@InstaZDLL
Copy link
Copy Markdown
Owner Author

@coderabbitai (and @InstaZDLL — fixing the misplacement you spotted)

Bonne prise — la card était dans le mauvais onglet. Le recon agent qui avait scouté l'emplacement original m'a mal renseigné la catégorie (il a dit "Appearance", la vérité c'était "Library"), et comme l'onglet Appearance est encore juste un placeholder ("Theme picker ships in v1.3"), la card était invisible.

Fix dans 50046d6 : <PlayerBarLayoutCard /> déplacée vers Settings → Playback, juste après <ExclusiveModeCard />, à côté de <EqualizerCard /> + ReplayGain / mono / WASAPI exclusive. C'est la bonne home sémantique (tout ce qu'elle expose concerne l'ergonomie de lecture, pas le theming visuel).

Bonus : ça évite de marcher sur PR #82 (CONFLICTING, ouvert) qui owne le futur theme picker dans Appearance.

Docs + commentaires de code rectifiés ("Settings → Appearance" → "Settings → Playback") dans CLAUDE.md, docs/features/ui.md, PlayerBar.tsx, PlayerBarLayoutCard.tsx. Lint + typecheck OK.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 23, 2026

Tip

For best results, initiate chat on the files or code changes.

@InstaZDLL Placement dans Settings → Playback est la bonne décision : tout ce qu'expose la card (visibilité des boutons, cover-click action) relève de l'ergonomie de lecture, pas du theming. Bien vu aussi pour l'évitement de la collision avec #82.

Le fix 50046d6 est propre — mise à jour cohérente de CLAUDE.md, docs, et les deux composants concernés. ✓

🐇


✏️ Learnings added
Learnt from: InstaZDLL
URL: https://github.com/InstaZDLL/WaveFlow/pull/122

Timestamp: 2026-05-23T18:49:55.474Z
Learning: In WaveFlow (InstaZDLL/WaveFlow), `PlayerBarLayoutCard` belongs in **Settings → Playback** (after `ExclusiveModeCard`, alongside `EqualizerCard`), NOT in Settings → Appearance. The Appearance tab is a placeholder reserved for the theme picker shipping in v1.3 (owned by PR `#82`). Any player-bar ergonomics controls (button visibility, cover-click action) should be placed under the Playback tab.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/components/player/PlayerBar.tsx (1)

231-244: 🧹 Nitpick | 🔵 Trivial | 💤 Low value

Bouton queue sans type="button" explicite.

Le bouton toggleQueue (ligne 232) n'a pas d'attribut type="button" contrairement aux autres boutons du composant. Par défaut, un <button> est de type submit, ce qui pourrait causer des comportements inattendus si le contexte change.

♻️ Correction suggérée
             {layout.showQueue && (
               <button
+                type="button"
                 onClick={toggleQueue}
                 aria-label={t("playerBar.queue")}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/player/PlayerBar.tsx` around lines 231 - 244, The queue toggle
button in the PlayerBar component is missing an explicit type, so it defaults to
type="submit" and can cause unintended form submissions; in the JSX for the
conditional block that checks layout.showQueue, update the button that calls
toggleQueue (the button using isQueueOpen and rendering <Menu />) to include
type="button" to match the other buttons in PlayerBar and prevent submit
behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/components/player/PlayerBar.tsx`:
- Around line 231-244: The queue toggle button in the PlayerBar component is
missing an explicit type, so it defaults to type="submit" and can cause
unintended form submissions; in the JSX for the conditional block that checks
layout.showQueue, update the button that calls toggleQueue (the button using
isQueueOpen and rendering <Menu />) to include type="button" to match the other
buttons in PlayerBar and prevent submit behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 001eca93-b594-492d-94dd-1230f1098591

📥 Commits

Reviewing files that changed from the base of the PR and between cabeb98 and 50046d6.

📒 Files selected for processing (5)
  • CLAUDE.md
  • docs/features/ui.md
  • src/components/player/PlayerBar.tsx
  • src/components/views/SettingsView.tsx
  • src/components/views/settings/PlayerBarLayoutCard.tsx

Drive-by consistency fix — the queue button was the only one in
PlayerBar without an explicit `type="button"`. No active bug (the
button isn't inside a form) but the inconsistency was pre-existing
code I kept inside the new `{layout.showQueue && ...}` conditional,
so worth normalising while I'm here.
@InstaZDLL
Copy link
Copy Markdown
Owner Author

@coderabbitai applied in 4034b0c. Tu as raison sur la cohérence — le bouton queue était le seul sans type="button" explicite. Aucun bug actif (le bouton n'est pas dans un form) mais c'était un oubli préexistant que j'ai gardé dans le nouveau conditionnel {layout.showQueue && ...}, donc autant le normaliser pendant que j'y suis.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 23, 2026

Tip

For best results, initiate chat on the files or code changes.

@InstaZDLL Normalisation propre — type="button" explicite sur tous les boutons est une bonne pratique défensive, même hors <form>. ✓

🐇

@InstaZDLL InstaZDLL merged commit 2f539ec into main May 23, 2026
12 of 13 checks passed
@InstaZDLL InstaZDLL deleted the feat/playerbar-layout-customization branch May 23, 2026 19:02
InstaZDLL added a commit that referenced this pull request May 23, 2026
Brings the themes branch up to date with the v1.2.0 → v1.3.0 main
work and applies the CodeRabbit findings on the original diff. Major
points of conflict resolution + new work in this merge:

* All ten modal backdrops now read `bg-black/80 backdrop-blur-md`
  (from PR #123) — the AnimatedModalShell default carries the blur,
  the four bespoke motion.div modals carry it in their own
  className.

* SettingsView keeps the Lokal-style category tablist from main
  (#83) and moves the theme picker into the now-active Appearance
  tab. `PlayerBarLayoutCard` also lives in Appearance now — it was
  parked under Playback while the tab was still a placeholder
  (deferred from #122 with the explicit "move when themes land"
  note).

* MoreActionsMenu / TrackPropertiesModal / BatchTagEditModal /
  ProfileSelectorModal / OnboardingModal kept their main-side
  imports and structure (delete view, profile-name step, 1080p cap,
  scroll-wheel volume, etc.) while taking the themes branch's
  framer-motion wrappers and the per-modal CodeRabbit cleanup.

CodeRabbit findings addressed in this merge:

* `index.html` ships an inline theme-bootstrap script that paints
  the right `dark` class + `data-theme` + ambient bg before React
  mounts, so a fresh boot with the dark default no longer flashes
  white (FOUC).

* `app.css` focus-visible outline reads `var(--accent-500)` instead
  of the hardcoded `#10b981`, so the focus ring follows the active
  preset (Midnight, Sunset, Crimson… were stuck on green).

* `ThemeContext.readStoredTheme` migrates the legacy
  `waveflow.theme.is_dark` boolean to the new id key on first read
  and removes the legacy entry, so a downgrade-then-upgrade cycle
  can't silently overwrite a custom preset with the stale boolean.

* `ThemeContext.setThemeId` wraps `document.startViewTransition` in
  try/catch — some WebKitGTK builds throw synchronously, which
  previously left `writeStoredTheme` persisted but `setTheme(next)`
  never called.

* `ContextMenu` and `ContextMenuSub` flip math reads
  `offsetWidth`/`offsetHeight` instead of `getBoundingClientRect()`
  so the menu mounted at `scale: 0.96` doesn't underestimate its
  size and misfire the flip near the viewport edges.

* `ProfileSelectorModal` aria-label is now computed from `view`
  (`select` / `create` / `delete`) so the screen reader announces
  the actual content, and the leftover `animate-fade-in` class on
  the create / delete cards was removed — the outer motion.div
  already owns the fade.
InstaZDLL added a commit that referenced this pull request May 24, 2026
…ates tokens

Three valid CodeRabbit findings, one skipped as contradicting the
intentional design.

Fixed:

* CoverPickerModal — both search-effect early-return branches
  (`!isOpen || tab !== "deezer"` and `trimmed.length < 2`) now also
  call setError(null) alongside setIsSearching(false). A failed
  Deezer search no longer leaves an error banner armed against the
  Local tab or against an empty query.

* CoverPickerModal — `handlePickDeezer` / `handlePickFile` capture a
  `sessionRef` token at the start and short-circuit before each
  post-await write (onSuccess / onClose / setError / setIsApplying)
  when the modal has closed mid-flight. The `!isOpen` reset bumps
  the token AND clears `isApplying` directly (same reason as the
  search spinner cleanup — the guarded finally would otherwise
  leave the loader on the next open).

* DuplicatesModal — split the single `requestIdRef` into two
  independent tokens, resolving a contradiction between two
  back-to-back CodeRabbit findings:
  (a) the delete spinner must clear on the happy path,
  (b) the Delete-others button must stay disabled across the
      post-delete rescan so a second click can't fire against
      stale `groups`.
  `scanSeqRef` is bumped per refresh() to guard concurrent scans;
  `sessionRef` is bumped only by the `!isOpen` reset so
  `handleDeleteOthers` can keep its spinner on across the embedded
  `refresh()` call and clear it only after the rescan completes
  (or the modal closes). Reverts the premature setIsDeleting(false)
  before refresh() from b63b558 — no longer needed.

Skipped:

* "PlayerBarLayoutCard back to Settings → Playback" is an
  intentional design decision the user made when the themes branch
  landed: the card was parked under Playback as a temporary
  placeholder in #122 with an explicit note to move it to Appearance
  once the picker filled that tab. CLAUDE.md line 96 documents the
  intent. Reverting would re-introduce the placeholder + scattered
  layout. Marked as not-planned.
InstaZDLL added a commit that referenced this pull request May 24, 2026
* feat(ui): animate panels/modals/menus and add 10 theme presets

framer-motion polish closes the perceived-smoothness gap with lokal:
- right panels (queue/nowplaying/lyrics) spring-animate width + opacity
  via animatepresence in applayout, swapping smoothly on toggle
- new animatedmodalshell + animatedmodalcontent primitive backs 7
  modals (createplaylist, createlibrary, coverpicker, duplicates,
  smartplaylisteditor, artistimagepicker, lyricseditor) with backdrop
  fade + content spring scale. 4 special-case modals (onboarding,
  profileselector, trackproperties, batchtagedit) use motion.div
  inline because their isopen gate is a track/trackids prop
- 6 dropdowns/menus get spring enter+exit (moreactions, sleeptimer,
  devicemenu, contextmenu primitive + submenu, topbar filters /
  search results / profile)
- main view crossfades on activeview change via animatepresence
  mode=wait

theme system extends to 10 presets while staying backward-compatible:
- src/lib/themes.ts defines emerald light/dark, oled, midnight, forest,
  sunset, lavender, crimson, ocean, neon. each has a full 50→950 oklch
  accent palette + ambient body color + light/dark mode
- :root exposes --accent-50..950 css vars; @theme inline remaps the
  emerald-* tailwind utilities to them, so every existing
  `bg-emerald-500` etc retints automatically with zero component churn
- new bg-app-ambient utility paints the applayout wrapper from
  --ambient-bg
- themecontext now tracks the full preset, exposes setthemeid() with
  optional view-transitions radial reveal, keeps isdark + toggletheme
  for the topbar button, and migrates the legacy
  waveflow.theme.is_dark localstorage key once on boot
- settings → apparence section renders a 10-card grid picker with
  live palette swatches
- i18n keys propagated across all 17 locales

* fix(themes): make sidebar + right panels follow the active theme tint

User report on PR #82: switching to "Lavender" left the sidebar pure
black against a violet body, because `Sidebar` / `QueuePanel` /
`NowPlayingPanel` / `LyricsPanel` all hardcoded `dark:bg-surface-dark`
(`#121212`) or `dark:bg-zinc-900` instead of inheriting the theme's
ambient family.

`PlayerBar` / `AudioQualityFooter` already used the
`dark:bg-surface-dark-elevated` token, so making the surface tokens
theme-aware is enough to retint them too — no per-component touch
required.

Adds `surfaceDark` / `surfaceDarkElevated` to `ThemePreset` and writes
both as `--color-surface-dark` / `--color-surface-dark-elevated` on
the root in `applyTheme`. Tailwind v4 generates the `bg-surface-dark*`
utilities from those custom-property names, so any existing
`dark:bg-surface-dark` retints automatically.

Each themed dark preset sets surfaces to its ambient family (OLED stays
pitch black, Midnight indigo-ink, Sunset amber-earth, Lavender violet,
Crimson rose, Ocean deep blue, Neon fuchsia). Falls back to the v1
defaults (`#121212` / `#181818`) for the default-dark theme + any light
theme so the existing visual is preserved.

The 3 right panels (Queue / NowPlaying / Lyrics) swap their hardcoded
`dark:bg-zinc-900` for `dark:bg-surface-dark` so they benefit from the
same retint.

Forest theme + theme catalog rework (drop duplicate emerald presets,
add light variants, refine accent palettes) tracked separately so this
fix can ship as a focused commit.

* feat(themes): drop forest, add 5 light variants, refresh catalog

Catalog rework on top of the surface-theming fix (4229e11). Picker now
ships **14 presets** organised into two visual rows:

- **Light (6)** — Émeraude · Midnight · Sunset · Lavender · Crimson · Ocean
- **Dark  (8)** — Émeraude · OLED · Midnight · Sunset · Lavender · Crimson · Ocean · Neon

Changes:

* `forest` removed — its emerald accent + dark-green ambient was too
  close to `default-dark` (same accent palette, similar perceived
  background) to justify a separate slot. Surface tokens it carried
  are gone with it; nothing else in the catalog references them.
* `midnight-light` / `sunset-light` / `lavender-light` / `crimson-light`
  / `ocean-light` added, each pairing the dark variant's accent
  palette (INDIGO / AMBER / VIOLET / ROSE / SKY) with a very pale
  ambient tint of the matching hue so the whole app gets the theme
  personality without sacrificing light-mode readability.
* `neon` stays dark-only — a fluorescent fuchsia on a pale ambient
  doesn't read as a coherent design choice.

i18n: 17 locales updated in lockstep — `themes.forest` removed,
`themes.midnightLight` / `sunsetLight` / `lavenderLight` /
`crimsonLight` / `oceanLight` added. Translations follow each locale's
existing convention for the dark variant (capitalisation, articles,
diacritics).

`index.html` bootstrap script kept in sync: `LIGHT_IDS` lookup now
lists the 6 light presets explicitly so the inline pre-mount paint
knows whether to set the `dark` class, and the ambient table covers
all 14 ids. Both tables must stay in sync with `themes.ts` if a preset
moves later.

Docs: `docs/features/ui.md` gains a "Theme system" section covering
the catalog, the CSS-variable plumbing, the surface tokens, the FOUC
bootstrap, and the View Transitions fallback. CLAUDE.md UI summary
mentions the 14-preset count and the Apparence panel migration.

* fix(themes): mirror the surface tokens for light mode

Follow-up to 4229e11 (dark-mode surface theming) — same bug existed in
light mode, just mirrored: themed light presets (Sunset Light /
Lavender Light / Midnight Light / Crimson Light / Ocean Light) tinted
only the accent and the body, leaving a stark white sidebar / panels /
player bar against a pale-tinted background because those surfaces all
hardcoded `bg-white` / `bg-[#FAFAFA]`.

Adds the light-mode mirrors of the dark tokens:

* `--color-surface-light` (default `#ffffff`) and
  `--color-surface-light-elevated` (default `#fafafa`) declared in
  `app.css`'s `@theme` block so Tailwind v4 generates the
  `bg-surface-light{-elevated}?` utilities at build time.
* `ThemePreset` gains optional `surfaceLight` / `surfaceLightElevated`
  fields; `applyTheme` writes both CSS vars on `<html>` (always
  re-set, so a swap back to default clears any custom override).
* Each themed light preset sets `surfaceLight = ambient` so sidebar /
  panels match the body and the theme reads as one surface, plus
  `surfaceLightElevated = #ffffff` so the player bar pops cleanly
  above the tinted body.
* Sidebar, QueuePanel, NowPlayingPanel, LyricsPanel swap their
  hardcoded `bg-white` for `bg-surface-light`.
* PlayerBar and AudioQualityFooter swap their hardcoded
  `bg-[#FAFAFA]` for `bg-surface-light-elevated`.

Net result: every theme — light or dark — now tints sidebar + panels +
player bar with the same family as the body, no remaining hardcoded
neutrals on the body-level surfaces.

* fix(themes): center column was masking the ambient + bump light tints

Two follow-ups after the light-surface mirror (516972a):

* AppLayout's center column used `bg-zinc-50` (fully opaque white) so
  on themed light presets the main scrollable area covered the body's
  amber / lavender / etc ambient with a flat white rectangle — the
  user reported "1 single part is yellow" because only the playlist
  header card (using `bg-accent-500/10` overlay) actually showed the
  amber tint. Switching to `bg-zinc-50/50` mirrors the existing
  `dark:bg-zinc-900/50` pattern: the ambient bleeds through, the
  subtle cool overlay still gives the column a slight separation
  from the sidebar.

* Light ambients bumped from the Tailwind "-50" shade to "-100". The
  "-50" range (`#fff7ed`, `#f4f6ff`, `#faf5ff`, `#fff1f2`, `#eff6ff`)
  was so close to pure white that the theme was effectively invisible
  outside the accent buttons. "-100" gives a clearly readable tint
  without veering into saturated sticky-note territory:

  | Theme           | -50 (old) | -100 (new) |
  |-----------------|-----------|------------|
  | midnight-light  | `#f4f6ff` | `#e0e7ff`  |
  | sunset-light    | `#fff7ed` | `#fef3c7`  |
  | lavender-light  | `#faf5ff` | `#ede9fe`  |
  | crimson-light   | `#fff1f2` | `#ffe4e6`  |
  | ocean-light     | `#eff6ff` | `#e0f2fe`  |

  `surfaceLight` updated in lockstep on each preset, and the inline
  `index.html` bootstrap script's ambient table now mirrors the new
  values so the pre-mount paint matches what `applyTheme` will set
  half a tick later.

Default Émeraude Light is unchanged — it has no ambient override and
falls back to `#ffffff`.

* fix(themes): let the player bar inherit the light-theme tint

Previous commit (bf5281e) set `surfaceLightElevated = #ffffff` on the 5
themed light variants thinking a pure-white player bar / audio quality
footer would "pop" cleanly above the tinted body. In practice it
broke the unified theme look — user pointed out the player bar was
the only surface NOT carrying the lavender / amber / etc tint.

Align `surfaceLightElevated = ambient` so all three surfaces (body,
sidebar + right panels, player bar + footer) share the same tinted
value. Separation between them comes from the borders that already
exist (`border-t` on the player bar, `border-r` on the sidebar,
`border-l` on the right panels) plus the slight cool blend of
`bg-zinc-50/50` on the center column.

Default Émeraude Light is unchanged — no overrides, surfaceLightElevated
falls back to the `#fafafa` default which still gives a subtle elevation
contrast against pure white.

* fix(modals): guard stale Deezer search + reset cover menu on close

Two CodeRabbit findings on PR #82 — both still-valid against the
post-merge code, fixes are minimal and self-contained.

* `CoverPickerModal.tsx` — the debounced Deezer search effect could
  fire `setResults` / `setError` / `setIsSearching` after the user
  closed the modal or moved on to a new query. `AnimatedModalShell`
  keeps the component mounted across its exit animation, so the
  request that was in-flight when the modal closed would still land
  and clobber the freshly-reset state. Adds a local `cancelled` token
  set in the effect cleanup and checked before each setter.

* `CreatePlaylistModal.tsx` — `coverMenuOpen` was never reset when
  the modal closed. Call sites (Sidebar, LibraryView, PlaylistView,
  every detail view) mount the component unconditionally and toggle
  it via the `isOpen` prop, so the state survives close → reopen and
  the menu would silently reappear on the next launch with its
  click-outside listener still attached. The existing reset effect
  (`useEffect(() => if (!isOpen) ...)`) gains a `setCoverMenuOpen(false)`
  call; the dedicated click-outside listener auto-detaches when the
  value flips to false, so no extra cleanup is needed.

* fix(themes): keep player bar white, tint search/pills/dropdowns instead

User feedback on the previous attempt to theme `surfaceLightElevated`:
the playback controls became hard to read against a tinted player bar.
Roll the player bar + audio quality footer back to `bg-white` and
narrow the theming to the surfaces that were actually flagged as
"why isn't this ambient?": search bar, Settings category pills,
profile dropdown menu, and the two TopBar popovers (filters + search
results).

Revised surface model for themed light presets:

* `surfaceLight = ambient` (-100 shade) — sidebar, right panels,
  search bar, Settings pills, profile menu, TopBar popovers. The
  entire body-level + interactive-pill UI shares one tinted canvas.
* `surfaceLightElevated` left at the default (`#fafafa`) — no themed
  preset overrides it anymore, the property is kept on `ThemePreset`
  for future presets that might want a separate elevated tier.
* `PlayerBar` + `AudioQualityFooter` keep their hardcoded `bg-white`
  in light mode (the original `#FAFAFA` rolled back to pure white for
  cleaner contrast against the tinted body).

`TopBar.tsx` swaps 4 hardcoded `bg-white` for `bg-surface-light` (search
input, filters popover, search-results popover, profile dropdown).
`SettingsView.tsx` swaps the inactive category pill's `bg-white` for
`bg-surface-light` (the active variant already uses the themed
`bg-emerald-50` which retints automatically).

* revert(themes): roll back the search/pills/dropdown tints

User-tested the cfaa7b7 attempt and confirmed the original split was
the right call: tinting the search bar, Settings pills, profile menu
and TopBar popovers hurt the contrast of the controls inside them
(same issue as the player bar that just rolled back to white).

Restores `bg-white` on:
- TopBar search input
- TopBar filters popover
- TopBar search results popover
- TopBar profile dropdown menu
- SettingsView inactive category pill

Net surface model now:
- Body / sidebar / right panels = theme-tinted via `bg-surface-light`
- Player bar, audio quality footer, search bar, pills, dropdown
  popovers = pure white in light mode for control contrast
- Borders + the slight `bg-zinc-50/50` blend on the center column
  still provide visual separation between the tinted body and the
  white interactive elements.

* fix: address remaining CodeRabbit findings on PR #82

Three valid issues fixed, three findings skipped as invalid against
the current code.

Fixed:

* `CLAUDE.md` line 96 — two occurrences of "Settings → Apparence"
  rewritten as "Settings → Appearance" so the English overview
  doesn't mix the French UI label into prose. The localised label
  itself ("Apparence" in fr.json) stays unchanged.

* `AnimatedModalShell` declared a `className?: string` prop but the
  component never destructured or applied it; no caller passed it
  either (grepped — every AnimatedModalShell call site under
  `src/components/common/` only sets `isOpen` / `onBackdropClick`).
  Dropped the dead prop from the interface; if a future caller needs
  per-instance class targeting, `backdropClassName` covers the
  backdrop and `AnimatedModalContent` already accepts `className`.

* `ProfileSelectorModal` swapped views instantly because the outer
  `AnimatePresence` only animated the backdrop. Added an inner
  `AnimatePresence mode="wait"` around the three conditional view
  blocks (select / create / delete), each wrapped in a `motion.div`
  with a unique key + per-view enter/exit (opacity for select, fade +
  light scale for the centered create/delete cards). The outgoing
  view finishes its exit before the incoming one mounts, which is
  the standard cross-fade pattern for stepping through nested modal
  states.

Skipped (still-valid CodeRabbit checks but the underlying issue is
already resolved or the suggestion doesn't apply):

* `docs/features/ui.md` claims `PlayerBarLayoutCard` is under
  Settings → Playback. Verified against `SettingsView.tsx`
  (line 2487, inside the `activeCategory === "appearance"` panel) —
  the card IS in Appearance, the doc is correct. The finding is
  reading a pre-merge snapshot of the file.

* The stylelint Tailwind v4 at-rules suggestion has no setup to
  amend — the repo doesn't ship a `.stylelintrc*` and stylelint
  isn't a dependency. Nothing to ignoreAtRules in.

* fix(modals): clear stale spinner + guard duplicates scan races

Two more CodeRabbit findings on PR #82, both still-valid against the
post-merge code. Two more findings skipped as no-ops.

Fixed:

* `CoverPickerModal`: when the user deletes letters back below 2 chars
  (the search-effect's early return path), the spinner stayed on
  forever — the previous request's `.finally` short-circuits on the
  `cancelled` token to avoid clobbering newer state, but that also
  skipped the `setIsSearching(false)` it would normally run. Added an
  explicit `setIsSearching(false)` before that early return, plus
  another in the `!isOpen` reset block so a closed-mid-fetch modal
  doesn't reopen with a stale spinner.

* `DuplicatesModal`: `refresh()` could let a slow `findDuplicates`
  response land after the user closed the modal (the component is
  kept mounted by `AnimatedModalShell` for the exit animation) or
  after the user re-triggered the scan via `handleDeleteOthers`. A
  monotonic `requestIdRef` is captured at call time and checked
  before each setter — stale responses are dropped on the floor. The
  `!isOpen` reset bumps the same counter so any in-flight request
  becomes stale immediately, and clears `isScanning` for the same
  reason as the CoverPicker spinner cleanup.

Skipped (invalid against current code):

* `SettingsView.tsx` line 2432 `focus-visible:ring-emerald-500` is
  flagged as theme-mismatched, but `app.css`'s `@theme inline` block
  remaps `--color-emerald-50..950` to `--accent-*`. Tailwind v4
  derives ALL emerald-* utilities (bg, text, border, ring, outline,
  fill, stroke, focus-visible:ring) from the same token, so the
  focus ring already follows the active accent — no change needed.

* `en.json` theme keys (`defaultDark`, `midnightLight`…) flagged as
  not matching `themes.ts` ids (`default-dark`, `midnight-light`).
  These are deliberately different fields: `id` is the persisted
  kebab-case preset id (also used as the `data-theme` attribute on
  `<html>`), `labelKey` is the camelCase i18n path that `t()` looks
  up. Every `labelKey` in `themes.ts` already matches its JSON key
  character-for-character — nothing to align.

* fix(duplicates): guard handleDeleteOthers against mid-delete close

Follow-up to 18c9b52 — the staleness contract added on `refresh` only
covered the scan path. `handleDeleteOthers` could close mid-delete
(AnimatedModalShell keeps the component mounted) and still:

* fire a wasted `findDuplicates` call after `deleteTracks` returned
  (the post-delete `await refresh()` ran unconditionally), and
* land `setError` / `setIsDeleting(false)` on a closed modal.

Snapshot `requestIdRef.current` at the start of the handler and
short-circuit before each post-`await` setter / refresh call when the
captured id no longer matches the latest (i.e. the modal's `!isOpen`
effect already bumped the counter). Same pattern as `refresh` itself,
so the two call sites participate in one staleness contract.

`!isOpen` reset also clears `isDeleting` for the same reason it
already cleared `isScanning`: the guarded `finally` skips the reset
when the request is stale, so the spinner would otherwise linger on
the next open.

* fix(cover-picker): clear stale spinner when switching tabs mid-fetch

Follow-up to 18c9b52 — that commit clipped the spinner-leak in the
"close modal" and "trimmed < 2 chars" paths but missed the third
exit path: the user switching from the Deezer tab to the Local tab
while a fetch was in flight.

Sequence that broke:
1. Type "abc" on Deezer → setTimeout fires → setIsSearching(true)
   → fetch begins.
2. Click the Local tab → effect re-runs → cleanup sets
   cancelled=true.
3. New effect body hits `!isOpen || tab !== "deezer"` and returns
   early without touching `isSearching`.
4. Original fetch resolves → `.finally` short-circuits on
   `cancelled` (would clobber the new tab's state otherwise) →
   `setIsSearching(false)` never runs.
5. Loader keeps spinning on the Local tab forever.

Adds a single `setIsSearching(false)` before the early return so
the spinner is force-cleared on tab swap (and redundantly on
close — the reset effect above already handles that path, but two
cheap writes are simpler than splitting the branch).

CodeRabbit also suggested mirroring the reset in the effect
cleanup; skipped because cleanup runs on every keystroke (the
effect re-runs with the new `query`), which would blink the
spinner off-then-on across the 300 ms debounce for the normal
typing path. The early-return reset is enough to cover the actual
exit conditions.

* fix(duplicates): clear delete spinner before refresh() bumps requestId

ebe71a0's staleness guard introduced a regression I missed: refresh()
bumps requestIdRef.current internally, so on the happy path
handleDeleteOthers's finally checks N === N+1, fails, and never
calls setIsDeleting(false). The spinner stays on and the action
button stays disabled after a successful delete.

Move setIsDeleting(false) right after the post-deleteTracks
staleness check, before await refresh(). The finally guard stays
to cover the error / early-exit paths where refresh() isn't
reached.

* fix(modals): clear stale errors + guard apply handlers + split duplicates tokens

Three valid CodeRabbit findings, one skipped as contradicting the
intentional design.

Fixed:

* CoverPickerModal — both search-effect early-return branches
  (`!isOpen || tab !== "deezer"` and `trimmed.length < 2`) now also
  call setError(null) alongside setIsSearching(false). A failed
  Deezer search no longer leaves an error banner armed against the
  Local tab or against an empty query.

* CoverPickerModal — `handlePickDeezer` / `handlePickFile` capture a
  `sessionRef` token at the start and short-circuit before each
  post-await write (onSuccess / onClose / setError / setIsApplying)
  when the modal has closed mid-flight. The `!isOpen` reset bumps
  the token AND clears `isApplying` directly (same reason as the
  search spinner cleanup — the guarded finally would otherwise
  leave the loader on the next open).

* DuplicatesModal — split the single `requestIdRef` into two
  independent tokens, resolving a contradiction between two
  back-to-back CodeRabbit findings:
  (a) the delete spinner must clear on the happy path,
  (b) the Delete-others button must stay disabled across the
      post-delete rescan so a second click can't fire against
      stale `groups`.
  `scanSeqRef` is bumped per refresh() to guard concurrent scans;
  `sessionRef` is bumped only by the `!isOpen` reset so
  `handleDeleteOthers` can keep its spinner on across the embedded
  `refresh()` call and clear it only after the rescan completes
  (or the modal closes). Reverts the premature setIsDeleting(false)
  before refresh() from b63b558 — no longer needed.

Skipped:

* "PlayerBarLayoutCard back to Settings → Playback" is an
  intentional design decision the user made when the themes branch
  landed: the card was parked under Playback as a temporary
  placeholder in #122 with an explicit note to move it to Appearance
  once the picker filled that tab. CLAUDE.md line 96 documents the
  intent. Reverting would re-introduce the placeholder + scattered
  layout. Marked as not-planned.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

scope: docs Docs, README, assets scope: frontend React/Vite frontend (src/) scope: i18n Translations (src/i18n/) size: xl > 500 lines type: feat New feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: will love to use the EQ from playerbar. feat: Immersive view button are view 2 time on playerbar.

1 participant