style: update subtitle text shadow, JLPT underlines, and frequency topX default (#96)

* style: update subtitle text shadow, JLPT underlines, and frequency topX

- Replace directional text-shadow with 4-corner outline shadow for sharper readability
- Increase JLPT level border-bottom from 2px to 3px; add drop-shadow filter
- Add margin-left 0.18em to character image token
- Raise frequencyDictionary topX default from 1000 to 10000

* docs: add subtitle style changelog fragment

* docs: update generated config examples

* style: wrap long textShadow strings and add blank line in CSS
This commit is contained in:
2026-05-28 00:18:39 -07:00
committed by GitHub
parent 8d0535f3ca
commit d33009d4a3
9 changed files with 45 additions and 25 deletions
+4
View File
@@ -0,0 +1,4 @@
type: changed
area: subtitles
- Updated subtitle defaults with a stronger outline-style text shadow, thicker JLPT underlines, and a `topX` frequency highlighting default of `10000`.
+3 -3
View File
@@ -380,7 +380,7 @@
"word-spacing": "0", // Word spacing setting.
"font-kerning": "normal", // Font kerning setting.
"text-rendering": "geometricPrecision", // Text rendering setting.
"text-shadow": "0 2px 6px rgba(0,0,0,0.9), 0 0 12px rgba(0,0,0,0.55)", // Text shadow setting.
"text-shadow": "-1px -1px 2px rgba(0,0,0,0.95), 1px -1px 2px rgba(0,0,0,0.95), -1px 1px 2px rgba(0,0,0,0.95), 1px 1px 2px rgba(0,0,0,0.95), 0 0 8px rgba(0,0,0,0.5)", // Text shadow setting.
"backdrop-filter": "blur(6px)", // Backdrop filter setting.
"--subtitle-hover-token-color": "#f4dbd6", // Subtitle hover token color setting.
"--subtitle-hover-token-background-color": "transparent" // Subtitle hover token background color setting.
@@ -405,7 +405,7 @@
"frequencyDictionary": {
"enabled": false, // Enable frequency-dictionary-based highlighting based on token rank. Values: true | false
"sourcePath": "", // Optional absolute path to a frequency dictionary directory. If empty, built-in discovery search paths are used.
"topX": 1000, // Only color tokens with frequency rank <= topX (default: 1000).
"topX": 10000, // Only color tokens with frequency rank <= topX (default: 10000).
"mode": "single", // single: use one color for all matching tokens. banded: use color ramp by frequency band. Values: single | banded
"matchMode": "headword", // headword: frequency lookup uses dictionary form. surface: lookup uses subtitle-visible token text. Values: headword | surface
"singleColor": "#f5a97f", // Color used when frequencyDictionary.mode is `single`.
@@ -430,7 +430,7 @@
"word-spacing": "0", // Word spacing setting.
"font-kerning": "normal", // Font kerning setting.
"text-rendering": "geometricPrecision", // Text rendering setting.
"text-shadow": "0 2px 6px rgba(0,0,0,0.9), 0 0 12px rgba(0,0,0,0.55)", // Text shadow setting.
"text-shadow": "-1px -1px 2px rgba(0,0,0,0.95), 1px -1px 2px rgba(0,0,0,0.95), -1px 1px 2px rgba(0,0,0,0.95), 1px 1px 2px rgba(0,0,0,0.95), 0 0 8px rgba(0,0,0,0.5)", // Text shadow setting.
"backdrop-filter": "blur(6px)" // Backdrop filter setting.
} // CSS declaration object applied to secondary subtitles after normal subtitle style defaults.
} // Secondary setting.
+3 -3
View File
@@ -380,7 +380,7 @@
"word-spacing": "0", // Word spacing setting.
"font-kerning": "normal", // Font kerning setting.
"text-rendering": "geometricPrecision", // Text rendering setting.
"text-shadow": "0 2px 6px rgba(0,0,0,0.9), 0 0 12px rgba(0,0,0,0.55)", // Text shadow setting.
"text-shadow": "-1px -1px 2px rgba(0,0,0,0.95), 1px -1px 2px rgba(0,0,0,0.95), -1px 1px 2px rgba(0,0,0,0.95), 1px 1px 2px rgba(0,0,0,0.95), 0 0 8px rgba(0,0,0,0.5)", // Text shadow setting.
"backdrop-filter": "blur(6px)", // Backdrop filter setting.
"--subtitle-hover-token-color": "#f4dbd6", // Subtitle hover token color setting.
"--subtitle-hover-token-background-color": "transparent" // Subtitle hover token background color setting.
@@ -405,7 +405,7 @@
"frequencyDictionary": {
"enabled": false, // Enable frequency-dictionary-based highlighting based on token rank. Values: true | false
"sourcePath": "", // Optional absolute path to a frequency dictionary directory. If empty, built-in discovery search paths are used.
"topX": 1000, // Only color tokens with frequency rank <= topX (default: 1000).
"topX": 10000, // Only color tokens with frequency rank <= topX (default: 10000).
"mode": "single", // single: use one color for all matching tokens. banded: use color ramp by frequency band. Values: single | banded
"matchMode": "headword", // headword: frequency lookup uses dictionary form. surface: lookup uses subtitle-visible token text. Values: headword | surface
"singleColor": "#f5a97f", // Color used when frequencyDictionary.mode is `single`.
@@ -430,7 +430,7 @@
"word-spacing": "0", // Word spacing setting.
"font-kerning": "normal", // Font kerning setting.
"text-rendering": "geometricPrecision", // Text rendering setting.
"text-shadow": "0 2px 6px rgba(0,0,0,0.9), 0 0 12px rgba(0,0,0,0.55)", // Text shadow setting.
"text-shadow": "-1px -1px 2px rgba(0,0,0,0.95), 1px -1px 2px rgba(0,0,0,0.95), -1px 1px 2px rgba(0,0,0,0.95), 1px 1px 2px rgba(0,0,0,0.95), 0 0 8px rgba(0,0,0,0.5)", // Text shadow setting.
"backdrop-filter": "blur(6px)" // Backdrop filter setting.
} // CSS declaration object applied to secondary subtitles after normal subtitle style defaults.
} // Secondary setting.
+2 -1
View File
@@ -22,7 +22,8 @@ import {
const DEFAULT_SUBTITLE_FONT_FAMILY =
'Hiragino Sans, M PLUS 1, Source Han Sans JP, Noto Sans CJK JP';
const DEFAULT_SECONDARY_SUBTITLE_FONT_FAMILY = DEFAULT_SUBTITLE_FONT_FAMILY;
const DEFAULT_SUBTITLE_TEXT_SHADOW = '0 2px 6px rgba(0,0,0,0.9), 0 0 12px rgba(0,0,0,0.55)';
const DEFAULT_SUBTITLE_TEXT_SHADOW =
'-1px -1px 2px rgba(0,0,0,0.95), 1px -1px 2px rgba(0,0,0,0.95), -1px 1px 2px rgba(0,0,0,0.95), 1px 1px 2px rgba(0,0,0,0.95), 0 0 8px rgba(0,0,0,0.5)';
const SUBTITLE_CSS_SCOPES: SubtitleCssScope[] = ['primary', 'secondary', 'sidebar'];
function makeTempDir(): string {
+5 -3
View File
@@ -23,7 +23,8 @@ export const SUBTITLE_DEFAULT_CONFIG: Pick<ResolvedConfig, 'subtitleStyle' | 'su
wordSpacing: 0,
fontKerning: 'normal',
textRendering: 'geometricPrecision',
textShadow: '0 2px 6px rgba(0,0,0,0.9), 0 0 12px rgba(0,0,0,0.55)',
textShadow:
'-1px -1px 2px rgba(0,0,0,0.95), 1px -1px 2px rgba(0,0,0,0.95), -1px 1px 2px rgba(0,0,0,0.95), 1px 1px 2px rgba(0,0,0,0.95), 0 0 8px rgba(0,0,0,0.5)',
paintOrder: '',
WebkitTextStroke: '',
fontStyle: 'normal',
@@ -41,7 +42,7 @@ export const SUBTITLE_DEFAULT_CONFIG: Pick<ResolvedConfig, 'subtitleStyle' | 'su
frequencyDictionary: {
enabled: false,
sourcePath: '',
topX: 1000,
topX: 10000,
mode: 'single',
matchMode: 'headword',
singleColor: '#f5a97f',
@@ -57,7 +58,8 @@ export const SUBTITLE_DEFAULT_CONFIG: Pick<ResolvedConfig, 'subtitleStyle' | 'su
wordSpacing: 0,
fontKerning: 'normal',
textRendering: 'geometricPrecision',
textShadow: '0 2px 6px rgba(0,0,0,0.9), 0 0 12px rgba(0,0,0,0.55)',
textShadow:
'-1px -1px 2px rgba(0,0,0,0.95), 1px -1px 2px rgba(0,0,0,0.95), -1px 1px 2px rgba(0,0,0,0.95), 1px 1px 2px rgba(0,0,0,0.95), 0 0 8px rgba(0,0,0,0.5)',
paintOrder: '',
WebkitTextStroke: '',
backgroundColor: 'transparent',
+1 -1
View File
@@ -127,7 +127,7 @@ export function buildSubtitleConfigOptionRegistry(
path: 'subtitleStyle.frequencyDictionary.topX',
kind: 'number',
defaultValue: defaultConfig.subtitleStyle.frequencyDictionary.topX,
description: 'Only color tokens with frequency rank <= topX (default: 1000).',
description: 'Only color tokens with frequency rank <= topX (default: 10000).',
},
{
path: 'subtitleStyle.frequencyDictionary.mode',
+22 -10
View File
@@ -667,9 +667,13 @@ body.subtitle-sidebar-embedded-open #subtitleContainer {
--subtitle-frequency-band-3-color: #f9e2af;
--subtitle-frequency-band-4-color: #8bd5ca;
--subtitle-frequency-band-5-color: #8aadf4;
text-shadow:
2px 2px 4px rgba(0, 0, 0, 0.8),
-1px -1px 2px rgba(0, 0, 0, 0.5);
-1px -1px 2px rgba(0, 0, 0, 0.95),
1px -1px 2px rgba(0, 0, 0, 0.95),
-1px 1px 2px rgba(0, 0, 0, 0.95),
1px 1px 2px rgba(0, 0, 0, 0.95),
0 0 8px rgba(0, 0, 0, 0.5);
/* Enable text selection for Yomitan */
user-select: text;
cursor: text;
@@ -817,6 +821,7 @@ body.settings-modal-open [data-subminer-yomitan-popup-host='true'] {
display: inline-block;
position: relative;
padding-left: 1.08em;
margin-left: 0.18em;
vertical-align: baseline;
}
@@ -837,7 +842,8 @@ body.settings-modal-open [data-subminer-yomitan-popup-host='true'] {
#subtitleRoot .word.word-jlpt-n1 {
text-decoration-line: none;
border-bottom: 2px solid var(--subtitle-jlpt-n1-color, #ed8796);
border-bottom: 3px solid var(--subtitle-jlpt-n1-color, #ed8796);
filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.5));
}
#subtitleRoot .word.word-jlpt-n1[data-jlpt-level]::after {
@@ -846,7 +852,8 @@ body.settings-modal-open [data-subminer-yomitan-popup-host='true'] {
#subtitleRoot .word.word-jlpt-n2 {
text-decoration-line: none;
border-bottom: 2px solid var(--subtitle-jlpt-n2-color, #f5a97f);
border-bottom: 3px solid var(--subtitle-jlpt-n2-color, #f5a97f);
filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.5));
}
#subtitleRoot .word.word-jlpt-n2[data-jlpt-level]::after {
@@ -855,7 +862,8 @@ body.settings-modal-open [data-subminer-yomitan-popup-host='true'] {
#subtitleRoot .word.word-jlpt-n3 {
text-decoration-line: none;
border-bottom: 2px solid var(--subtitle-jlpt-n3-color, #f9e2af);
border-bottom: 3px solid var(--subtitle-jlpt-n3-color, #f9e2af);
filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.5));
}
#subtitleRoot .word.word-jlpt-n3[data-jlpt-level]::after {
@@ -864,7 +872,8 @@ body.settings-modal-open [data-subminer-yomitan-popup-host='true'] {
#subtitleRoot .word.word-jlpt-n4 {
text-decoration-line: none;
border-bottom: 2px solid var(--subtitle-jlpt-n4-color, #a6e3a1);
border-bottom: 3px solid var(--subtitle-jlpt-n4-color, #a6e3a1);
filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.5));
}
#subtitleRoot .word.word-jlpt-n4[data-jlpt-level]::after {
@@ -873,7 +882,8 @@ body.settings-modal-open [data-subminer-yomitan-popup-host='true'] {
#subtitleRoot .word.word-jlpt-n5 {
text-decoration-line: none;
border-bottom: 2px solid var(--subtitle-jlpt-n5-color, #8aadf4);
border-bottom: 3px solid var(--subtitle-jlpt-n5-color, #8aadf4);
filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.5));
}
#subtitleRoot .word.word-jlpt-n5[data-jlpt-level]::after {
@@ -1186,9 +1196,11 @@ body.layer-modal #overlay {
-webkit-text-stroke: 0.45px rgba(0, 0, 0, 0.7);
paint-order: stroke fill;
text-shadow:
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);
-1px -1px 2px rgba(0, 0, 0, 0.95),
1px -1px 2px rgba(0, 0, 0, 0.95),
-1px 1px 2px rgba(0, 0, 0, 0.95),
1px 1px 2px rgba(0, 0, 0, 0.95),
0 0 8px rgba(0, 0, 0, 0.5);
user-select: text;
cursor: text;
}
+4 -3
View File
@@ -909,8 +909,8 @@ test('subtitle annotation CSS underlines JLPT tokens without changing token colo
assert.doesNotMatch(plainJlptBlock, /text-decoration\s*:[^;]*\bunderline\b/i);
assert.match(
plainJlptBlock,
new RegExp(`border-bottom:\\s*2px\\s+solid\\s+var\\(--subtitle-jlpt-n${level}-color,`),
`JLPT level must paint a permanent 2px border-bottom in the level color`,
new RegExp(`border-bottom:\\s*3px\\s+solid\\s+var\\(--subtitle-jlpt-n${level}-color,`),
`JLPT level must paint a permanent 3px border-bottom in the level color`,
);
// JLPT tagging must communicate level *only* via the underline; it must
@@ -973,6 +973,7 @@ test('subtitle annotation CSS underlines JLPT tokens without changing token colo
assert.match(characterImageTokenBlock, /display:\s*inline-block;/);
assert.match(characterImageTokenBlock, /position:\s*relative;/);
assert.match(characterImageTokenBlock, /padding-left:\s*1\.08em;/);
assert.match(characterImageTokenBlock, /margin-left:\s*0\.18em;/);
const characterImageBlock = extractClassBlock(cssText, '#subtitleRoot .word-character-image');
assert.match(characterImageBlock, /position:\s*absolute;/);
@@ -1186,7 +1187,7 @@ test('subtitle annotation CSS underlines JLPT tokens without changing token colo
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\);/,
/text-shadow:\s*-1px -1px 2px rgba\(0,\s*0,\s*0,\s*0\.95\),\s*1px -1px 2px rgba\(0,\s*0,\s*0,\s*0\.95\),\s*-1px 1px 2px rgba\(0,\s*0,\s*0,\s*0\.95\),\s*1px 1px 2px rgba\(0,\s*0,\s*0,\s*0\.95\),\s*0 0 8px rgba\(0,\s*0,\s*0,\s*0\.5\);/,
);
const secondaryHoverBaseBlock = extractClassBlock(
+1 -1
View File
@@ -89,7 +89,7 @@ function sanitizeSubtitleHoverTokenBackgroundColor(value: unknown): string {
const DEFAULT_FREQUENCY_RENDER_SETTINGS: FrequencyRenderSettings = {
enabled: false,
topX: 1000,
topX: 10000,
mode: 'single',
singleColor: '#f5a97f',
bandedColors: ['#ed8796', '#f5a97f', '#f9e2af', '#8bd5ca', '#8aadf4'],