fix: improve secondary subtitle readability

This commit is contained in:
2026-03-07 23:51:23 -08:00
parent 55dff6ced7
commit f775f90360
6 changed files with 153 additions and 10 deletions

View File

@@ -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.

View File

@@ -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);

View File

@@ -50,10 +50,10 @@ export const SUBTITLE_DEFAULT_CONFIG: Pick<ResolvedConfig, 'subtitleStyle'> = {
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',
},
},

View File

@@ -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;

View File

@@ -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<string, string>;
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,

View File

@@ -155,6 +155,33 @@ const CONTAINER_STYLE_KEYS = new Set<string>([
'-webkit-backdrop-filter',
]);
function resolveSecondaryBackgroundColor(declarations: Record<string, unknown>): 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, unknown>): 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) {