feat(settings): add candidate appearance controls and sync preview behavior

Add candidate font, size, color, and page size controls with system font dropdowns. Keep the settings preview aligned with theme, helpcode visibility, and browser/WebView runtime updates.
This commit is contained in:
fanlusky committed 2026-07-28 15:20:28 +08:00
1 parent cf5eeb2985
commit bf2adeefbe
16 files changed
+604 -96

No files matched your search

@@ -6,9 +6,16 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>(IME)VCandWnd</title>
<style>
:root {
--cand-font-family: 'Segoe UI', 'Noto Sans SC', 'Microsoft YaHei', sans-serif;
--cand-font-size: 16px;
--cand-text: #e9e8e8;
--cand-num: #e9e8e89d;
}
body {
font-family: 'Noto Sans SC', 'Microsoft YaHei', sans-serif;
font-size: 16px;
font-family: var(--cand-font-family);
font-size: var(--cand-font-size);
height: 100vh;
margin: 0;
overflow: hidden;
@@ -103,7 +110,7 @@
.text {
padding-left: 5px;
padding-right: 5px;
color: #e9e8e8;
color: var(--cand-text);
display: block;
min-width: 0;
max-width: 100%;
@@ -117,7 +124,7 @@
.num {
font-size: 0.8em;
color: #e9e8e89d;
color: var(--cand-num);
margin-right: 1px;
}
@@ -6,9 +6,16 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>(IME)VCandWnd</title>
<style>
:root {
--cand-font-family: 'Segoe UI', 'Noto Sans SC', 'Microsoft YaHei', sans-serif;
--cand-font-size: 16px;
--cand-text: #1a1a1a;
--cand-num: rgba(26, 26, 26, 0.55);
}
body {
font-family: 'Noto Sans SC', 'Microsoft YaHei', sans-serif;
font-size: 16px;
font-family: var(--cand-font-family);
font-size: var(--cand-font-size);
height: 100vh;
margin: 0;
overflow: hidden;
@@ -103,7 +110,7 @@
.text {
padding-left: 5px;
padding-right: 5px;
color: #1a1a1a;
color: var(--cand-text);
display: block;
min-width: 0;
max-width: 100%;
@@ -117,7 +124,7 @@
.num {
font-size: 0.8em;
color: rgba(26, 26, 26, 0.55);
color: var(--cand-num);
margin-right: 1px;
}
@@ -6,9 +6,16 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>(IME)VCandWnd</title>
<style>
:root {
--cand-font-family: 'Segoe UI', 'Noto Sans SC', 'Microsoft YaHei', sans-serif;
--cand-font-size: 16px;
--cand-text: #e9e8e8;
--cand-num: #e9e8e89d;
}
body {
font-family: 'Noto Sans SC', 'Microsoft YaHei', sans-serif;
font-size: 16px;
font-family: var(--cand-font-family);
font-size: var(--cand-font-size);
height: 100vh;
margin: 0;
overflow: hidden;
@@ -88,7 +95,7 @@
.text {
padding-left: 8px;
color: #e9e8e8;
color: var(--cand-text);
display: block;
min-width: 0;
max-width: 100%;
@@ -102,7 +109,7 @@
.cand-no {
font-size: 0.8em;
color: #e9e8e89d;
color: var(--cand-num);
margin-right: 3px;
}
@@ -6,9 +6,16 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>(IME)VCandWnd</title>
<style>
:root {
--cand-font-family: 'Segoe UI', 'Noto Sans SC', 'Microsoft YaHei', sans-serif;
--cand-font-size: 16px;
--cand-text: #1a1a1a;
--cand-num: rgba(26, 26, 26, 0.55);
}
body {
font-family: 'Noto Sans SC', 'Microsoft YaHei', sans-serif;
font-size: 16px;
font-family: var(--cand-font-family);
font-size: var(--cand-font-size);
height: 100vh;
margin: 0;
overflow: hidden;
@@ -88,7 +95,7 @@
.text {
padding-left: 8px;
color: #1a1a1a;
color: var(--cand-text);
display: block;
min-width: 0;
max-width: 100%;
@@ -102,7 +109,7 @@
.cand-no {
font-size: 0.8em;
color: rgba(26, 26, 26, 0.55);
color: var(--cand-num);
margin-right: 3px;
}
@@ -1,17 +1,214 @@
import { applyDropdownValue, setupDropdownMenu } from './shared';
import { applyDropdownValue, populateDropdownMenu, setupDropdownMenu } from './shared';
import { loadHTML } from '../utils/common-utils';
import { applyThemeConfig, type ThemeConfig } from './theme';
import { applyThemeConfig, type ResolvedTheme, type ThemeConfig } from './theme';
function postConfigUpdate(path: string, value: string | number): void {
window.chrome?.webview?.postMessage(
JSON.stringify({
type: 'configUpdate',
data: { path, value }
})
);
}
export type CandidateAppearanceConfig = {
font?: string;
english_font?: string;
default_font?: string;
font_size?: number;
cand_text_color?: string;
page_size?: number;
system_fonts?: string[];
};
export type CandidatePreviewHelpcodeConfig = {
input_schema?: string;
shuangpin_helpcode?: boolean;
quanpin_helpcode?: boolean;
show_sp_helpcode_in_candidate_window?: boolean;
show_qp_helpcode_in_candidate_window?: boolean;
};
let previewFont = 'Noto Sans SC';
let previewEnglishFont = 'Segoe UI';
let previewDefaultFont = 'Microsoft YaHei';
let previewFontSize = 16;
let previewTextColor = 'auto';
let previewPageSize = 8;
let previewCandTheme: ResolvedTheme = 'dark';
let previewInputSchema = 'shuangpin';
let previewShuangpinHelpcode = true;
let previewQuanpinHelpcode = true;
let previewShowSpHelpcode = true;
let previewShowQpHelpcode = true;
const FALLBACK_CJK_FONTS = [
'Noto Sans SC',
'Microsoft YaHei',
'Microsoft YaHei UI',
'SimHei',
'SimSun',
'DengXian',
'KaiTi',
'FangSong'
];
const FALLBACK_ENGLISH_FONTS = [
'Segoe UI',
'Arial',
'Calibri',
'Consolas',
'Cascadia Mono',
'Times New Roman',
'Courier New',
'Tahoma'
];
function quoteFont(name: string): string {
return /\s/.test(name) ? `"${name.replace(/"/g, '\\"')}"` : name;
}
function appearancePreviewRoots(): HTMLElement[] {
return Array.from(document.querySelectorAll<HTMLElement>('.cand-preview .candidate'));
}
function themeFallbackColor(): string {
return previewCandTheme === 'light' ? '#1a1a1a' : '#e9e8e8';
}
function shouldShowHelpcodeInPreview(): boolean {
if (previewInputSchema === 'quanpin') {
return previewQuanpinHelpcode && previewShowQpHelpcode;
}
if (previewInputSchema === 'shuangpin') {
return previewShuangpinHelpcode && previewShowSpHelpcode;
}
return false;
}
export function applyCandidatePreviewHelpcode(): void {
const show = shouldShowHelpcodeInPreview();
document.querySelectorAll<HTMLElement>('.cand-preview .cand-helpcode').forEach((el) => {
el.hidden = !show;
});
}
export function updateCandidatePreviewHelpcode(config: CandidatePreviewHelpcodeConfig): void {
if (typeof config.input_schema === 'string') {
previewInputSchema = config.input_schema;
}
if (typeof config.shuangpin_helpcode === 'boolean') {
previewShuangpinHelpcode = config.shuangpin_helpcode;
}
if (typeof config.quanpin_helpcode === 'boolean') {
previewQuanpinHelpcode = config.quanpin_helpcode;
}
if (typeof config.show_sp_helpcode_in_candidate_window === 'boolean') {
previewShowSpHelpcode = config.show_sp_helpcode_in_candidate_window;
}
if (typeof config.show_qp_helpcode_in_candidate_window === 'boolean') {
previewShowQpHelpcode = config.show_qp_helpcode_in_candidate_window;
}
applyCandidatePreviewHelpcode();
}
function applyCandidatePreviewStyle(): void {
// English font first so Latin glyphs prefer it; CJK falls through to Chinese font.
const family = [previewEnglishFont, previewFont, previewDefaultFont, 'sans-serif']
.filter(Boolean)
.map(quoteFont)
.join(', ');
appearancePreviewRoots().forEach((el) => {
el.style.setProperty('--cand-font-family', family);
el.style.setProperty('--cand-font-size', `${previewFontSize}px`);
el.style.fontFamily = family;
el.style.fontSize = `${previewFontSize}px`;
if (previewTextColor && previewTextColor !== 'auto') {
el.style.setProperty('--cand-text', previewTextColor);
el.style.setProperty('--cand-num', previewTextColor.length === 7 ? `${previewTextColor}9d` : previewTextColor);
} else {
el.style.removeProperty('--cand-text');
el.style.removeProperty('--cand-num');
}
el.querySelectorAll<HTMLElement>('.container').forEach((container) => {
container.style.fontFamily = family;
container.style.fontSize = `${previewFontSize}px`;
});
el.querySelectorAll<HTMLElement>('.row-wrapper').forEach((wrapper, index) => {
wrapper.style.display = index < previewPageSize ? '' : 'none';
});
});
}
function syncColorControls(color: string | undefined): void {
const input = document.getElementById('candTextColorInput') as HTMLInputElement | null;
const resetBtn = document.getElementById('candTextColorResetBtn');
const normalized = !color || color === 'auto' ? 'auto' : color;
previewTextColor = normalized;
if (input) {
input.value = normalized === 'auto' ? themeFallbackColor() : normalized;
}
resetBtn?.classList.toggle('is-active', normalized === 'auto');
}
/** Called when candidate-surface theme (or global follow) resolves to a new dark/light. */
export function onCandidateSurfaceThemeChanged(theme: ResolvedTheme): void {
previewCandTheme = theme;
if (previewTextColor === 'auto') {
syncColorControls('auto');
applyCandidatePreviewStyle();
}
}
function populateFontMenus(systemFonts: string[] | undefined): void {
const fonts =
systemFonts && systemFonts.length > 0
? systemFonts
: [...FALLBACK_CJK_FONTS, ...FALLBACK_ENGLISH_FONTS];
populateDropdownMenu('candFontMenu', fonts, { selected: previewFont, previewFont: true });
populateDropdownMenu('candEnglishFontMenu', fonts, {
selected: previewEnglishFont,
previewFont: true
});
applyDropdownValue('candFontBtn', 'candFontMenu', previewFont);
applyDropdownValue('candEnglishFontBtn', 'candEnglishFontMenu', previewEnglishFont);
}
export function applyAppearanceConfig(
candidateWindowPreeditStyle: string | undefined,
tsfPreeditStyle: string | undefined,
themeConfig?: ThemeConfig
themeConfig?: ThemeConfig,
candidateAppearance?: CandidateAppearanceConfig
): void {
applyDropdownValue('candPreeditStyleBtn', 'candPreeditStyleMenu', candidateWindowPreeditStyle);
applyDropdownValue('tsfPreeditStyleBtn', 'tsfPreeditStyleMenu', tsfPreeditStyle);
if (themeConfig) {
applyThemeConfig(themeConfig);
}
if (candidateAppearance?.font) {
previewFont = candidateAppearance.font;
}
if (candidateAppearance?.english_font) {
previewEnglishFont = candidateAppearance.english_font;
}
if (candidateAppearance?.default_font) {
previewDefaultFont = candidateAppearance.default_font;
}
if (typeof candidateAppearance?.font_size === 'number') {
previewFontSize = candidateAppearance.font_size;
applyDropdownValue('candFontSizeBtn', 'candFontSizeMenu', String(candidateAppearance.font_size));
}
if (typeof candidateAppearance?.page_size === 'number') {
previewPageSize = candidateAppearance.page_size;
applyDropdownValue('candPageSizeBtn', 'candPageSizeMenu', String(candidateAppearance.page_size));
}
populateFontMenus(candidateAppearance?.system_fonts);
syncColorControls(candidateAppearance?.cand_text_color);
applyCandidatePreviewStyle();
applyCandidatePreviewHelpcode();
}
export async function setupAppearance() {
@@ -21,6 +218,9 @@ export async function setupAppearance() {
const wnd_h = document.getElementById('candidate-wnd-h')!;
wnd_h.innerHTML = await loadHTML(`/src/partials/candidate/candidate-wnd-h.html`);
wnd_h.style.display = 'none';
populateFontMenus(undefined);
applyCandidatePreviewStyle();
applyCandidatePreviewHelpcode();
// 主题模式(全局)
setupDropdownMenu('themeBtn', 'themeMenu', 'changeTheme', false, 'appearance.theme_mode');
@@ -40,6 +240,43 @@ export async function setupAppearance() {
// 候选项排列方式
setupDropdownMenu('arrangeBtn', 'arrangeMenu', 'changeCandidateArrange');
// 候选窗样式
setupDropdownMenu('candFontBtn', 'candFontMenu', '', true, 'appearance.font', (value) => {
previewFont = value;
applyCandidatePreviewStyle();
return value;
});
setupDropdownMenu('candEnglishFontBtn', 'candEnglishFontMenu', '', true, 'appearance.english_font', (value) => {
previewEnglishFont = value;
applyCandidatePreviewStyle();
return value;
});
setupDropdownMenu('candFontSizeBtn', 'candFontSizeMenu', '', true, 'appearance.font_size', (value) => {
previewFontSize = Number(value) || 16;
applyCandidatePreviewStyle();
return previewFontSize;
});
setupDropdownMenu('candPageSizeBtn', 'candPageSizeMenu', '', true, 'appearance.page_size', (value) => {
previewPageSize = Number(value) || 8;
applyCandidatePreviewStyle();
return previewPageSize;
});
const colorInput = document.getElementById('candTextColorInput') as HTMLInputElement | null;
colorInput?.addEventListener('input', () => {
const value = colorInput.value;
previewTextColor = value;
syncColorControls(value);
applyCandidatePreviewStyle();
postConfigUpdate('appearance.cand_text_color', value);
});
document.getElementById('candTextColorResetBtn')?.addEventListener('click', () => {
previewTextColor = 'auto';
syncColorControls('auto');
applyCandidatePreviewStyle();
postConfigUpdate('appearance.cand_text_color', 'auto');
});
// 候选窗预编辑
setupDropdownMenu(
'candPreeditStyleBtn',
@@ -1,5 +1,5 @@
import { applyCandidateArrange, applyDropdownValue, applyToggleState } from './shared';
import { applyAppearanceConfig } from './appearance';
import { applyAppearanceConfig, updateCandidatePreviewHelpcode } from './appearance';
import { applyFrequencyConfig, applyInputConfig } from './input';
import { applyVoiceConfig } from './voice';
import { applyAiConfig } from './ai-settings';
@@ -33,6 +33,15 @@ export function setupConfigSync(): void {
theme_cand: payload.data?.appearance?.theme_cand,
theme_ftb: payload.data?.appearance?.theme_ftb,
theme_menu: payload.data?.appearance?.theme_menu
},
{
font: payload.data?.appearance?.font,
english_font: payload.data?.appearance?.english_font,
default_font: payload.data?.appearance?.default_font,
font_size: payload.data?.appearance?.font_size,
cand_text_color: payload.data?.appearance?.cand_text_color,
page_size: payload.data?.appearance?.page_size,
system_fonts: payload.data?.appearance?.system_fonts
}
);
applyInputConfig(
@@ -41,6 +50,13 @@ export function setupConfigSync(): void {
payload.data?.input?.shuangpin_schema,
payload.data?.input?.wubi_schema
);
updateCandidatePreviewHelpcode({
input_schema: payload.data?.input?.schema,
shuangpin_helpcode: payload.data?.helpcode?.shuangpin_helpcode,
quanpin_helpcode: payload.data?.helpcode?.quanpin_helpcode,
show_sp_helpcode_in_candidate_window: payload.data?.helpcode?.show_sp_helpcode_in_candidate_window,
show_qp_helpcode_in_candidate_window: payload.data?.helpcode?.show_qp_helpcode_in_candidate_window
});
applyFrequencyConfig(payload.data?.frequency_adjustment);
if (payload.data?.voice_input && typeof payload.data.voice_input === 'object') {
applyVoiceConfig(payload.data.voice_input);
@@ -1,5 +1,6 @@
import { setupDropdownMenu, setupToggleButton } from './shared';
import { updateConfig } from './config-sync';
import { updateCandidatePreviewHelpcode } from './appearance';
export function setupHelpcode(): void {
// 双拼辅助码方案
@@ -13,20 +14,24 @@ export function setupHelpcode(): void {
// 双拼辅助码开关
setupToggleButton('shuangpinHelpcodeToggleBtn', (active) => {
updateConfig('helpcode.shuangpin_helpcode', active);
updateCandidatePreviewHelpcode({ shuangpin_helpcode: active });
});
// 全拼辅助码开关
setupToggleButton('quanpinHelpcodeToggleBtn', (active) => {
updateConfig('helpcode.quanpin_helpcode', active);
updateCandidatePreviewHelpcode({ quanpin_helpcode: active });
});
// 是否在候选窗口中显示双拼辅助码
setupToggleButton('showShuangpinHelpcodeToggleBtn', (active) => {
updateConfig('helpcode.show_sp_helpcode_in_candidate_window', active);
updateCandidatePreviewHelpcode({ show_sp_helpcode_in_candidate_window: active });
});
// 是否在候选窗口中显示全拼辅助码
setupToggleButton('showQuanpinHelpcodeToggleBtn', (active) => {
updateConfig('helpcode.show_qp_helpcode_in_candidate_window', active);
updateCandidatePreviewHelpcode({ show_qp_helpcode_in_candidate_window: active });
});
}
@@ -1,5 +1,6 @@
import { applyDropdownValue, setupDropdownMenu, setupToggleButton } from './shared';
import { updateConfig } from './config-sync';
import { updateCandidatePreviewHelpcode } from './appearance';
type InputScheme = 'quanpin' | 'shuangpin' | 'wubi';
@@ -21,6 +22,7 @@ export function applyInputConfig(
if (radio) {
radio.checked = true;
}
updateCandidatePreviewHelpcode({ input_schema: schema });
}
applyDropdownValue('characterSetBtn', 'characterSetMenu', characterSet);
@@ -43,6 +45,7 @@ export function setupInput(): void {
}
const schema = radio.value as InputScheme;
updateInputConfig('input.schema', schema);
updateCandidatePreviewHelpcode({ input_schema: schema });
});
});
@@ -36,64 +36,68 @@ export function setupDropdownMenu(
menu.classList.toggle('open', willOpen);
});
const menuItems = menu.querySelectorAll('.dropdown-item');
menuItems.forEach((item: Element) => {
item.addEventListener('click', () => {
textSpan.textContent = item.textContent;
// Event delegation so dynamically rebuilt items (e.g. system fonts) keep working.
menu.addEventListener('click', (event: Event) => {
const item = (event.target as HTMLElement | null)?.closest('.dropdown-item') as HTMLElement | null;
if (!item || !menu.contains(item)) {
return;
}
// 分发给具体的函数去处理
switch (messageAction) {
case 'changeTheme':
setThemeMode((item as HTMLElement).dataset.value);
break;
case 'changeSettingsTheme':
setSurfaceTheme('settings', (item as HTMLElement).dataset.value);
break;
case 'changeCandTheme':
setSurfaceTheme('cand', (item as HTMLElement).dataset.value);
break;
case 'changeFtbTheme':
setSurfaceTheme('ftb', (item as HTMLElement).dataset.value);
break;
case 'changeMenuTheme':
setSurfaceTheme('menu', (item as HTMLElement).dataset.value);
break;
case 'changeCandidateArrange':
const htmlItem = item as HTMLElement;
applyCandidateArrange(htmlItem.dataset.value);
break;
default:
break;
}
textSpan.textContent = item.textContent;
if (window.chrome?.webview && configPath) {
const htmlItem = item as HTMLElement;
window.chrome.webview.postMessage(JSON.stringify({
type: 'configUpdate',
data: {
path: configPath,
value: valueTransform(htmlItem.dataset.value ?? '')
}
}));
} else if (window.chrome?.webview && messageAction === 'changeCandidateArrange') {
const htmlItem = item as HTMLElement;
window.chrome.webview.postMessage(JSON.stringify({
type: 'configUpdate',
data: {
path: 'appearance.candidate_window_layout',
value: htmlItem.dataset.value
}
}));
} else if (window.chrome?.webview) {
const htmlItem = item as HTMLElement;
window.chrome.webview.postMessage(JSON.stringify({
type: messageAction,
data: htmlItem.dataset.value
}));
}
switch (messageAction) {
case 'changeTheme':
setThemeMode(item.dataset.value);
break;
case 'changeSettingsTheme':
setSurfaceTheme('settings', item.dataset.value);
break;
case 'changeCandTheme':
setSurfaceTheme('cand', item.dataset.value);
break;
case 'changeFtbTheme':
setSurfaceTheme('ftb', item.dataset.value);
break;
case 'changeMenuTheme':
setSurfaceTheme('menu', item.dataset.value);
break;
case 'changeCandidateArrange':
applyCandidateArrange(item.dataset.value);
break;
default:
break;
}
menu.classList.remove('open');
});
// Always run transform first so local preview updates even without WebView2
// (e.g. vite browser preview).
const nextValue = configPath
? valueTransform(item.dataset.value ?? '')
: (item.dataset.value ?? '');
if (window.chrome?.webview && configPath) {
window.chrome.webview.postMessage(JSON.stringify({
type: 'configUpdate',
data: {
path: configPath,
value: nextValue
}
}));
} else if (window.chrome?.webview && messageAction === 'changeCandidateArrange') {
window.chrome.webview.postMessage(JSON.stringify({
type: 'configUpdate',
data: {
path: 'appearance.candidate_window_layout',
value: item.dataset.value
}
}));
} else if (window.chrome?.webview) {
window.chrome.webview.postMessage(JSON.stringify({
type: messageAction,
data: item.dataset.value
}));
}
menu.classList.remove('open');
});
// 点击外部关闭
@@ -111,10 +115,45 @@ export function applyDropdownValue(btnId: string, menuId: string, value: string
}
const btnLabel = document.querySelector<HTMLElement>(`#${btnId} span`);
const item = document.querySelector<HTMLElement>(`#${menuId} .dropdown-item[data-value="${value}"]`);
if (btnLabel && item) {
btnLabel.textContent = item.textContent;
if (!btnLabel) {
return;
}
const item = Array.from(document.querySelectorAll<HTMLElement>(`#${menuId} .dropdown-item`)).find(
(el) => el.dataset.value === value
);
btnLabel.textContent = item?.textContent || value;
}
export function populateDropdownMenu(
menuId: string,
values: string[],
options?: { selected?: string; previewFont?: boolean }
): void {
const menu = document.getElementById(menuId);
if (!menu) {
return;
}
const names = [...values];
const selected = options?.selected;
if (selected && !names.includes(selected)) {
names.unshift(selected);
}
const fragment = document.createDocumentFragment();
for (const name of names) {
const item = document.createElement('div');
item.className = 'dropdown-item';
item.dataset.value = name;
item.textContent = name;
if (options?.previewFont) {
const quoted = /\s/.test(name) ? `"${name.replace(/"/g, '\\"')}"` : name;
item.style.fontFamily = `${quoted}, sans-serif`;
}
fragment.appendChild(item);
}
menu.replaceChildren(fragment);
}
// 切换按钮功能
@@ -1,4 +1,5 @@
import { syncSkinPreviewTheme } from './skin';
import { onCandidateSurfaceThemeChanged } from './appearance';
export type ThemeMode = 'dark' | 'light' | 'system';
export type SurfaceTheme = 'follow' | 'dark' | 'light';
@@ -77,6 +78,7 @@ export function applyCandidatePreviewTheme(theme: ResolvedTheme): void {
element.classList.toggle('theme-light', theme === 'light');
element.classList.toggle('theme-dark', theme === 'dark');
});
onCandidateSurfaceThemeChanged(theme);
}
function ensureSystemThemeListener(onChange: () => void): void {
@@ -12,6 +12,87 @@
</div>
</div>
<!-- 候选窗样式 -->
<div class="section sec-cand-style">
<div class="section-header">
<div class="section-title">候选窗主字体</div>
<div class="dropdown">
<button class="dropdown-toggle" id="candEnglishFontBtn">
<span>Segoe UI</span>
<div class="arrow">
<img src="./assets/arrow.svg" alt="arrow">
</div>
</button>
<div class="dropdown-menu font-dropdown-menu compact-scroll-menu" id="candEnglishFontMenu"></div>
</div>
</div>
<div class="divider"></div>
<div class="section-header">
<div class="section-title">候选窗中文补充字体</div>
<div class="dropdown">
<button class="dropdown-toggle" id="candFontBtn">
<span>Noto Sans SC</span>
<div class="arrow">
<img src="./assets/arrow.svg" alt="arrow">
</div>
</button>
<div class="dropdown-menu font-dropdown-menu compact-scroll-menu" id="candFontMenu"></div>
</div>
</div>
<div class="divider"></div>
<div class="section-header">
<div class="section-title">候选窗字号</div>
<div class="dropdown">
<button class="dropdown-toggle" id="candFontSizeBtn">
<span>16</span>
<div class="arrow">
<img src="./assets/arrow.svg" alt="arrow">
</div>
</button>
<div class="dropdown-menu compact-scroll-menu" id="candFontSizeMenu">
<div class="dropdown-item" data-value="12">12</div>
<div class="dropdown-item" data-value="13">13</div>
<div class="dropdown-item" data-value="14">14</div>
<div class="dropdown-item" data-value="15">15</div>
<div class="dropdown-item" data-value="16">16</div>
<div class="dropdown-item" data-value="18">18</div>
<div class="dropdown-item" data-value="20">20</div>
<div class="dropdown-item" data-value="22">22</div>
<div class="dropdown-item" data-value="24">24</div>
</div>
</div>
</div>
<div class="divider"></div>
<div class="section-header">
<div class="section-title">候选文字颜色</div>
<div class="cand-color-control">
<input type="color" id="candTextColorInput" value="#e9e8e8" title="自定义颜色">
<button type="button" class="cand-color-reset" id="candTextColorResetBtn">跟随主题</button>
</div>
</div>
<div class="divider"></div>
<div class="section-header">
<div class="section-title">每页候选项数量</div>
<div class="dropdown">
<button class="dropdown-toggle" id="candPageSizeBtn">
<span>8</span>
<div class="arrow">
<img src="./assets/arrow.svg" alt="arrow">
</div>
</button>
<div class="dropdown-menu compact-scroll-menu" id="candPageSizeMenu">
<div class="dropdown-item" data-value="3">3</div>
<div class="dropdown-item" data-value="4">4</div>
<div class="dropdown-item" data-value="5">5</div>
<div class="dropdown-item" data-value="6">6</div>
<div class="dropdown-item" data-value="7">7</div>
<div class="dropdown-item" data-value="8">8</div>
<div class="dropdown-item" data-value="9">9</div>
</div>
</div>
</div>
</div>
<!-- 主题模式(全局) -->
<div class="section sec-theme">
<div class="section-header theme-toggle-header">
@@ -7,50 +7,50 @@
<!--1Anchor-->
<div class="row-wrapper first-row-wrapper">
<div class="row cand first">
<div class="text"><span class="num">1</span> 你们(rR)</div>
<div class="text"><span class="num">1</span> 你们<span class="cand-helpcode">(rR)</span></div>
</div>
</div>
<!--2Anchor-->
<div class="row-wrapper">
<div class="row cand">
<div class="text"><span class="num">2</span> 你(rX)</div>
<div class="text"><span class="num">2</span> 你<span class="cand-helpcode">(rX)</span></div>
</div>
</div>
<!--3Anchor-->
<div class="row-wrapper">
<div class="row cand">
<div class="text"><span class="num">3</span> 尼(uV)</div>
<div class="text"><span class="num">3</span> 尼<span class="cand-helpcode">(uV)</span></div>
</div>
</div>
<!--4Anchor-->
<div class="row-wrapper">
<div class="row cand">
<div class="text"><span class="num">4</span> 妮(nV)</div>
<div class="text"><span class="num">4</span> 妮<span class="cand-helpcode">(nV)</span></div>
</div>
</div>
<!--5Anchor-->
<div class="row-wrapper">
<div class="row cand">
<div class="text"><span class="num">5</span> 泥(dV)</div>
<div class="text"><span class="num">5</span> 泥<span class="cand-helpcode">(dV)</span></div>
</div>
</div>
<!--6Anchor-->
<div class="row-wrapper">
<div class="row cand">
<div class="text"><span class="num">6</span> 逆(zQ)</div>
<div class="text"><span class="num">6</span> 逆<span class="cand-helpcode">(zQ)</span></div>
</div>
</div>
<!--7Anchor-->
<div class="row-wrapper">
<div class="row cand">
<div class="text"><span class="num">7</span> 拟(fR)</div>
<div class="text"><span class="num">7</span> 拟<span class="cand-helpcode">(fR)</span></div>
</div>
</div>
<!--8Anchor-->
<div class="row-wrapper">
<div class="row cand">
<div class="text"><span class="num">8</span> 腻(oD)</div>
<div class="text"><span class="num">8</span> 腻<span class="cand-helpcode">(oD)</span></div>
</div>
</div>
<!--9Anchor-->
</div>
</div>
@@ -7,49 +7,49 @@
<!--1Anchor-->
<div class="row-wrapper">
<div class="row cand first">
<div class="text"><span class="cand-no">1</span><span class="cand-content">你们(rR)</span></div>
<div class="text"><span class="cand-no">1</span><span class="cand-content">你们<span class="cand-helpcode">(rR)</span></span></div>
</div>
</div>
<!--2Anchor-->
<div class="row-wrapper">
<div class="row cand">
<div class="text"><span class="cand-no">2</span><span class="cand-content">你(rX)</span></div>
<div class="text"><span class="cand-no">2</span><span class="cand-content">你<span class="cand-helpcode">(rX)</span></span></div>
</div>
</div>
<!--3Anchor-->
<div class="row-wrapper">
<div class="row cand">
<div class="text"><span class="cand-no">3</span><span class="cand-content">尼(uV)</span></div>
<div class="text"><span class="cand-no">3</span><span class="cand-content">尼<span class="cand-helpcode">(uV)</span></span></div>
</div>
</div>
<!--4Anchor-->
<div class="row-wrapper">
<div class="row cand">
<div class="text"><span class="cand-no">4</span><span class="cand-content">妮(nV)</span></div>
<div class="text"><span class="cand-no">4</span><span class="cand-content">妮<span class="cand-helpcode">(nV)</span></span></div>
</div>
</div>
<!--5Anchor-->
<div class="row-wrapper">
<div class="row cand">
<div class="text"><span class="cand-no">5</span><span class="cand-content">泥(dV)</span></div>
<div class="text"><span class="cand-no">5</span><span class="cand-content">泥<span class="cand-helpcode">(dV)</span></span></div>
</div>
</div>
<!--6Anchor-->
<div class="row-wrapper">
<div class="row cand">
<div class="text"><span class="cand-no">6</span><span class="cand-content">逆(zQ)</span></div>
<div class="text"><span class="cand-no">6</span><span class="cand-content">逆<span class="cand-helpcode">(zQ)</span></span></div>
</div>
</div>
<!--7Anchor-->
<div class="row-wrapper">
<div class="row cand">
<div class="text"><span class="cand-no">7</span><span class="cand-content">拟(fR)</span></div>
<div class="text"><span class="cand-no">7</span><span class="cand-content">拟<span class="cand-helpcode">(fR)</span></span></div>
</div>
</div>
<!--8Anchor-->
<div class="row-wrapper">
<div class="row cand">
<div class="text"><span class="cand-no">8</span><span class="cand-content">腻(oD)</span></div>
<div class="text"><span class="cand-no">8</span><span class="cand-content">腻<span class="cand-helpcode">(oD)</span></span></div>
</div>
</div>
<!--9Anchor-->
@@ -1,6 +1,5 @@
* {
box-sizing: border-box;
font-family: "Noto Sans SC", "Segoe UI Variable", "Segoe UI", sans-serif;
user-select: none;
}
@@ -59,6 +59,41 @@
display: block;
}
/* Show exactly 6 rows; scroll for the rest (fonts / long numeric lists). */
.dropdown-menu.compact-scroll-menu {
--compact-item-line: 20px;
--compact-item-pad-y: 4px;
--compact-item-margin-y: 2px;
/* 6 content boxes + 5 collapsed inter-item margins + outer top/bottom margins */
max-height: calc(
6 * (var(--compact-item-line) + 2 * var(--compact-item-pad-y)) +
5 * var(--compact-item-margin-y) +
2 * var(--compact-item-margin-y)
);
overflow-x: hidden;
overflow-y: auto;
}
.dropdown-menu.compact-scroll-menu .dropdown-item {
line-height: var(--compact-item-line);
padding-top: var(--compact-item-pad-y);
padding-bottom: var(--compact-item-pad-y);
margin-top: var(--compact-item-margin-y);
margin-bottom: var(--compact-item-margin-y);
}
.dropdown-menu.font-dropdown-menu {
/* Match the toggle button width (.dropdown is sized by the button). */
left: 0;
right: 0;
min-width: 0;
max-width: none;
}
.dropdown:has(> .font-dropdown-menu) .dropdown-toggle {
min-width: 168px;
}
.arrow {
width: 18px;
height: 18px;
@@ -22,6 +22,69 @@
display: flex;
flex-direction: column;
align-items: center;
font-family: var(--cand-font-family, inherit);
font-size: var(--cand-font-size, inherit);
}
.cand-preview .candidate,
.cand-preview .candidate .container,
.cand-preview .candidate .text,
.cand-preview .candidate .cand-content,
.cand-preview .candidate .cand-helpcode,
.cand-preview .candidate .num,
.cand-preview .candidate .cand-no,
.cand-preview .candidate .pinyin {
font-family: var(--cand-font-family, inherit) !important;
font-size: var(--cand-font-size, inherit);
}
.cand-preview .candidate .num,
.cand-preview .candidate .cand-no {
font-size: 0.8em;
}
.cand-preview .candidate .pinyin {
font-size: 14px;
}
.cand-preview .cand-helpcode[hidden] {
display: none !important;
}
.cand-color-control {
display: flex;
align-items: center;
gap: 10px;
}
.cand-color-control input[type="color"] {
width: 36px;
height: 28px;
padding: 0;
border: 1px solid var(--control-border);
border-radius: 6px;
background: transparent;
cursor: pointer;
}
.cand-color-reset {
border: 1px solid var(--control-border);
background: var(--button-secondary-bg);
color: var(--button-secondary-text);
border-radius: 6px;
padding: 4px 10px;
font-size: 12px;
cursor: pointer;
}
.cand-color-reset:hover {
background: var(--button-secondary-hover);
}
.cand-color-reset.is-active {
border-color: var(--accent-soft-border);
background: var(--accent-soft);
color: var(--text-color);
}
/**/