diff --git a/ui/src/audioplayer/LyricsPanel.jsx b/ui/src/audioplayer/LyricsPanel.jsx
index a998b4c5d..fb9174047 100644
--- a/ui/src/audioplayer/LyricsPanel.jsx
+++ b/ui/src/audioplayer/LyricsPanel.jsx
@@ -37,7 +37,6 @@ import { finiteTime } from './lyricsTimeline'
import useLyricsTimeline from './useLyricsTimeline'
const KARAOKE_LAYER_OPACITY_TRANSITION = `opacity ${KARAOKE_LINE_OPACITY_MS}ms ${KARAOKE_EASING}`
-const KARAOKE_AUX_LINE_TRANSITION = `color ${KARAOKE_LINE_OPACITY_MS}ms ${KARAOKE_EASING}, -webkit-text-fill-color ${KARAOKE_LINE_OPACITY_MS}ms ${KARAOKE_EASING}`
const useStyles = makeStyles((theme) => ({
root: {
@@ -123,18 +122,15 @@ const useStyles = makeStyles((theme) => ({
backgroundColor: alpha(theme.palette.text.primary, 0.055),
},
},
- '&[data-raised="true"][data-line-motion="line"]': {
+ '&[data-raised="true"][data-line-motion="line"] $line': {
transform: `translateY(-${KARAOKE_LINE_LIFT_PX}px)`,
- transition: `transform ${KARAOKE_LINE_ENTER_MS}ms ${KARAOKE_LINE_MOTION_EASING}, background-color 150ms ${KARAOKE_LINE_MOTION_EASING}`,
+ transition: `${KARAOKE_LAYER_OPACITY_TRANSITION}, transform ${KARAOKE_LINE_ENTER_MS}ms ${KARAOKE_LINE_MOTION_EASING}`,
},
'&[data-raised="true"][data-line-motion="character"][data-character-wave="false"] $line':
{
transform: `translateY(-${KARAOKE_LINE_LIFT_PX}px)`,
+ transition: `${KARAOKE_LAYER_OPACITY_TRANSITION}, transform ${KARAOKE_LINE_ENTER_MS}ms ${KARAOKE_LINE_MOTION_EASING}`,
},
- '&[data-raised="true"][data-line-motion="character"] $translationLine': {
- transform: `translateY(-${KARAOKE_LINE_LIFT_PX}px)`,
- transition: `${KARAOKE_AUX_LINE_TRANSITION}, transform ${KARAOKE_LINE_ENTER_MS}ms ${KARAOKE_LINE_MOTION_EASING}`,
- },
'&[data-active="true"]': {
'--lyrics-main-current-color':
'var(--lyrics-main-active-color, var(--lyrics-main-idle-color, currentColor))',
@@ -147,11 +143,10 @@ const useStyles = makeStyles((theme) => ({
'@media (prefers-reduced-motion: reduce)': {
transition: 'none',
transform: 'none',
- '&[data-line-motion="character"] $line, &[data-line-motion="character"] $translationLine':
- {
- transition: 'none',
- transform: 'none',
- },
+ '& $line': {
+ transition: 'none',
+ transform: 'none',
+ },
},
},
waveCharacter: {
@@ -200,7 +195,7 @@ const useStyles = makeStyles((theme) => ({
color: 'var(--lyrics-translation-current-color, currentColor)',
WebkitTextFillColor:
'var(--lyrics-translation-current-color, currentColor)',
- transition: KARAOKE_AUX_LINE_TRANSITION,
+ transition: 'none',
'@media (prefers-reduced-motion: reduce)': {
transition: 'none',
transform: 'none',
@@ -508,7 +503,7 @@ const LyricsPanel = ({
return {
opacity: 1,
color: sourceColor,
- '--lyrics-active-color': colors.main,
+ '--lyrics-active-color': sourceColor,
}
}
if (!hasTimedMainLines) {
diff --git a/ui/src/audioplayer/LyricsPanel.test.jsx b/ui/src/audioplayer/LyricsPanel.test.jsx
index 0eb18386a..64d2cfc07 100644
--- a/ui/src/audioplayer/LyricsPanel.test.jsx
+++ b/ui/src/audioplayer/LyricsPanel.test.jsx
@@ -44,13 +44,20 @@ const renderPanel = (props, selectedTheme = theme) => {
}
const parseCssColor = (value) => {
- const hex = String(value).match(/^#([0-9a-f]{6})$/i)
+ const hex = String(value).match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/i)
if (hex) {
+ const normalized =
+ hex[1].length === 3
+ ? hex[1]
+ .split('')
+ .map((channel) => channel.repeat(2))
+ .join('')
+ : hex[1]
return {
channels: [
- parseInt(hex[1].slice(0, 2), 16),
- parseInt(hex[1].slice(2, 4), 16),
- parseInt(hex[1].slice(4, 6), 16),
+ parseInt(normalized.slice(0, 2), 16),
+ parseInt(normalized.slice(2, 4), 16),
+ parseInt(normalized.slice(4, 6), 16),
],
alpha: 1,
}
@@ -347,7 +354,7 @@ describe('', () => {
expect(pronunciation.style.color).toBe('')
})
- it('uses one shared state transition for every static line layer', () => {
+ it('keeps static translations visually stable while main layers transition', () => {
renderPanel({
mainLyric,
pronunciationLyric: {
@@ -379,16 +386,14 @@ describe('', () => {
expect(window.getComputedStyle(mainRow).transition).toContain(
`opacity ${KARAOKE_LINE_OPACITY_MS}ms`,
)
- expect(window.getComputedStyle(translationRow).transition).toContain(
- `color ${KARAOKE_LINE_OPACITY_MS}ms`,
- )
+ expect(window.getComputedStyle(translationRow).transition).toBe('none')
pronunciation.forEach((token) =>
expect(token).toHaveAttribute('data-timed', 'false'),
)
expect(window.getComputedStyle(translationRow).opacity).toBe('1')
})
- it('raises a line once and keeps it elevated after release', () => {
+ it('raises the main line without moving its translation', () => {
const { rerenderPanel } = renderPanel({
mainLyric,
translationLyric: {
@@ -403,12 +408,16 @@ describe('', () => {
const translation = screen
.getByText('translated line')
.closest('[data-tokenized]')
+ const mainRow = screen.getByText('Main line').closest('[data-tokenized]')
const activeStyle = window.getComputedStyle(group)
expect(group).toHaveAttribute('data-highlight-active', 'true')
expect(group).toHaveAttribute('data-raised', 'true')
expect(group).toHaveAttribute('data-line-motion', 'line')
expect(group).toHaveAttribute('data-character-wave', 'false')
- expect(activeStyle.transform).toBe(`translateY(-${KARAOKE_LINE_LIFT_PX}px)`)
+ expect(activeStyle.transform).toBe('translateY(0)')
+ expect(window.getComputedStyle(mainRow).transform).toBe(
+ `translateY(-${KARAOKE_LINE_LIFT_PX}px)`,
+ )
expect(window.getComputedStyle(translation).transform).toBe('')
rerenderPanel({
@@ -424,13 +433,14 @@ describe('', () => {
const releasedStyle = window.getComputedStyle(group)
expect(group).toHaveAttribute('data-highlight-active', 'false')
expect(group).toHaveAttribute('data-raised', 'true')
- expect(releasedStyle.transform).toBe(
+ expect(releasedStyle.transform).toBe('translateY(0)')
+ expect(window.getComputedStyle(mainRow).transform).toBe(
`translateY(-${KARAOKE_LINE_LIFT_PX}px)`,
)
expect(window.getComputedStyle(translation).transform).toBe('')
})
- it('keeps every word-timed layer on the same rise and release lifecycle', () => {
+ it('keeps word-timed translations still while main and pronunciation rise', () => {
const settledFirstCharacterTime =
(KARAOKE_CHARACTER_RISE_MS - KARAOKE_HIGHLIGHT_LEAD_MS) / 1000
const propsAt = (currentTime) => ({
@@ -464,9 +474,7 @@ describe('', () => {
expect(group).toHaveAttribute('data-line-motion', 'character')
expect(group).toHaveAttribute('data-character-wave', 'true')
expect(window.getComputedStyle(group).transform).toBe('translateY(0)')
- expect(window.getComputedStyle(translation).transform).toBe(
- `translateY(-${KARAOKE_LINE_LIFT_PX}px)`,
- )
+ expect(window.getComputedStyle(translation).transform).toBe('')
expect(mainCharacters).toHaveLength(4)
expect(pronunciationCharacters).toHaveLength(4)
expect(mainCharacters[0].style.transform).toBe(
@@ -499,16 +507,14 @@ describe('', () => {
expect(window.getComputedStyle(releasedMainRow).transform).toBe(
`translateY(-${KARAOKE_LINE_LIFT_PX}px)`,
)
- expect(window.getComputedStyle(translation).transform).toBe(
- `translateY(-${KARAOKE_LINE_LIFT_PX}px)`,
- )
+ expect(window.getComputedStyle(translation).transform).toBe('')
const idleText = screen
.getAllByTestId('lyrics-token')[0]
.querySelector('[data-lyrics-wave-text="true"]')
expect(idleText.style.backgroundImage).toContain('linear-gradient')
})
- it('keeps the translation raised when a word-timed line wraps or unwraps', () => {
+ it('keeps the translation still when a word-timed line wraps or unwraps', () => {
let wrapped = false
const observers = []
window.ResizeObserver = class {
@@ -565,28 +571,20 @@ describe('', () => {
.closest('[data-tokenized]')
expect(row).toHaveAttribute('data-wrapped', 'false')
- expect(window.getComputedStyle(translation).transform).toBe(
- `translateY(-${KARAOKE_LINE_LIFT_PX}px)`,
- )
+ expect(window.getComputedStyle(translation).transform).toBe('')
wrapped = true
act(notifyResizeObservers)
expect(row).toHaveAttribute('data-wrapped', 'true')
- expect(window.getComputedStyle(translation).transform).toBe(
- `translateY(-${KARAOKE_LINE_LIFT_PX}px)`,
- )
+ expect(window.getComputedStyle(translation).transform).toBe('')
rerenderPanel(propsAt(1.1))
- expect(window.getComputedStyle(translation).transform).toBe(
- `translateY(-${KARAOKE_LINE_LIFT_PX}px)`,
- )
+ expect(window.getComputedStyle(translation).transform).toBe('')
wrapped = false
act(notifyResizeObservers)
expect(row).toHaveAttribute('data-wrapped', 'false')
- expect(window.getComputedStyle(translation).transform).toBe(
- `translateY(-${KARAOKE_LINE_LIFT_PX}px)`,
- )
+ expect(window.getComputedStyle(translation).transform).toBe('')
})
it('keeps detailed grapheme markup on active lines without changing token layout', () => {
@@ -841,13 +839,18 @@ describe('', () => {
).toBe(accessibleTheme.palette.text.secondary)
expect(
group.style.getPropertyValue('--lyrics-translation-active-color'),
- ).toBe(accessibleTheme.palette.text.primary)
+ ).toBe(accessibleTheme.palette.text.secondary)
expect(window.getComputedStyle(translation).opacity).toBe('1')
expect(parseCssColor(futureColor).alpha).toBe(TOKEN_FUTURE_ALPHA)
expect(contrastRatio(futureColor, background)).toBeGreaterThanOrEqual(4.5)
expect(
contrastRatio(accessibleTheme.palette.text.secondary, background),
).toBeGreaterThanOrEqual(4.5)
+ expect(
+ contrastRatio(accessibleTheme.palette.text.secondary, background),
+ ).toBeLessThan(
+ contrastRatio(accessibleTheme.palette.text.primary, background),
+ )
},
)
})
diff --git a/ui/src/audioplayer/playerBreakpoints.js b/ui/src/audioplayer/playerBreakpoints.js
index 16dde7777..72dd2e68a 100644
--- a/ui/src/audioplayer/playerBreakpoints.js
+++ b/ui/src/audioplayer/playerBreakpoints.js
@@ -1,11 +1,4 @@
-import { APP_SIDEBAR_OPEN_WIDTH } from '../layout/layoutWidths'
-import { LYRICS_SIDEBAR_MAX_WIDTH } from './lyricsSidebarWidth'
-
-export const LYRICS_SIDEBAR_MIN_CONTENT_WIDTH = LYRICS_SIDEBAR_MAX_WIDTH
-export const PLAYER_DESKTOP_MIN_WIDTH =
- APP_SIDEBAR_OPEN_WIDTH +
- LYRICS_SIDEBAR_MAX_WIDTH +
- LYRICS_SIDEBAR_MIN_CONTENT_WIDTH
+export const PLAYER_DESKTOP_MIN_WIDTH = 810
export const PLAYER_MEDIA_QUERY_STEP = 0.05
export const PLAYER_MOBILE_MAX_WIDTH =
PLAYER_DESKTOP_MIN_WIDTH - PLAYER_MEDIA_QUERY_STEP
diff --git a/ui/src/audioplayer/playerBreakpoints.test.js b/ui/src/audioplayer/playerBreakpoints.test.js
index c603665ad..4d19e5e68 100644
--- a/ui/src/audioplayer/playerBreakpoints.test.js
+++ b/ui/src/audioplayer/playerBreakpoints.test.js
@@ -1,9 +1,7 @@
import { describe, expect, it } from 'vitest'
-import { APP_SIDEBAR_OPEN_WIDTH } from '../layout/layoutWidths'
import {
LYRICS_SIDEBAR_DESKTOP_MEDIA_QUERY,
LYRICS_SIDEBAR_DESKTOP_MIN_WIDTH,
- LYRICS_SIDEBAR_MIN_CONTENT_WIDTH,
PLAYER_DESKTOP_MEDIA_QUERY,
PLAYER_DESKTOP_MIN_WIDTH,
PLAYER_MOBILE_MATCH_MEDIA_QUERY,
@@ -11,7 +9,6 @@ import {
PLAYER_MOBILE_MEDIA_QUERY,
PLAYER_MEDIA_QUERY_STEP,
} from './playerBreakpoints'
-import { LYRICS_SIDEBAR_MAX_WIDTH } from './lyricsSidebarWidth'
describe('player breakpoints', () => {
it('keeps compact and desktop player ranges from overlapping', () => {
@@ -29,15 +26,13 @@ describe('player breakpoints', () => {
)
})
- it('only opens the desktop lyrics sidebar when primary content still fits', () => {
+ it('keeps tablet layouts on the desktop player and lyrics sidebar', () => {
+ expect(PLAYER_DESKTOP_MIN_WIDTH).toBe(810)
+ expect(PLAYER_MOBILE_MAX_WIDTH).toBe(809.95)
+ expect(LYRICS_SIDEBAR_DESKTOP_MIN_WIDTH).toBe(810)
expect(LYRICS_SIDEBAR_DESKTOP_MEDIA_QUERY).toBe(
`(min-width:${LYRICS_SIDEBAR_DESKTOP_MIN_WIDTH}px)`,
)
- expect(
- LYRICS_SIDEBAR_DESKTOP_MIN_WIDTH -
- APP_SIDEBAR_OPEN_WIDTH -
- LYRICS_SIDEBAR_MAX_WIDTH,
- ).toBeGreaterThanOrEqual(LYRICS_SIDEBAR_MIN_CONTENT_WIDTH)
expect(LYRICS_SIDEBAR_DESKTOP_MIN_WIDTH).toBe(PLAYER_DESKTOP_MIN_WIDTH)
})
})