diff --git a/config.example.jsonc b/config.example.jsonc index 6c836f5..34654c7 100644 --- a/config.example.jsonc +++ b/config.example.jsonc @@ -185,10 +185,10 @@ "wordSpacing": 0, // Word spacing setting. "fontKerning": "normal", // Font kerning setting. "textRendering": "geometricPrecision", // Text rendering setting. - "textShadow": "0 3px 10px rgba(0,0,0,0.69)", // Text shadow setting. - "backgroundColor": "transparent", // Background color setting. + "textShadow": "0 2px 4px rgba(0,0,0,0.95), 0 0 8px rgba(0,0,0,0.8), 0 0 16px rgba(0,0,0,0.55)", // Text shadow setting. + "backgroundColor": "rgba(20, 22, 34, 0.78)", // Background color setting. "backdropFilter": "blur(6px)", // Backdrop filter setting. - "fontWeight": "normal", // Font weight setting. + "fontWeight": "600", // Font weight setting. "fontStyle": "normal" // Font style setting. } // Secondary setting. }, // Primary and secondary subtitle styling. diff --git a/src/config/config.test.ts b/src/config/config.test.ts index c42ee35..612abed 100644 --- a/src/config/config.test.ts +++ b/src/config/config.test.ts @@ -64,6 +64,12 @@ test('loads defaults when config is missing', () => { 'Inter, Noto Sans, Helvetica Neue, sans-serif', ); assert.equal(config.subtitleStyle.secondary.fontColor, '#cad3f5'); + assert.equal(config.subtitleStyle.secondary.fontWeight, '600'); + assert.equal( + config.subtitleStyle.secondary.textShadow, + '0 2px 4px rgba(0,0,0,0.95), 0 0 8px rgba(0,0,0,0.8), 0 0 16px rgba(0,0,0,0.55)', + ); + assert.equal(config.subtitleStyle.secondary.backgroundColor, 'rgba(20, 22, 34, 0.78)'); assert.equal(config.immersionTracking.enabled, true); assert.equal(config.immersionTracking.dbPath, ''); assert.equal(config.immersionTracking.batchSize, 25); diff --git a/src/config/definitions/defaults-subtitle.ts b/src/config/definitions/defaults-subtitle.ts index d6792f8..6798d3d 100644 --- a/src/config/definitions/defaults-subtitle.ts +++ b/src/config/definitions/defaults-subtitle.ts @@ -50,10 +50,10 @@ export const SUBTITLE_DEFAULT_CONFIG: Pick = { wordSpacing: 0, fontKerning: 'normal', textRendering: 'geometricPrecision', - textShadow: '0 3px 10px rgba(0,0,0,0.69)', - backgroundColor: 'transparent', + textShadow: '0 2px 4px rgba(0,0,0,0.95), 0 0 8px rgba(0,0,0,0.8), 0 0 16px rgba(0,0,0,0.55)', + backgroundColor: 'rgba(20, 22, 34, 0.78)', backdropFilter: 'blur(6px)', - fontWeight: 'normal', + fontWeight: '600', fontStyle: 'normal', }, }, diff --git a/src/renderer/style.css b/src/renderer/style.css index 6804567..075372b 100644 --- a/src/renderer/style.css +++ b/src/renderer/style.css @@ -673,13 +673,17 @@ body.platform-macos.layer-visible #subtitleRoot { } #secondarySubContainer { + --secondary-sub-background-color: transparent; + --secondary-sub-backdrop-filter: none; position: absolute; top: 40px; left: 50%; transform: translateX(-50%); max-width: 80%; padding: 10px 18px; - background: transparent; + background: var(--secondary-sub-background-color, transparent); + backdrop-filter: var(--secondary-sub-backdrop-filter, none); + -webkit-backdrop-filter: var(--secondary-sub-backdrop-filter, none); border-radius: 8px; pointer-events: auto; } @@ -736,6 +740,8 @@ body.settings-modal-open #secondarySubContainer { transform: none; max-width: 100%; background: transparent; + backdrop-filter: none; + -webkit-backdrop-filter: none; padding: 40px 0 0 0; border-radius: 0; display: flex; @@ -744,6 +750,8 @@ body.settings-modal-open #secondarySubContainer { #secondarySubContainer.secondary-sub-hover #secondarySubRoot { background: transparent; + backdrop-filter: none; + -webkit-backdrop-filter: none; border-radius: 8px; padding: 10px 18px; } @@ -752,6 +760,12 @@ body.settings-modal-open #secondarySubContainer { opacity: 1; } +#secondarySubContainer.secondary-sub-hover:hover #secondarySubRoot { + background: var(--secondary-sub-background-color, transparent); + backdrop-filter: var(--secondary-sub-backdrop-filter, none); + -webkit-backdrop-filter: var(--secondary-sub-backdrop-filter, none); +} + iframe.yomitan-popup, iframe[id^='yomitan-popup'] { pointer-events: auto !important; diff --git a/src/renderer/subtitle-render.test.ts b/src/renderer/subtitle-render.test.ts index 334d0ee..0ce9d43 100644 --- a/src/renderer/subtitle-render.test.ts +++ b/src/renderer/subtitle-render.test.ts @@ -347,6 +347,58 @@ test('applySubtitleStyle sets subtitle name-match color variable', () => { } }); +test('applySubtitleStyle stores secondary background styles in hover-aware css variables', () => { + const restoreDocument = installFakeDocument(); + try { + const subtitleRoot = new FakeElement('div'); + const subtitleContainer = new FakeElement('div'); + const secondarySubRoot = new FakeElement('div'); + const secondarySubContainer = new FakeElement('div'); + const ctx = { + state: createRendererState(), + dom: { + subtitleRoot, + subtitleContainer, + secondarySubRoot, + secondarySubContainer, + }, + } as never; + + const renderer = createSubtitleRenderer(ctx); + renderer.applySubtitleStyle({ + secondary: { + backgroundColor: 'rgba(20, 22, 34, 0.78)', + backdropFilter: 'blur(6px)', + fontWeight: '600', + }, + } as never); + + const secondaryStyleValues = ( + secondarySubContainer.style as unknown as { + values?: Map; + backgroundColor?: string; + backdropFilter?: string; + } + ).values; + assert.equal( + secondaryStyleValues?.get('--secondary-sub-background-color'), + 'rgba(20, 22, 34, 0.78)', + ); + assert.equal(secondaryStyleValues?.get('--secondary-sub-backdrop-filter'), 'blur(6px)'); + assert.equal( + (secondarySubContainer.style as unknown as { backgroundColor?: string }).backgroundColor, + undefined, + ); + assert.equal( + (secondarySubContainer.style as unknown as { backdropFilter?: string }).backdropFilter, + undefined, + ); + assert.equal((secondarySubRoot.style as unknown as { fontWeight?: string }).fontWeight, '600'); + } finally { + restoreDocument(); + } +}); + test('computeWordClass adds frequency class for single mode when rank is within topX', () => { const token = createToken({ surface: '猫', @@ -819,6 +871,42 @@ test('JLPT CSS rules use underline-only styling in renderer stylesheet', () => { /-webkit-text-fill-color:\s*var\(--subtitle-hover-token-color,\s*#f4dbd6\)\s*!important;/, ); + const secondaryContainerBlock = extractClassBlock(cssText, '#secondarySubContainer'); + assert.match( + secondaryContainerBlock, + /background:\s*var\(--secondary-sub-background-color,\s*transparent\);/, + ); + assert.match( + secondaryContainerBlock, + /backdrop-filter:\s*var\(--secondary-sub-backdrop-filter,\s*none\);/, + ); + + const secondaryRootBlock = extractClassBlock(cssText, '#secondarySubRoot'); + assert.match(secondaryRootBlock, /-webkit-text-stroke:\s*0\.45px rgba\(0,\s*0,\s*0,\s*0\.7\);/); + assert.match( + secondaryRootBlock, + /text-shadow:\s*0 2px 4px rgba\(0,\s*0,\s*0,\s*0\.95\),\s*0 0 8px rgba\(0,\s*0,\s*0,\s*0\.8\),\s*0 0 16px rgba\(0,\s*0,\s*0,\s*0\.55\);/, + ); + + const secondaryHoverBaseBlock = extractClassBlock( + cssText, + '#secondarySubContainer.secondary-sub-hover #secondarySubRoot', + ); + assert.match(secondaryHoverBaseBlock, /background:\s*transparent;/); + + const secondaryHoverVisibleBlock = extractClassBlock( + cssText, + '#secondarySubContainer.secondary-sub-hover:hover #secondarySubRoot', + ); + assert.match( + secondaryHoverVisibleBlock, + /background:\s*var\(--secondary-sub-background-color,\s*transparent\);/, + ); + assert.match( + secondaryHoverVisibleBlock, + /backdrop-filter:\s*var\(--secondary-sub-backdrop-filter,\s*none\);/, + ); + assert.doesNotMatch( cssText, /body\.layer-visible\s+#secondarySubContainer\s*\{[^}]*display:\s*none/i, diff --git a/src/renderer/subtitle-render.ts b/src/renderer/subtitle-render.ts index 4c93970..1fb3276 100644 --- a/src/renderer/subtitle-render.ts +++ b/src/renderer/subtitle-render.ts @@ -155,6 +155,33 @@ const CONTAINER_STYLE_KEYS = new Set([ '-webkit-backdrop-filter', ]); +function resolveSecondaryBackgroundColor(declarations: Record): string { + for (const key of ['backgroundColor', 'background']) { + const value = declarations[key]; + if (typeof value === 'string' && value.trim().length > 0) { + return value.trim(); + } + } + + return 'transparent'; +} + +function resolveSecondaryBackdropFilter(declarations: Record): string { + for (const key of [ + 'backdropFilter', + 'WebkitBackdropFilter', + 'webkitBackdropFilter', + '-webkit-backdrop-filter', + ]) { + const value = declarations[key]; + if (typeof value === 'string' && value.trim().length > 0) { + return value.trim(); + } + } + + return 'none'; +} + function getFrequencyDictionaryClass( token: MergedToken, settings: FrequencyRenderSettings, @@ -700,9 +727,17 @@ export function createSubtitleRenderer(ctx: RendererContext) { secondaryStyleDeclarations, CONTAINER_STYLE_KEYS, ); - applyInlineStyleDeclarations( - ctx.dom.secondarySubContainer, - pickInlineStyleDeclarations(secondaryStyleDeclarations, CONTAINER_STYLE_KEYS), + const secondaryContainerStyleDeclarations = pickInlineStyleDeclarations( + secondaryStyleDeclarations, + CONTAINER_STYLE_KEYS, + ); + ctx.dom.secondarySubContainer.style.setProperty( + '--secondary-sub-background-color', + resolveSecondaryBackgroundColor(secondaryContainerStyleDeclarations), + ); + ctx.dom.secondarySubContainer.style.setProperty( + '--secondary-sub-backdrop-filter', + resolveSecondaryBackdropFilter(secondaryContainerStyleDeclarations), ); if (secondaryStyle.fontFamily) {