mirror of
https://github.com/nimbold/Firelink.git
synced 2026-08-09 18:59:36 +00:00
feat(properties): harden standalone properties lifecycle
- synchronize child appearance and hidden-window readiness - serialize native Torrent mutations transactionally without double-encoded rows - preserve native lifecycle markers and fence stale or duplicate actions - remove the obsolete modal surface and ignore implementation_plan.md
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { applyDocumentAppearance } from './documentAppearance';
|
||||
|
||||
const fakeDocument = () => {
|
||||
const classes = new Set<string>(['theme-light', 'dark']);
|
||||
const root = {
|
||||
classList: {
|
||||
add: (...values: string[]) => values.forEach(value => classes.add(value)),
|
||||
remove: (...values: string[]) => values.forEach(value => classes.delete(value)),
|
||||
},
|
||||
dataset: {} as Record<string, string>,
|
||||
style: {} as Record<string, string>,
|
||||
lang: '',
|
||||
dir: '',
|
||||
};
|
||||
return {
|
||||
classes,
|
||||
root,
|
||||
document: { documentElement: root } as unknown as Document,
|
||||
};
|
||||
};
|
||||
|
||||
describe('document appearance synchronization', () => {
|
||||
it('applies a complete dark RTL projection without retaining stale theme classes', () => {
|
||||
const target = fakeDocument();
|
||||
applyDocumentAppearance(target.document, {
|
||||
theme: 'nord',
|
||||
fontFamily: 'vazirmatn',
|
||||
appFontSize: 'large',
|
||||
listRowDensity: 'compact',
|
||||
locale: 'fa',
|
||||
}, false);
|
||||
|
||||
expect([...target.classes].sort()).toEqual(['dark', 'theme-nord']);
|
||||
expect(target.root.dataset).toEqual({
|
||||
resolvedTheme: 'dark',
|
||||
fontFamily: 'vazirmatn',
|
||||
fontSize: 'large',
|
||||
listDensity: 'compact',
|
||||
});
|
||||
expect(target.root.style.colorScheme).toBe('dark');
|
||||
expect(target.root.lang).toBe('fa');
|
||||
expect(target.root.dir).toBe('rtl');
|
||||
});
|
||||
|
||||
it('resolves system appearance while preserving an LTR locale', () => {
|
||||
const target = fakeDocument();
|
||||
applyDocumentAppearance(target.document, {
|
||||
theme: 'system',
|
||||
fontFamily: 'system',
|
||||
appFontSize: 'standard',
|
||||
listRowDensity: 'standard',
|
||||
locale: 'en',
|
||||
}, false);
|
||||
|
||||
expect([...target.classes]).toEqual(['theme-light']);
|
||||
expect(target.root.dataset.resolvedTheme).toBe('light');
|
||||
expect(target.root.style.colorScheme).toBe('light');
|
||||
expect(target.root.lang).toBe('en');
|
||||
expect(target.root.dir).toBe('ltr');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,55 @@
|
||||
import type { AppFontSize } from '../bindings/AppFontSize';
|
||||
import type { FontFamily } from '../bindings/FontFamily';
|
||||
import type { ListRowDensity } from '../bindings/ListRowDensity';
|
||||
import type { Theme } from '../bindings/Theme';
|
||||
import { localeDirection, resolveAppLocale, type AppLocale } from '../i18n/locales';
|
||||
|
||||
export type DocumentAppearance = {
|
||||
theme: Theme;
|
||||
fontFamily: FontFamily;
|
||||
appFontSize: AppFontSize;
|
||||
listRowDensity: ListRowDensity;
|
||||
locale: AppLocale;
|
||||
};
|
||||
|
||||
const DARK_THEMES: ReadonlySet<Theme> = new Set(['dark', 'dracula', 'nord']);
|
||||
const THEME_CLASSES = ['theme-dark', 'theme-light', 'theme-dracula', 'theme-nord', 'dark'] as const;
|
||||
|
||||
export const applyDocumentAppearance = (
|
||||
document: Document,
|
||||
appearance: DocumentAppearance,
|
||||
systemDark: boolean,
|
||||
): void => {
|
||||
const root = document.documentElement;
|
||||
const resolvedTheme = appearance.theme === 'system'
|
||||
? (systemDark ? 'dark' : 'light')
|
||||
: (DARK_THEMES.has(appearance.theme) ? 'dark' : 'light');
|
||||
const themeClass = appearance.theme === 'system'
|
||||
? `theme-${resolvedTheme}`
|
||||
: `theme-${appearance.theme}`;
|
||||
|
||||
root.classList.remove(...THEME_CLASSES);
|
||||
root.classList.add(themeClass);
|
||||
if (resolvedTheme === 'dark') root.classList.add('dark');
|
||||
root.dataset.resolvedTheme = resolvedTheme;
|
||||
root.style.colorScheme = resolvedTheme;
|
||||
root.dataset.fontFamily = appearance.fontFamily;
|
||||
root.dataset.fontSize = appearance.appFontSize;
|
||||
root.dataset.listDensity = appearance.listRowDensity;
|
||||
|
||||
const locale = resolveAppLocale(appearance.locale);
|
||||
root.lang = locale;
|
||||
root.dir = localeDirection(locale);
|
||||
};
|
||||
|
||||
export const synchronizeDocumentAppearance = (
|
||||
window: Window,
|
||||
appearance: DocumentAppearance,
|
||||
): (() => void) => {
|
||||
const media = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
const apply = () => applyDocumentAppearance(window.document, appearance, media.matches);
|
||||
apply();
|
||||
if (appearance.theme !== 'system') return () => undefined;
|
||||
media.addEventListener('change', apply);
|
||||
return () => media.removeEventListener('change', apply);
|
||||
};
|
||||
Reference in New Issue
Block a user