Expose `::part(dict-content)` on the MDict shadow content and add a dictionary popup font-size setting (Settings → Language → Dictionaries), independent of the main reading view. - mdictProvider: tag the in-shadow body with `part="dict-content"` and a stable `dict-shadow-host` class so the popup's `::part()` rule can reach across the shadow boundary — MDict is the only provider that renders into a shadow root, so ordinary popup CSS can't touch it. - DictionarySettings.fontScale (default 1) with setFontScale + load-merge; synced cross-device via the `dictionarySettings.fontScale` whitelist entry. - DictionaryResultsView drives `--dict-font-scale` + `data-dict-content` on each per-tab container. globals.css re-bases the light-DOM Tailwind text utilities to `em` within that scope and sizes the MDict shadow body via `::part(dict-content)`, so every provider scales from one lever. - SettingsSelect control (85–175%) in the Dictionaries panel. Tests: jsdom unit tests (part attribute, store fontScale, sync whitelist) plus a real-Chromium browser test for the em-rebasing + `::part` + custom- property-inheritance CSS contract jsdom cannot model. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+117
@@ -0,0 +1,117 @@
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
|
||||
// Real-browser regression test for readest#4443 (adjustable dictionary popup
|
||||
// font size). The feature hangs entirely on CSS behaviour jsdom cannot model:
|
||||
// 1. Tailwind `text-*` utilities re-based to `em` so they ride a scaled
|
||||
// `font-size` root on `[data-dict-content]`.
|
||||
// 2. `::part(dict-content)` reaching into the MDict shadow root, with the
|
||||
// `--dict-font-scale` custom property inheriting across the boundary.
|
||||
// Both need a layout engine + a real Shadow DOM + Custom Highlight-free part
|
||||
// styling, so this lives in the browser lane.
|
||||
//
|
||||
// The rules below mirror the `[data-dict-content]` block in
|
||||
// `src/styles/globals.css`; keep them byte-identical to what ships.
|
||||
const POPUP_FONT_RULES = `
|
||||
[data-dict-content] {
|
||||
font-size: calc(var(--dict-font-scale, 1) * 1em);
|
||||
}
|
||||
[data-dict-content] .text-xs { font-size: 0.75em; }
|
||||
[data-dict-content] .text-sm { font-size: 0.875em; }
|
||||
[data-dict-content] .text-base { font-size: 1em; }
|
||||
[data-dict-content] .text-lg { font-size: 1.125em; }
|
||||
[data-dict-content] .dict-shadow-host::part(dict-content) {
|
||||
font-size: calc(var(--dict-font-scale, 1) * 0.875rem);
|
||||
}
|
||||
`;
|
||||
|
||||
let style: HTMLStyleElement | null = null;
|
||||
let root: HTMLElement | null = null;
|
||||
|
||||
const px = (el: Element): number => parseFloat(getComputedStyle(el).fontSize);
|
||||
|
||||
/**
|
||||
* Build the exact DOM the popup produces for a card: a `[data-dict-content]`
|
||||
* content root holding a light-DOM headword + body (e.g. the DICT provider)
|
||||
* and an MDict shadow host whose shadow body carries `part="dict-content"`.
|
||||
*/
|
||||
const mountCard = (scale: number) => {
|
||||
root = document.createElement('div');
|
||||
// The popup's surrounding chrome inherits the document base; force a known
|
||||
// 16px base so `rem`/`em` math is deterministic regardless of UA defaults.
|
||||
root.style.fontSize = '16px';
|
||||
root.innerHTML = `
|
||||
<div data-dict-content style="--dict-font-scale: ${scale}">
|
||||
<h1 class="text-lg" data-testid="headword">word</h1>
|
||||
<pre class="text-sm" data-testid="light-body">a definition</pre>
|
||||
<div class="dict-shadow-host mt-2 text-sm" data-testid="host"></div>
|
||||
</div>`;
|
||||
document.body.appendChild(root);
|
||||
|
||||
const host = root.querySelector('[data-testid="host"]') as HTMLElement;
|
||||
const shadow = host.attachShadow({ mode: 'open' });
|
||||
const body = document.createElement('div');
|
||||
body.dataset['dictKind'] = 'mdict';
|
||||
body.setAttribute('part', 'dict-content');
|
||||
body.textContent = 'shadow definition';
|
||||
shadow.appendChild(body);
|
||||
|
||||
return {
|
||||
headword: root.querySelector('[data-testid="headword"]') as HTMLElement,
|
||||
lightBody: root.querySelector('[data-testid="light-body"]') as HTMLElement,
|
||||
shadowBody: body,
|
||||
};
|
||||
};
|
||||
|
||||
describe('dictionary popup font size (#4443)', () => {
|
||||
afterEach(() => {
|
||||
root?.remove();
|
||||
root = null;
|
||||
style?.remove();
|
||||
style = null;
|
||||
});
|
||||
|
||||
it('renders default sizes at scale 1 and scales every region linearly', () => {
|
||||
style = document.createElement('style');
|
||||
style.textContent = POPUP_FONT_RULES;
|
||||
document.head.appendChild(style);
|
||||
|
||||
const base = mountCard(1);
|
||||
// Defaults match the pre-feature look exactly: text-lg=18, text-sm=14,
|
||||
// and the MDict shadow body keeps its 0.875rem (14px) base.
|
||||
expect(px(base.headword)).toBeCloseTo(18, 1);
|
||||
expect(px(base.lightBody)).toBeCloseTo(14, 1);
|
||||
expect(px(base.shadowBody)).toBeCloseTo(14, 1);
|
||||
root!.remove();
|
||||
|
||||
const big = mountCard(1.5);
|
||||
// Light-DOM utilities ride the scaled `[data-dict-content]` root...
|
||||
expect(px(big.headword)).toBeCloseTo(27, 1); // 18 * 1.5
|
||||
expect(px(big.lightBody)).toBeCloseTo(21, 1); // 14 * 1.5
|
||||
// ...and `::part()` carries the same factor across the shadow boundary,
|
||||
// proving --dict-font-scale inherits into the shadow tree.
|
||||
expect(px(big.shadowBody)).toBeCloseTo(21, 1); // 14 * 1.5
|
||||
});
|
||||
|
||||
it('leaves the shadow body at its default when the part hook is absent', () => {
|
||||
// Without the part attribute the rule cannot reach the shadow content —
|
||||
// this guards against a future refactor dropping the hook silently.
|
||||
style = document.createElement('style');
|
||||
style.textContent = POPUP_FONT_RULES;
|
||||
document.head.appendChild(style);
|
||||
|
||||
root = document.createElement('div');
|
||||
root.style.fontSize = '16px';
|
||||
root.innerHTML = `<div data-dict-content style="--dict-font-scale: 1.5">
|
||||
<div class="dict-shadow-host" data-testid="host"></div>
|
||||
</div>`;
|
||||
document.body.appendChild(root);
|
||||
const host = root.querySelector('[data-testid="host"]') as HTMLElement;
|
||||
const shadow = host.attachShadow({ mode: 'open' });
|
||||
const body = document.createElement('div');
|
||||
body.style.fontSize = '0.875rem'; // its own author style, no part exposed
|
||||
body.textContent = 'unreachable';
|
||||
shadow.appendChild(body);
|
||||
|
||||
expect(px(body)).toBeCloseTo(14, 1); // unscaled — the ::part rule can't bind
|
||||
});
|
||||
});
|
||||
@@ -846,6 +846,44 @@ describe('mdictProvider', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('exposes part="dict-content" on the in-shadow body so outer CSS can size it via ::part()', async () => {
|
||||
const jsmdict = await import('js-mdict');
|
||||
const origMDXCreate = jsmdict.MDX.create.bind(jsmdict.MDX);
|
||||
type FakeMDX = ReturnType<typeof origMDXCreate> extends Promise<infer T> ? T : never;
|
||||
jsmdict.MDX.create = async () =>
|
||||
({
|
||||
meta: { encrypt: 0 },
|
||||
header: {},
|
||||
lookup: async (word: string) => ({
|
||||
keyText: word,
|
||||
definition: `<p>def</p>`,
|
||||
}),
|
||||
}) as unknown as FakeMDX;
|
||||
|
||||
try {
|
||||
const provider = createMdictProvider({ dict: buildDict(false), fs: makeFs() });
|
||||
const container = document.createElement('div');
|
||||
await provider.lookup('hello', {
|
||||
signal: new AbortController().signal,
|
||||
container,
|
||||
});
|
||||
|
||||
const shadow = getMdictShadow(container);
|
||||
// The content root carries BOTH the data-dict-kind hook and the
|
||||
// shadow `part` so the popup's `::part(dict-content)` font-size rule
|
||||
// can reach across the shadow boundary (it otherwise can't).
|
||||
const body = shadow.querySelector('[data-dict-kind="mdict"]');
|
||||
expect(body).not.toBeNull();
|
||||
expect(body!.getAttribute('part')).toBe('dict-content');
|
||||
// The host must be selectable from the outer tree for the `::part()`
|
||||
// rule's host selector to match.
|
||||
const host = container.lastElementChild as HTMLElement;
|
||||
expect(host.classList.contains('dict-shadow-host')).toBe(true);
|
||||
} finally {
|
||||
jsmdict.MDX.create = origMDXCreate;
|
||||
}
|
||||
});
|
||||
|
||||
it('keeps the auto-prepended headword when the dict body has a different h1 text', async () => {
|
||||
const jsmdict = await import('js-mdict');
|
||||
const origMDXCreate = jsmdict.MDX.create.bind(jsmdict.MDX);
|
||||
|
||||
@@ -141,6 +141,8 @@ describe('SETTINGS_WHITELIST', () => {
|
||||
expect(SETTINGS_WHITELIST).toContain('dictionarySettings.providerOrder');
|
||||
expect(SETTINGS_WHITELIST).toContain('dictionarySettings.providerEnabled');
|
||||
expect(SETTINGS_WHITELIST).toContain('dictionarySettings.webSearches');
|
||||
// Dictionary popup font size (#4443) follows the user across devices.
|
||||
expect(SETTINGS_WHITELIST).toContain('dictionarySettings.fontScale');
|
||||
});
|
||||
|
||||
test('does NOT sync dictionarySettings.defaultProviderId (per-device last-used tab)', () => {
|
||||
|
||||
@@ -695,3 +695,74 @@ describe('customDictionaryStore — loadCustomDictionaries reconciliation', () =
|
||||
expect('imp-tombstoned' in after.providerEnabled).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('customDictionaryStore — fontScale (dictionary popup font size, #4443)', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
useCustomDictionaryStore.setState({
|
||||
dictionaries: [],
|
||||
settings: {
|
||||
providerOrder: ['local-x'],
|
||||
providerEnabled: { 'local-x': true },
|
||||
webSearches: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('setFontScale updates the in-memory setting', () => {
|
||||
const { setFontScale } = useCustomDictionaryStore.getState();
|
||||
setFontScale(1.3);
|
||||
expect(useCustomDictionaryStore.getState().settings.fontScale).toBe(1.3);
|
||||
});
|
||||
|
||||
it('applyRemoteDictionarySettings overlays a remote fontScale patch', () => {
|
||||
const { applyRemoteDictionarySettings } = useCustomDictionaryStore.getState();
|
||||
applyRemoteDictionarySettings({ fontScale: 1.5 });
|
||||
expect(useCustomDictionaryStore.getState().settings.fontScale).toBe(1.5);
|
||||
});
|
||||
|
||||
it('loadCustomDictionaries defaults fontScale to 1 when the persisted settings omit it', async () => {
|
||||
type SettingsState = ReturnType<typeof useSettingsStore.getState>;
|
||||
useSettingsStore.setState({
|
||||
settings: {
|
||||
customDictionaries: [],
|
||||
dictionarySettings: {
|
||||
providerOrder: ['builtin:wikipedia'],
|
||||
providerEnabled: { 'builtin:wikipedia': true },
|
||||
webSearches: [],
|
||||
},
|
||||
} as unknown as SettingsState['settings'],
|
||||
} as unknown as SettingsState);
|
||||
|
||||
const fakeAppService = { exists: vi.fn().mockResolvedValue(false) };
|
||||
const fakeEnv = {
|
||||
getAppService: () => Promise.resolve(fakeAppService),
|
||||
} as unknown as EnvConfigType;
|
||||
|
||||
await useCustomDictionaryStore.getState().loadCustomDictionaries(fakeEnv);
|
||||
expect(useCustomDictionaryStore.getState().settings.fontScale).toBe(1);
|
||||
});
|
||||
|
||||
it('loadCustomDictionaries preserves a persisted fontScale', async () => {
|
||||
type SettingsState = ReturnType<typeof useSettingsStore.getState>;
|
||||
useSettingsStore.setState({
|
||||
settings: {
|
||||
customDictionaries: [],
|
||||
dictionarySettings: {
|
||||
providerOrder: ['builtin:wikipedia'],
|
||||
providerEnabled: { 'builtin:wikipedia': true },
|
||||
webSearches: [],
|
||||
fontScale: 1.15,
|
||||
},
|
||||
} as unknown as SettingsState['settings'],
|
||||
} as unknown as SettingsState);
|
||||
|
||||
const fakeAppService = { exists: vi.fn().mockResolvedValue(false) };
|
||||
const fakeEnv = {
|
||||
getAppService: () => Promise.resolve(fakeAppService),
|
||||
} as unknown as EnvConfigType;
|
||||
|
||||
await useCustomDictionaryStore.getState().loadCustomDictionaries(fakeEnv);
|
||||
expect(useCustomDictionaryStore.getState().settings.fontScale).toBe(1.15);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -49,6 +49,8 @@ export interface DictionaryResultsState {
|
||||
resolveWebSearchUrl: (id: string) => string | undefined;
|
||||
onWebSearchClickTauri: (e: React.MouseEvent<HTMLAnchorElement>, id: string) => void;
|
||||
noProvidersAtAll: boolean;
|
||||
/** Dictionary popup font-size multiplier (#4443); `1` = default sizes. */
|
||||
fontScale: number;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -312,6 +314,7 @@ export function useDictionaryResults({
|
||||
resolveWebSearchUrl,
|
||||
onWebSearchClickTauri,
|
||||
noProvidersAtAll,
|
||||
fontScale: settings.fontScale ?? 1,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -377,6 +380,7 @@ export const DictionaryResultsBody: React.FC<DictionaryResultsBodyProps> = ({
|
||||
resolveWebSearchUrl,
|
||||
onWebSearchClickTauri,
|
||||
noProvidersAtAll,
|
||||
fontScale,
|
||||
}) => {
|
||||
const _ = useTranslation();
|
||||
return (
|
||||
@@ -440,6 +444,12 @@ export const DictionaryResultsBody: React.FC<DictionaryResultsBodyProps> = ({
|
||||
<div
|
||||
ref={setContainerRef(p.id)}
|
||||
onClick={handleContainerClick}
|
||||
// `data-dict-content` + `--dict-font-scale` drive the
|
||||
// popup font-size rules in globals.css (#4443): the
|
||||
// light-DOM `font-size` cascade and the MDict shadow
|
||||
// `::part(dict-content)` rule both read this scope.
|
||||
data-dict-content=''
|
||||
style={{ '--dict-font-scale': fontScale } as React.CSSProperties}
|
||||
className={clsx(
|
||||
'font-sans',
|
||||
isLoading && 'hidden',
|
||||
|
||||
@@ -44,7 +44,17 @@ import {
|
||||
isValidUrlTemplate,
|
||||
} from '@/services/dictionaries/webSearchTemplates';
|
||||
import SubPageHeader from './SubPageHeader';
|
||||
import { Tips } from './primitives';
|
||||
import { BoxedList, SettingsRow, SettingsSelect, Tips } from './primitives';
|
||||
|
||||
/** Dictionary popup font-size multipliers, surfaced as percentages (#4443). */
|
||||
const FONT_SCALE_OPTIONS = [
|
||||
{ value: '0.85', label: '85%' },
|
||||
{ value: '1', label: '100%' },
|
||||
{ value: '1.15', label: '115%' },
|
||||
{ value: '1.3', label: '130%' },
|
||||
{ value: '1.5', label: '150%' },
|
||||
{ value: '1.75', label: '175%' },
|
||||
];
|
||||
|
||||
interface CustomDictionariesProps {
|
||||
onBack: () => void;
|
||||
@@ -254,6 +264,7 @@ const CustomDictionaries: React.FC<CustomDictionariesProps> = ({ onBack }) => {
|
||||
updateDictionary,
|
||||
reorder,
|
||||
setEnabled,
|
||||
setFontScale,
|
||||
addWebSearch,
|
||||
updateWebSearch,
|
||||
removeWebSearch,
|
||||
@@ -284,6 +295,11 @@ const CustomDictionaries: React.FC<CustomDictionariesProps> = ({ onBack }) => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [appService]);
|
||||
const handleFontScaleChange = async (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
setFontScale(Number(e.target.value));
|
||||
await saveCustomDictionaries(envConfig);
|
||||
};
|
||||
|
||||
const handleResetLookupApp = async () => {
|
||||
await clearRememberedLookupApp();
|
||||
setRememberedLookupApp(null);
|
||||
@@ -769,6 +785,23 @@ const CustomDictionaries: React.FC<CustomDictionariesProps> = ({ onBack }) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<BoxedList
|
||||
className='mt-4'
|
||||
title={_('Appearance')}
|
||||
description={_(
|
||||
'Sets the text size of dictionary results, independent of the reading view.',
|
||||
)}
|
||||
>
|
||||
<SettingsRow label={_('Font Size')}>
|
||||
<SettingsSelect
|
||||
value={String(settings.fontScale ?? 1)}
|
||||
onChange={handleFontScaleChange}
|
||||
options={FONT_SCALE_OPTIONS}
|
||||
ariaLabel={_('Font Size')}
|
||||
/>
|
||||
</SettingsRow>
|
||||
</BoxedList>
|
||||
|
||||
<div className='mt-4 grid grid-cols-1 gap-2 sm:grid-cols-2'>
|
||||
<button
|
||||
type='button'
|
||||
|
||||
@@ -579,6 +579,12 @@ export const createMdictProvider = ({
|
||||
// surrounding chrome.
|
||||
const body = document.createElement('div');
|
||||
body.dataset['dictKind'] = dict.kind;
|
||||
// Expose the content root as a shadow `part` (#4443). The MDict body
|
||||
// lives inside a shadow root, so the popup's font-size rule can't
|
||||
// reach it with an ordinary descendant selector — `::part(dict-content)`
|
||||
// is the only hook that crosses the boundary. The matching host class
|
||||
// (`dict-shadow-host`, set below) is the selector subject for that rule.
|
||||
body.setAttribute('part', 'dict-content');
|
||||
body.innerHTML = result.definition;
|
||||
|
||||
await resolveImageResources(body, mdds, ctx.signal, trackedUrls);
|
||||
@@ -605,7 +611,9 @@ export const createMdictProvider = ({
|
||||
// into the app's layout. Click events still bubble naturally so the
|
||||
// `sound://` / `entry://` / external-link delegation keeps working.
|
||||
const shadowHost = document.createElement('div');
|
||||
shadowHost.className = 'mt-2 text-sm';
|
||||
// `dict-shadow-host` is the stable host selector the popup's
|
||||
// `::part(dict-content)` font-size rule targets (#4443).
|
||||
shadowHost.className = 'dict-shadow-host mt-2 text-sm';
|
||||
ctx.container.appendChild(shadowHost);
|
||||
const shadow = shadowHost.attachShadow({ mode: 'open' });
|
||||
// Baseline app-level styles first (theme-aware blend rules for
|
||||
|
||||
@@ -170,6 +170,14 @@ export interface DictionarySettings {
|
||||
* Merriam-Webster) are hardcoded in the registry and not stored here.
|
||||
*/
|
||||
webSearches?: WebSearchEntry[];
|
||||
/**
|
||||
* Font-size multiplier for the dictionary popup content (independent of the
|
||||
* main reading view, #4443). `1` = the default sizes; larger values scale
|
||||
* every provider's rendered definition up. Drives the `--dict-font-scale`
|
||||
* CSS variable on the popup content root, which feeds the light-DOM
|
||||
* `font-size` rules and the MDict shadow `::part(dict-content)` rule alike.
|
||||
*/
|
||||
fontScale?: number;
|
||||
}
|
||||
|
||||
/** Stable ids for the built-in providers. */
|
||||
|
||||
@@ -49,6 +49,7 @@ export const SETTINGS_WHITELIST = [
|
||||
'dictionarySettings.providerOrder',
|
||||
'dictionarySettings.providerEnabled',
|
||||
'dictionarySettings.webSearches',
|
||||
'dictionarySettings.fontScale',
|
||||
// External integrations. Server URL + identifiers sync as plaintext;
|
||||
// the credential fields are listed in `encryptedFields` below so the
|
||||
// publish/pull middleware wraps them in cipher envelopes.
|
||||
|
||||
@@ -53,6 +53,7 @@ const DEFAULT_DICTIONARY_SETTINGS: DictionarySettings = {
|
||||
[BUILTIN_WEB_SEARCH_IDS.goodreads]: false,
|
||||
},
|
||||
webSearches: [],
|
||||
fontScale: 1,
|
||||
};
|
||||
|
||||
interface DictionaryStoreState {
|
||||
@@ -114,6 +115,8 @@ interface DictionaryStoreState {
|
||||
setEnabled(id: string, enabled: boolean): void;
|
||||
/** Persist the last-used tab id so the popup re-opens on it. */
|
||||
setDefaultProviderId(id: string | undefined): void;
|
||||
/** Set the dictionary popup font-size multiplier (#4443). */
|
||||
setFontScale(scale: number): void;
|
||||
|
||||
/** Add a custom web search (id is generated). Appended + enabled by default. */
|
||||
addWebSearch(name: string, urlTemplate: string): WebSearchEntry;
|
||||
@@ -435,6 +438,12 @@ export const useCustomDictionaryStore = create<DictionaryStoreState>((set, get)
|
||||
}));
|
||||
},
|
||||
|
||||
setFontScale: (scale) => {
|
||||
set((state) => ({
|
||||
settings: { ...state.settings, fontScale: scale },
|
||||
}));
|
||||
},
|
||||
|
||||
addWebSearch: (name, urlTemplate) => {
|
||||
const trimmedName = name.trim();
|
||||
const trimmedUrl = urlTemplate.trim();
|
||||
@@ -582,6 +591,7 @@ export const useCustomDictionaryStore = create<DictionaryStoreState>((set, get)
|
||||
},
|
||||
defaultProviderId: persistedSettings.defaultProviderId,
|
||||
webSearches: persistedSettings.webSearches ?? [],
|
||||
fontScale: persistedSettings.fontScale ?? DEFAULT_DICTIONARY_SETTINGS.fontScale,
|
||||
};
|
||||
set({ dictionaries, settings: settingsMerged });
|
||||
} catch (error) {
|
||||
|
||||
@@ -665,6 +665,45 @@ input[type='range'].slider-input {
|
||||
--tglbg: theme('colors.base-100') !important;
|
||||
}
|
||||
|
||||
/* Dictionary popup font size (#4443).
|
||||
*
|
||||
* `--dict-font-scale` is set per content card by DictionaryResultsView; `1`
|
||||
* = default sizes. It scales every provider's rendered definition while
|
||||
* leaving the popup chrome (header, source label, web-search rows — all
|
||||
* outside this scope) untouched.
|
||||
*
|
||||
* `[data-dict-content]` is the per-tab content root. Light-DOM providers
|
||||
* size text with Tailwind `text-*` utilities (root-relative `rem`), which a
|
||||
* container `font-size` alone can't move — so those utilities are re-based to
|
||||
* `em` WITHIN THIS SCOPE ONLY, letting them ride the scaled root.
|
||||
*
|
||||
* MDict renders into a shadow root, so its body is unreachable by ordinary
|
||||
* selectors; `::part(dict-content)` (exposed on the body in mdictProvider) is
|
||||
* the only hook that crosses the boundary. */
|
||||
[data-dict-content] {
|
||||
font-size: calc(var(--dict-font-scale, 1) * 1em);
|
||||
}
|
||||
|
||||
[data-dict-content] .text-xs {
|
||||
font-size: 0.75em;
|
||||
}
|
||||
|
||||
[data-dict-content] .text-sm {
|
||||
font-size: 0.875em;
|
||||
}
|
||||
|
||||
[data-dict-content] .text-base {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
[data-dict-content] .text-lg {
|
||||
font-size: 1.125em;
|
||||
}
|
||||
|
||||
[data-dict-content] .dict-shadow-host::part(dict-content) {
|
||||
font-size: calc(var(--dict-font-scale, 1) * 0.875rem);
|
||||
}
|
||||
|
||||
.no-transitions * {
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user