fix: polish typography settings and storage helpers
This commit is contained in:
135
package-lock.json
generated
135
package-lock.json
generated
@@ -34,7 +34,6 @@
|
|||||||
"eslint-plugin-react-hooks": "^7.0.1",
|
"eslint-plugin-react-hooks": "^7.0.1",
|
||||||
"eslint-plugin-react-refresh": "^0.5.0",
|
"eslint-plugin-react-refresh": "^0.5.0",
|
||||||
"globals": "^16.5.0",
|
"globals": "^16.5.0",
|
||||||
"happy-dom": "^20.8.3",
|
|
||||||
"jsdom": "^28.1.0",
|
"jsdom": "^28.1.0",
|
||||||
"typescript": "~5.9.3",
|
"typescript": "~5.9.3",
|
||||||
"typescript-eslint": "^8.55.0",
|
"typescript-eslint": "^8.55.0",
|
||||||
@@ -1792,6 +1791,60 @@
|
|||||||
"node": ">=14.0.0"
|
"node": ">=14.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": {
|
||||||
|
"version": "1.7.1",
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@emnapi/wasi-threads": "1.1.0",
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": {
|
||||||
|
"version": "1.7.1",
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@emnapi/core": "^1.7.1",
|
||||||
|
"@emnapi/runtime": "^1.7.1",
|
||||||
|
"@tybys/wasm-util": "^0.10.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": {
|
||||||
|
"version": "0.10.1",
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": {
|
||||||
|
"version": "2.8.1",
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "0BSD",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
|
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
|
||||||
"version": "4.1.18",
|
"version": "4.1.18",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz",
|
||||||
@@ -2046,23 +2099,6 @@
|
|||||||
"integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
|
"integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/whatwg-mimetype": {
|
|
||||||
"version": "3.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz",
|
|
||||||
"integrity": "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@types/ws": {
|
|
||||||
"version": "8.18.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
|
|
||||||
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/node": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "8.55.0",
|
"version": "8.55.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.55.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.55.0.tgz",
|
||||||
@@ -3492,47 +3528,6 @@
|
|||||||
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/happy-dom": {
|
|
||||||
"version": "20.8.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-20.8.3.tgz",
|
|
||||||
"integrity": "sha512-lMHQRRwIPyJ70HV0kkFT7jH/gXzSI7yDkQFe07E2flwmNDFoWUTRMKpW2sglsnpeA7b6S2TJPp98EbQxai8eaQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/node": ">=20.0.0",
|
|
||||||
"@types/whatwg-mimetype": "^3.0.2",
|
|
||||||
"@types/ws": "^8.18.1",
|
|
||||||
"entities": "^7.0.1",
|
|
||||||
"whatwg-mimetype": "^3.0.0",
|
|
||||||
"ws": "^8.18.3"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=20.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/happy-dom/node_modules/entities": {
|
|
||||||
"version": "7.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
|
|
||||||
"integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "BSD-2-Clause",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.12"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/happy-dom/node_modules/whatwg-mimetype": {
|
|
||||||
"version": "3.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz",
|
|
||||||
"integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/has-flag": {
|
"node_modules/has-flag": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||||
@@ -6574,28 +6569,6 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ws": {
|
|
||||||
"version": "8.19.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
|
|
||||||
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10.0.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"bufferutil": "^4.0.1",
|
|
||||||
"utf-8-validate": ">=5.0.2"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"bufferutil": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"utf-8-validate": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/xml-name-validator": {
|
"node_modules/xml-name-validator": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
|
||||||
|
|||||||
@@ -62,7 +62,6 @@
|
|||||||
"eslint-plugin-react-hooks": "^7.0.1",
|
"eslint-plugin-react-hooks": "^7.0.1",
|
||||||
"eslint-plugin-react-refresh": "^0.5.0",
|
"eslint-plugin-react-refresh": "^0.5.0",
|
||||||
"globals": "^16.5.0",
|
"globals": "^16.5.0",
|
||||||
"happy-dom": "^20.8.3",
|
|
||||||
"jsdom": "^28.1.0",
|
"jsdom": "^28.1.0",
|
||||||
"typescript": "~5.9.3",
|
"typescript": "~5.9.3",
|
||||||
"typescript-eslint": "^8.55.0",
|
"typescript-eslint": "^8.55.0",
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useSendShortcut } from '../hooks/useSendShortcut';
|
|||||||
import { useT, useLocale } from '../hooks/useLocale';
|
import { useT, useLocale } from '../hooks/useLocale';
|
||||||
import { setLocale, supportedLocales, localeLabels } from '../lib/i18n';
|
import { setLocale, supportedLocales, localeLabels } from '../lib/i18n';
|
||||||
import type { ThemeName, AccentColor, UiFont, MonoFont } from '../contexts/ThemeContextDef';
|
import type { ThemeName, AccentColor, UiFont, MonoFont } from '../contexts/ThemeContextDef';
|
||||||
|
import { uiFontStacks, monoFontStacks } from '../contexts/ThemeContextDef';
|
||||||
import type { TranslationKey } from '../lib/i18n';
|
import type { TranslationKey } from '../lib/i18n';
|
||||||
|
|
||||||
const themeOptions: { value: ThemeName; icon: typeof Sun; labelKey: TranslationKey }[] = [
|
const themeOptions: { value: ThemeName; icon: typeof Sun; labelKey: TranslationKey }[] = [
|
||||||
@@ -91,20 +92,6 @@ export function SettingsModal({ open, onClose, soundEnabled, onToggleSound }: Pr
|
|||||||
return base;
|
return base;
|
||||||
}, [onToggleSound, t]);
|
}, [onToggleSound, t]);
|
||||||
|
|
||||||
const uiFontStacks: Record<UiFont, string> = {
|
|
||||||
system: "-apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif",
|
|
||||||
inter: "'Inter', 'Segoe UI', system-ui, sans-serif",
|
|
||||||
segoe: "'Segoe UI Variable Text', 'Segoe UI', system-ui, sans-serif",
|
|
||||||
sf: "'SF Pro Text', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif",
|
|
||||||
};
|
|
||||||
|
|
||||||
const monoFontStacks: Record<MonoFont, string> = {
|
|
||||||
jetbrains: "'JetBrains Mono', 'Cascadia Code', 'Fira Code', ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
|
|
||||||
fira: "'Fira Code', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
|
|
||||||
cascadia: "'Cascadia Code', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
|
|
||||||
'system-mono': "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
|
|
||||||
};
|
|
||||||
|
|
||||||
const uiFontOptions: { value: UiFont; label: string; sample: string }[] = [
|
const uiFontOptions: { value: UiFont; label: string; sample: string }[] = [
|
||||||
{ value: 'system', label: 'System', sample: 'Segoe/UI' },
|
{ value: 'system', label: 'System', sample: 'Segoe/UI' },
|
||||||
{ value: 'inter', label: 'Inter', sample: 'Inter' },
|
{ value: 'inter', label: 'Inter', sample: 'Inter' },
|
||||||
@@ -146,7 +133,7 @@ export function SettingsModal({ open, onClose, soundEnabled, onToggleSound }: Pr
|
|||||||
|
|
||||||
{/* Modal */}
|
{/* Modal */}
|
||||||
<div
|
<div
|
||||||
className="relative w-full max-w-md mx-4 rounded-3xl border border-pc-border bg-[var(--pc-bg-base)]/95 backdrop-blur-xl shadow-2xl animate-fade-in"
|
className="relative w-full max-w-xl mx-4 rounded-3xl border border-pc-border bg-[var(--pc-bg-base)]/95 backdrop-blur-xl shadow-2xl animate-fade-in"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
@@ -308,6 +295,8 @@ export function SettingsModal({ open, onClose, soundEnabled, onToggleSound }: Pr
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="text-[11px] text-pc-text-faint mb-4 text-center">{t('settings.fontNote')}</div>
|
||||||
|
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<div className="text-xs text-pc-text-secondary mb-2">{t('settings.fontSize')}</div>
|
<div className="text-xs text-pc-text-secondary mb-2">{t('settings.fontSize')}</div>
|
||||||
<div className="flex gap-1.5 flex-wrap">
|
<div className="flex gap-1.5 flex-wrap">
|
||||||
@@ -351,11 +340,12 @@ export function SettingsModal({ open, onClose, soundEnabled, onToggleSound }: Pr
|
|||||||
<div className="rounded-2xl border border-pc-border bg-[var(--pc-bg-surface)]/70 p-3">
|
<div className="rounded-2xl border border-pc-border bg-[var(--pc-bg-surface)]/70 p-3">
|
||||||
<div className="text-[11px] uppercase tracking-wide text-pc-text-faint mb-2">{t('settings.preview')}</div>
|
<div className="text-[11px] uppercase tracking-wide text-pc-text-faint mb-2">{t('settings.preview')}</div>
|
||||||
<div className="text-sm text-pc-text mb-2" style={{ fontFamily: 'var(--pc-font-ui)', fontSize: 'var(--pc-font-size)' }}>
|
<div className="text-sm text-pc-text mb-2" style={{ fontFamily: 'var(--pc-font-ui)', fontSize: 'var(--pc-font-size)' }}>
|
||||||
The quick brown fox jumps over the lazy dog — 1234567890
|
{t('settings.previewText')}
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-xl border border-dashed border-pc-border p-2 bg-[var(--pc-bg-code)] text-[13px]" style={{ fontFamily: 'var(--pc-font-mono)', fontSize: 'var(--pc-font-size-mono)' }}>
|
<div className="rounded-xl border border-dashed border-pc-border p-2 bg-[var(--pc-bg-code)] text-[13px]" style={{ fontFamily: 'var(--pc-font-mono)', fontSize: 'var(--pc-font-size-mono)' }}>
|
||||||
const hello = 'PinchChat'; // typography preview
|
const hello = 'PinchChat'; // typography preview
|
||||||
</div>
|
</div>
|
||||||
|
<div className="text-[11px] text-pc-text-faint mt-2 text-center">{t('settings.fontNote')}</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,19 +1,9 @@
|
|||||||
import { useState, useEffect, useCallback, type ReactNode } from 'react';
|
import { useState, useEffect, useCallback, type ReactNode } from 'react';
|
||||||
import { ThemeContext, type ThemeName, type AccentColor, type UiFont, type MonoFont } from './ThemeContextDef';
|
import { ThemeContext, type ThemeName, type AccentColor, type UiFont, type MonoFont, uiFontStacks, monoFontStacks } from './ThemeContextDef';
|
||||||
|
import { loadStored, STORAGE_KEY } from './themeStorage';
|
||||||
|
|
||||||
export type { ThemeName, AccentColor } from './ThemeContextDef';
|
export type { ThemeName, AccentColor } from './ThemeContextDef';
|
||||||
|
|
||||||
const STORAGE_KEY = 'pinchchat-theme';
|
|
||||||
|
|
||||||
interface StoredTheme {
|
|
||||||
theme: ThemeName;
|
|
||||||
accent: AccentColor;
|
|
||||||
uiFont: UiFont;
|
|
||||||
monoFont: MonoFont;
|
|
||||||
uiFontSize: number;
|
|
||||||
monoFontSize: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
type ConcreteTheme = 'dark' | 'light' | 'oled';
|
type ConcreteTheme = 'dark' | 'light' | 'oled';
|
||||||
const themes: Record<ConcreteTheme, Record<string, string>> = {
|
const themes: Record<ConcreteTheme, Record<string, string>> = {
|
||||||
dark: {
|
dark: {
|
||||||
@@ -123,20 +113,6 @@ const accents: Record<AccentColor, Record<string, string>> = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const uiFonts: Record<UiFont, string> = {
|
|
||||||
system: "-apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif",
|
|
||||||
inter: "'Inter', 'Segoe UI', system-ui, sans-serif",
|
|
||||||
segoe: "'Segoe UI Variable Text', 'Segoe UI', system-ui, sans-serif",
|
|
||||||
sf: "'SF Pro Text', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif",
|
|
||||||
};
|
|
||||||
|
|
||||||
const monoFonts: Record<MonoFont, string> = {
|
|
||||||
jetbrains: "'JetBrains Mono', 'Cascadia Code', 'Fira Code', ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
|
|
||||||
fira: "'Fira Code', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
|
|
||||||
cascadia: "'Cascadia Code', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
|
|
||||||
'system-mono': "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
|
|
||||||
};
|
|
||||||
|
|
||||||
function applyVars(vars: Record<string, string>) {
|
function applyVars(vars: Record<string, string>) {
|
||||||
const root = document.documentElement;
|
const root = document.documentElement;
|
||||||
for (const [k, v] of Object.entries(vars)) {
|
for (const [k, v] of Object.entries(vars)) {
|
||||||
@@ -152,36 +128,19 @@ function resolveTheme(name: ThemeName): 'dark' | 'light' | 'oled' {
|
|||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadStored(): StoredTheme {
|
interface ThemeSettings {
|
||||||
try {
|
theme: ThemeName;
|
||||||
const raw = localStorage.getItem(STORAGE_KEY);
|
accent: AccentColor;
|
||||||
if (raw) {
|
uiFont: UiFont;
|
||||||
const parsed = JSON.parse(raw);
|
monoFont: MonoFont;
|
||||||
const themeValid = parsed.theme in themes || parsed.theme === 'system';
|
uiFontSize: number;
|
||||||
const accentValid = parsed.accent in accents;
|
monoFontSize: number;
|
||||||
const uiFont: UiFont = uiFonts[parsed.uiFont as UiFont] ? parsed.uiFont : 'system';
|
|
||||||
const monoFont: MonoFont = monoFonts[parsed.monoFont as MonoFont] ? parsed.monoFont : 'jetbrains';
|
|
||||||
const uiFontSize = Number.isFinite(parsed.uiFontSize) ? Math.min(20, Math.max(12, Number(parsed.uiFontSize))) : 15;
|
|
||||||
const monoFontSize = Number.isFinite(parsed.monoFontSize) ? Math.min(20, Math.max(12, Number(parsed.monoFontSize))) : 14;
|
|
||||||
if (themeValid && accentValid) {
|
|
||||||
return {
|
|
||||||
theme: parsed.theme,
|
|
||||||
accent: parsed.accent,
|
|
||||||
uiFont,
|
|
||||||
monoFont,
|
|
||||||
uiFontSize,
|
|
||||||
monoFontSize,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch { /* ignore invalid stored JSON */ }
|
|
||||||
return { theme: 'dark', accent: 'cyan', uiFont: 'system', monoFont: 'jetbrains', uiFontSize: 15, monoFontSize: 14 };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function fontVars(uiFont: UiFont, monoFont: MonoFont, uiFontSize: number, monoFontSize: number) {
|
function fontVars(uiFont: UiFont, monoFont: MonoFont, uiFontSize: number, monoFontSize: number) {
|
||||||
return {
|
return {
|
||||||
'--pc-font-ui': uiFonts[uiFont],
|
'--pc-font-ui': uiFontStacks[uiFont],
|
||||||
'--pc-font-mono': monoFonts[monoFont],
|
'--pc-font-mono': monoFontStacks[monoFont],
|
||||||
'--pc-font-size': `${uiFontSize}px`,
|
'--pc-font-size': `${uiFontSize}px`,
|
||||||
'--pc-font-size-mono': `${monoFontSize}px`,
|
'--pc-font-size-mono': `${monoFontSize}px`,
|
||||||
};
|
};
|
||||||
@@ -196,73 +155,79 @@ export function ThemeProvider({ children }: { children: ReactNode }) {
|
|||||||
const [uiFontSize, setUiFontSizeState] = useState<number>(stored.uiFontSize);
|
const [uiFontSize, setUiFontSizeState] = useState<number>(stored.uiFontSize);
|
||||||
const [monoFontSize, setMonoFontSizeState] = useState<number>(stored.monoFontSize);
|
const [monoFontSize, setMonoFontSizeState] = useState<number>(stored.monoFontSize);
|
||||||
|
|
||||||
const persist = useCallback((t: ThemeName, a: AccentColor, ui: UiFont, mono: MonoFont, uiSize: number, monoSize: number) => {
|
const persist = useCallback((s: ThemeSettings) => {
|
||||||
localStorage.setItem(STORAGE_KEY, JSON.stringify({
|
localStorage.setItem(STORAGE_KEY, JSON.stringify({
|
||||||
theme: t,
|
theme: s.theme,
|
||||||
accent: a,
|
accent: s.accent,
|
||||||
uiFont: ui,
|
uiFont: s.uiFont,
|
||||||
monoFont: mono,
|
monoFont: s.monoFont,
|
||||||
uiFontSize: uiSize,
|
uiFontSize: s.uiFontSize,
|
||||||
monoFontSize: monoSize,
|
monoFontSize: s.monoFontSize,
|
||||||
}));
|
}));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const applyAll = useCallback((nextTheme: ThemeName, nextAccent: AccentColor, nextUiFont: UiFont, nextMonoFont: MonoFont, nextUiSize: number, nextMonoSize: number) => {
|
const applyAll = useCallback((s: ThemeSettings) => {
|
||||||
applyVars({
|
applyVars({
|
||||||
...themes[resolveTheme(nextTheme)],
|
...themes[resolveTheme(s.theme)],
|
||||||
...accents[nextAccent],
|
...accents[s.accent],
|
||||||
...fontVars(nextUiFont, nextMonoFont, nextUiSize, nextMonoSize),
|
...fontVars(s.uiFont, s.monoFont, s.uiFontSize, s.monoFontSize),
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const setTheme = useCallback((t: ThemeName) => {
|
const setTheme = useCallback((t: ThemeName) => {
|
||||||
setThemeState(t);
|
setThemeState(t);
|
||||||
applyAll(t, accent, uiFont, monoFont, uiFontSize, monoFontSize);
|
const next: ThemeSettings = { theme: t, accent, uiFont, monoFont, uiFontSize, monoFontSize };
|
||||||
persist(t, accent, uiFont, monoFont, uiFontSize, monoFontSize);
|
applyAll(next);
|
||||||
|
persist(next);
|
||||||
}, [accent, applyAll, persist, uiFont, monoFont, uiFontSize, monoFontSize]);
|
}, [accent, applyAll, persist, uiFont, monoFont, uiFontSize, monoFontSize]);
|
||||||
|
|
||||||
const setAccent = useCallback((a: AccentColor) => {
|
const setAccent = useCallback((a: AccentColor) => {
|
||||||
setAccentState(a);
|
setAccentState(a);
|
||||||
applyAll(theme, a, uiFont, monoFont, uiFontSize, monoFontSize);
|
const next: ThemeSettings = { theme, accent: a, uiFont, monoFont, uiFontSize, monoFontSize };
|
||||||
persist(theme, a, uiFont, monoFont, uiFontSize, monoFontSize);
|
applyAll(next);
|
||||||
|
persist(next);
|
||||||
}, [theme, applyAll, persist, uiFont, monoFont, uiFontSize, monoFontSize]);
|
}, [theme, applyAll, persist, uiFont, monoFont, uiFontSize, monoFontSize]);
|
||||||
|
|
||||||
const setUiFont = useCallback((f: UiFont) => {
|
const setUiFont = useCallback((f: UiFont) => {
|
||||||
setUiFontState(f);
|
setUiFontState(f);
|
||||||
applyAll(theme, accent, f, monoFont, uiFontSize, monoFontSize);
|
const next: ThemeSettings = { theme, accent, uiFont: f, monoFont, uiFontSize, monoFontSize };
|
||||||
persist(theme, accent, f, monoFont, uiFontSize, monoFontSize);
|
applyAll(next);
|
||||||
|
persist(next);
|
||||||
}, [accent, theme, applyAll, persist, monoFont, uiFontSize, monoFontSize]);
|
}, [accent, theme, applyAll, persist, monoFont, uiFontSize, monoFontSize]);
|
||||||
|
|
||||||
const setMonoFont = useCallback((f: MonoFont) => {
|
const setMonoFont = useCallback((f: MonoFont) => {
|
||||||
setMonoFontState(f);
|
setMonoFontState(f);
|
||||||
applyAll(theme, accent, uiFont, f, uiFontSize, monoFontSize);
|
const next: ThemeSettings = { theme, accent, uiFont, monoFont: f, uiFontSize, monoFontSize };
|
||||||
persist(theme, accent, uiFont, f, uiFontSize, monoFontSize);
|
applyAll(next);
|
||||||
|
persist(next);
|
||||||
}, [accent, theme, applyAll, persist, uiFont, uiFontSize, monoFontSize]);
|
}, [accent, theme, applyAll, persist, uiFont, uiFontSize, monoFontSize]);
|
||||||
|
|
||||||
const setUiFontSize = useCallback((size: number) => {
|
const setUiFontSize = useCallback((size: number) => {
|
||||||
const clamped = Math.min(20, Math.max(12, size));
|
const clamped = Math.min(20, Math.max(12, size));
|
||||||
setUiFontSizeState(clamped);
|
setUiFontSizeState(clamped);
|
||||||
applyAll(theme, accent, uiFont, monoFont, clamped, monoFontSize);
|
const next: ThemeSettings = { theme, accent, uiFont, monoFont, uiFontSize: clamped, monoFontSize };
|
||||||
persist(theme, accent, uiFont, monoFont, clamped, monoFontSize);
|
applyAll(next);
|
||||||
|
persist(next);
|
||||||
}, [accent, theme, applyAll, persist, uiFont, monoFont, monoFontSize]);
|
}, [accent, theme, applyAll, persist, uiFont, monoFont, monoFontSize]);
|
||||||
|
|
||||||
const setMonoFontSize = useCallback((size: number) => {
|
const setMonoFontSize = useCallback((size: number) => {
|
||||||
const clamped = Math.min(20, Math.max(12, size));
|
const clamped = Math.min(20, Math.max(12, size));
|
||||||
setMonoFontSizeState(clamped);
|
setMonoFontSizeState(clamped);
|
||||||
applyAll(theme, accent, uiFont, monoFont, uiFontSize, clamped);
|
const next: ThemeSettings = { theme, accent, uiFont, monoFont, uiFontSize, monoFontSize: clamped };
|
||||||
persist(theme, accent, uiFont, monoFont, uiFontSize, clamped);
|
applyAll(next);
|
||||||
|
persist(next);
|
||||||
}, [accent, theme, applyAll, persist, uiFont, monoFont, uiFontSize]);
|
}, [accent, theme, applyAll, persist, uiFont, monoFont, uiFontSize]);
|
||||||
|
|
||||||
// Apply on mount
|
// Apply on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
applyAll(theme, accent, uiFont, monoFont, uiFontSize, monoFontSize);
|
applyAll({ theme, accent, uiFont, monoFont, uiFontSize, monoFontSize });
|
||||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
// Listen to OS color scheme changes when theme is 'system'
|
// Listen to OS color scheme changes when theme is 'system'
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (theme !== 'system') return;
|
if (theme !== 'system') return;
|
||||||
const mq = window.matchMedia('(prefers-color-scheme: light)');
|
const mq = window.matchMedia('(prefers-color-scheme: light)');
|
||||||
const handler = () => applyAll(mq.matches ? 'light' : 'dark', accent, uiFont, monoFont, uiFontSize, monoFontSize);
|
const handler = () => applyAll({ theme: mq.matches ? 'light' : 'dark', accent, uiFont, monoFont, uiFontSize, monoFontSize });
|
||||||
mq.addEventListener('change', handler);
|
mq.addEventListener('change', handler);
|
||||||
return () => mq.removeEventListener('change', handler);
|
return () => mq.removeEventListener('change', handler);
|
||||||
}, [theme, accent, applyAll, uiFont, monoFont, uiFontSize, monoFontSize]);
|
}, [theme, accent, applyAll, uiFont, monoFont, uiFontSize, monoFontSize]);
|
||||||
|
|||||||
@@ -5,6 +5,20 @@ export type AccentColor = 'cyan' | 'violet' | 'emerald' | 'amber' | 'rose' | 'bl
|
|||||||
export type UiFont = 'system' | 'inter' | 'segoe' | 'sf';
|
export type UiFont = 'system' | 'inter' | 'segoe' | 'sf';
|
||||||
export type MonoFont = 'jetbrains' | 'fira' | 'cascadia' | 'system-mono';
|
export type MonoFont = 'jetbrains' | 'fira' | 'cascadia' | 'system-mono';
|
||||||
|
|
||||||
|
export const uiFontStacks: Record<UiFont, string> = {
|
||||||
|
system: "-apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif",
|
||||||
|
inter: "'Inter', 'Segoe UI', system-ui, sans-serif",
|
||||||
|
segoe: "'Segoe UI Variable Text', 'Segoe UI', system-ui, sans-serif",
|
||||||
|
sf: "'SF Pro Text', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const monoFontStacks: Record<MonoFont, string> = {
|
||||||
|
jetbrains: "'JetBrains Mono', 'Cascadia Code', 'Fira Code', ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
|
||||||
|
fira: "'Fira Code', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
|
||||||
|
cascadia: "'Cascadia Code', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
|
||||||
|
'system-mono': "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
|
||||||
|
};
|
||||||
|
|
||||||
export interface ThemeContextValue {
|
export interface ThemeContextValue {
|
||||||
theme: ThemeName;
|
theme: ThemeName;
|
||||||
accent: AccentColor;
|
accent: AccentColor;
|
||||||
|
|||||||
55
src/contexts/__tests__/ThemeContext.test.ts
Normal file
55
src/contexts/__tests__/ThemeContext.test.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { describe, it, expect, beforeEach } from 'vitest';
|
||||||
|
import { loadStored, STORAGE_KEY } from '../themeStorage';
|
||||||
|
|
||||||
|
const STORAGE_KEY = 'pinchchat-theme';
|
||||||
|
|
||||||
|
describe('ThemeContext loadStoredForTest', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
localStorage.clear();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns defaults when storage is empty', () => {
|
||||||
|
expect(loadStored()).toEqual({
|
||||||
|
theme: 'dark',
|
||||||
|
accent: 'cyan',
|
||||||
|
uiFont: 'system',
|
||||||
|
monoFont: 'jetbrains',
|
||||||
|
uiFontSize: 15,
|
||||||
|
monoFontSize: 14,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reads valid stored values and clamps sizes', () => {
|
||||||
|
localStorage.setItem(STORAGE_KEY, JSON.stringify({
|
||||||
|
theme: 'light',
|
||||||
|
accent: 'rose',
|
||||||
|
uiFont: 'inter',
|
||||||
|
monoFont: 'fira',
|
||||||
|
uiFontSize: 25, // should clamp to 20
|
||||||
|
monoFontSize: 10, // should clamp to 12
|
||||||
|
}));
|
||||||
|
|
||||||
|
expect(loadStored()).toEqual({
|
||||||
|
theme: 'light',
|
||||||
|
accent: 'rose',
|
||||||
|
uiFont: 'inter',
|
||||||
|
monoFont: 'fira',
|
||||||
|
uiFontSize: 20,
|
||||||
|
monoFontSize: 12,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back to defaults on invalid font keys', () => {
|
||||||
|
localStorage.setItem(STORAGE_KEY, JSON.stringify({
|
||||||
|
theme: 'dark',
|
||||||
|
accent: 'cyan',
|
||||||
|
uiFont: 'nonexistent',
|
||||||
|
monoFont: 'nope',
|
||||||
|
uiFontSize: 16,
|
||||||
|
monoFontSize: 16,
|
||||||
|
}));
|
||||||
|
|
||||||
|
expect(loadStored().uiFont).toBe('system');
|
||||||
|
expect(loadStored().monoFont).toBe('jetbrains');
|
||||||
|
});
|
||||||
|
});
|
||||||
44
src/contexts/themeStorage.ts
Normal file
44
src/contexts/themeStorage.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import type { ThemeName, AccentColor, UiFont, MonoFont } from './ThemeContextDef';
|
||||||
|
import { uiFontStacks, monoFontStacks } from './ThemeContextDef';
|
||||||
|
|
||||||
|
export const STORAGE_KEY = 'pinchchat-theme';
|
||||||
|
|
||||||
|
export interface StoredTheme {
|
||||||
|
theme: ThemeName;
|
||||||
|
accent: AccentColor;
|
||||||
|
uiFont: UiFont;
|
||||||
|
monoFont: MonoFont;
|
||||||
|
uiFontSize: number;
|
||||||
|
monoFontSize: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULTS: StoredTheme = {
|
||||||
|
theme: 'dark',
|
||||||
|
accent: 'cyan',
|
||||||
|
uiFont: 'system',
|
||||||
|
monoFont: 'jetbrains',
|
||||||
|
uiFontSize: 15,
|
||||||
|
monoFontSize: 14,
|
||||||
|
};
|
||||||
|
|
||||||
|
const validThemes: ThemeName[] = ['dark', 'light', 'oled', 'system'];
|
||||||
|
const validAccents: AccentColor[] = ['cyan', 'violet', 'emerald', 'amber', 'rose', 'blue'];
|
||||||
|
|
||||||
|
export function loadStored(): StoredTheme {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(STORAGE_KEY);
|
||||||
|
if (raw) {
|
||||||
|
const parsed = JSON.parse(raw);
|
||||||
|
const themeValid = validThemes.includes(parsed.theme);
|
||||||
|
const accentValid = validAccents.includes(parsed.accent);
|
||||||
|
const uiFont: UiFont = uiFontStacks[parsed.uiFont as UiFont] ? parsed.uiFont : DEFAULTS.uiFont;
|
||||||
|
const monoFont: MonoFont = monoFontStacks[parsed.monoFont as MonoFont] ? parsed.monoFont : DEFAULTS.monoFont;
|
||||||
|
const uiFontSize = Number.isFinite(parsed.uiFontSize) ? Math.min(20, Math.max(12, Number(parsed.uiFontSize))) : DEFAULTS.uiFontSize;
|
||||||
|
const monoFontSize = Number.isFinite(parsed.monoFontSize) ? Math.min(20, Math.max(12, Number(parsed.monoFontSize))) : DEFAULTS.monoFontSize;
|
||||||
|
if (themeValid && accentValid) {
|
||||||
|
return { theme: parsed.theme, accent: parsed.accent, uiFont, monoFont, uiFontSize, monoFontSize };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch { /* ignore malformed storage */ }
|
||||||
|
return DEFAULTS;
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;600&family=Fira+Code:wght@400;600&display=swap');
|
||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
@import "highlight.js/styles/base16/material-palenight.min.css";
|
@import "highlight.js/styles/base16/material-palenight.min.css";
|
||||||
|
|
||||||
|
|||||||
@@ -183,6 +183,8 @@ const en = {
|
|||||||
'settings.fontSize': 'UI font size',
|
'settings.fontSize': 'UI font size',
|
||||||
'settings.fontMonoSize': 'Code font size',
|
'settings.fontMonoSize': 'Code font size',
|
||||||
'settings.preview': 'Preview',
|
'settings.preview': 'Preview',
|
||||||
|
'settings.previewText': 'The quick brown fox jumps over the lazy dog — 1234567890',
|
||||||
|
'settings.fontNote': 'Inter / JetBrains / Fira load automatically; Cascadia and SF use your system if installed.',
|
||||||
|
|
||||||
// Bookmarks
|
// Bookmarks
|
||||||
'message.bookmark': 'Bookmark message',
|
'message.bookmark': 'Bookmark message',
|
||||||
@@ -364,6 +366,8 @@ const fr: Record<keyof typeof en, string> = {
|
|||||||
'settings.fontSize': 'Taille police UI',
|
'settings.fontSize': 'Taille police UI',
|
||||||
'settings.fontMonoSize': 'Taille police code',
|
'settings.fontMonoSize': 'Taille police code',
|
||||||
'settings.preview': 'Aperçu',
|
'settings.preview': 'Aperçu',
|
||||||
|
'settings.previewText': 'Le vif renard brun saute par-dessus le chien paresseux — 1234567890',
|
||||||
|
'settings.fontNote': 'Inter / JetBrains / Fira se chargent automatiquement ; Cascadia et SF dépendent des polices installées.',
|
||||||
|
|
||||||
'message.bookmark': 'Marquer le message',
|
'message.bookmark': 'Marquer le message',
|
||||||
'message.removeBookmark': 'Retirer le marque-page',
|
'message.removeBookmark': 'Retirer le marque-page',
|
||||||
@@ -544,6 +548,8 @@ const es: Record<keyof typeof en, string> = {
|
|||||||
'settings.fontSize': 'Tamaño fuente UI',
|
'settings.fontSize': 'Tamaño fuente UI',
|
||||||
'settings.fontMonoSize': 'Tamaño fuente código',
|
'settings.fontMonoSize': 'Tamaño fuente código',
|
||||||
'settings.preview': 'Vista previa',
|
'settings.preview': 'Vista previa',
|
||||||
|
'settings.previewText': 'El veloz zorro marrón salta sobre el perro perezoso — 1234567890',
|
||||||
|
'settings.fontNote': 'Inter / JetBrains / Fira se cargan automáticamente; Cascadia y SF dependen de que estén instaladas.',
|
||||||
|
|
||||||
'message.bookmark': 'Marcar mensaje',
|
'message.bookmark': 'Marcar mensaje',
|
||||||
'message.removeBookmark': 'Quitar marcador',
|
'message.removeBookmark': 'Quitar marcador',
|
||||||
@@ -726,6 +732,8 @@ const de: Record<keyof typeof en, string> = {
|
|||||||
'settings.fontSize': 'UI-Schriftgröße',
|
'settings.fontSize': 'UI-Schriftgröße',
|
||||||
'settings.fontMonoSize': 'Code-Schriftgröße',
|
'settings.fontMonoSize': 'Code-Schriftgröße',
|
||||||
'settings.preview': 'Vorschau',
|
'settings.preview': 'Vorschau',
|
||||||
|
'settings.previewText': 'Falsches Üben von Xylophonmusik quält jeden größeren Zwerg — 1234567890',
|
||||||
|
'settings.fontNote': 'Inter / JetBrains / Fira werden automatisch geladen; Cascadia und SF erfordern Systeminstallation.',
|
||||||
|
|
||||||
'message.bookmark': 'Nachricht markieren',
|
'message.bookmark': 'Nachricht markieren',
|
||||||
'message.reply': 'Antworten',
|
'message.reply': 'Antworten',
|
||||||
@@ -906,6 +914,8 @@ const ja: Record<keyof typeof en, string> = {
|
|||||||
'settings.fontSize': 'UIフォントサイズ',
|
'settings.fontSize': 'UIフォントサイズ',
|
||||||
'settings.fontMonoSize': 'コードフォントサイズ',
|
'settings.fontMonoSize': 'コードフォントサイズ',
|
||||||
'settings.preview': 'プレビュー',
|
'settings.preview': 'プレビュー',
|
||||||
|
'settings.previewText': '素早い茶色の狐はのんびり犬を飛び越える — 1234567890',
|
||||||
|
'settings.fontNote': 'Inter / JetBrains / Fira は自動読み込み。Cascadia と SF はシステムにある場合のみ使用。',
|
||||||
|
|
||||||
'message.bookmark': 'メッセージをブックマーク',
|
'message.bookmark': 'メッセージをブックマーク',
|
||||||
'message.reply': '返信',
|
'message.reply': '返信',
|
||||||
@@ -1086,6 +1096,8 @@ const pt: Record<keyof typeof en, string> = {
|
|||||||
'settings.fontSize': 'Tamanho da fonte UI',
|
'settings.fontSize': 'Tamanho da fonte UI',
|
||||||
'settings.fontMonoSize': 'Tamanho da fonte de código',
|
'settings.fontMonoSize': 'Tamanho da fonte de código',
|
||||||
'settings.preview': 'Pré-visualização',
|
'settings.preview': 'Pré-visualização',
|
||||||
|
'settings.previewText': 'A rápida raposa marrom pula sobre o cão preguiçoso — 1234567890',
|
||||||
|
'settings.fontNote': 'Inter / JetBrains / Fira carregam automaticamente; Cascadia e SF dependem de estarem instaladas.',
|
||||||
|
|
||||||
'message.bookmark': 'Marcar mensagem',
|
'message.bookmark': 'Marcar mensagem',
|
||||||
'message.reply': 'Responder',
|
'message.reply': 'Responder',
|
||||||
@@ -1266,6 +1278,8 @@ const zh: Record<keyof typeof en, string> = {
|
|||||||
'settings.fontSize': '界面字号',
|
'settings.fontSize': '界面字号',
|
||||||
'settings.fontMonoSize': '代码字号',
|
'settings.fontMonoSize': '代码字号',
|
||||||
'settings.preview': '预览',
|
'settings.preview': '预览',
|
||||||
|
'settings.previewText': '敏捷的棕狐跳过懒狗 — 1234567890',
|
||||||
|
'settings.fontNote': 'Inter / JetBrains / Fira 自动加载;Cascadia 和 SF 依赖系统已安装字体。',
|
||||||
|
|
||||||
'message.bookmark': '收藏消息',
|
'message.bookmark': '收藏消息',
|
||||||
'message.reply': '回复',
|
'message.reply': '回复',
|
||||||
@@ -1446,6 +1460,8 @@ const it: Record<keyof typeof en, string> = {
|
|||||||
'settings.fontSize': 'Dimensione font UI',
|
'settings.fontSize': 'Dimensione font UI',
|
||||||
'settings.fontMonoSize': 'Dimensione font codice',
|
'settings.fontMonoSize': 'Dimensione font codice',
|
||||||
'settings.preview': 'Anteprima',
|
'settings.preview': 'Anteprima',
|
||||||
|
'settings.previewText': 'Quel vitello jazz fonde sciolto whiskey e cioccolato — 1234567890',
|
||||||
|
'settings.fontNote': 'Inter / JetBrains / Fira si caricano automaticamente; Cascadia e SF richiedono font di sistema.',
|
||||||
|
|
||||||
'message.bookmark': 'Aggiungi ai segnalibri',
|
'message.bookmark': 'Aggiungi ai segnalibri',
|
||||||
'message.reply': 'Rispondi',
|
'message.reply': 'Rispondi',
|
||||||
|
|||||||
Reference in New Issue
Block a user