feat(settings): preview theme and UI fonts on hover
This commit is contained in:
8 files changed
+194
-17
No files matched your search
@@ -118,6 +118,7 @@ import { shouldBlockAppNativeSelectAll } from "@/lib/common/clipboard";
|
|||||||
import { APP_FONT_SANS_CSS_VAR, DATA_GRID_FONT_FAMILY_CSS_VAR, DEFAULT_DATA_GRID_FONT_FAMILY, DEFAULT_UI_FONT_FAMILY } from "@/lib/app/appFonts";
|
import { APP_FONT_SANS_CSS_VAR, DATA_GRID_FONT_FAMILY_CSS_VAR, DEFAULT_DATA_GRID_FONT_FAMILY, DEFAULT_UI_FONT_FAMILY } from "@/lib/app/appFonts";
|
||||||
import { DATA_GRID_TYPE_COLOR_KEYS, dataGridTypeColorCssVar, resolveActiveDataGridTypeColors } from "@/lib/dataGrid/dataGridTypeColorScheme";
|
import { DATA_GRID_TYPE_COLOR_KEYS, dataGridTypeColorCssVar, resolveActiveDataGridTypeColors } from "@/lib/dataGrid/dataGridTypeColorScheme";
|
||||||
import { rankSavedSqlHistory } from "@/lib/savedSql/savedSqlHistory";
|
import { rankSavedSqlHistory } from "@/lib/savedSql/savedSqlHistory";
|
||||||
|
import { useUiFontFamilyPreview } from "@/composables/useUiFontFamilyPreview";
|
||||||
import { savedSqlErrorMessage } from "@/lib/savedSql/savedSqlErrors";
|
import { savedSqlErrorMessage } from "@/lib/savedSql/savedSqlErrors";
|
||||||
import { savedSqlDefaultTargetForWrite } from "@/lib/savedSql/savedSqlExecutionTarget";
|
import { savedSqlDefaultTargetForWrite } from "@/lib/savedSql/savedSqlExecutionTarget";
|
||||||
import { countActiveUpdateBlockingTasks } from "@/lib/app/appUpdateTaskGuard";
|
import { countActiveUpdateBlockingTasks } from "@/lib/app/appUpdateTaskGuard";
|
||||||
@@ -171,6 +172,7 @@ const { t } = useI18n();
|
|||||||
const connectionStore = useConnectionStore();
|
const connectionStore = useConnectionStore();
|
||||||
const queryStore = useQueryStore();
|
const queryStore = useQueryStore();
|
||||||
const settingsStore = useSettingsStore();
|
const settingsStore = useSettingsStore();
|
||||||
|
const { uiFontFamilyPreview } = useUiFontFamilyPreview();
|
||||||
const savedSqlStore = useSavedSqlStore();
|
const savedSqlStore = useSavedSqlStore();
|
||||||
const promptTemplateStore = usePromptTemplateStore();
|
const promptTemplateStore = usePromptTemplateStore();
|
||||||
const recentConnectionIds = ref<readonly string[]>(parseRecentConnectionIds(safeLocalStorageGet(RECENT_CONNECTION_IDS_STORAGE_KEY)));
|
const recentConnectionIds = ref<readonly string[]>(parseRecentConnectionIds(safeLocalStorageGet(RECENT_CONNECTION_IDS_STORAGE_KEY)));
|
||||||
@@ -1128,7 +1130,7 @@ function applyDataGridTypeColors() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const appUiFontFamilyStyle = computed<Record<string, string>>(() => {
|
const appUiFontFamilyStyle = computed<Record<string, string>>(() => {
|
||||||
const fontFamily = settingsStore.editorSettings.uiFontFamily || DEFAULT_UI_FONT_FAMILY;
|
const fontFamily = uiFontFamilyPreview.value || settingsStore.editorSettings.uiFontFamily || DEFAULT_UI_FONT_FAMILY;
|
||||||
return {
|
return {
|
||||||
[APP_FONT_SANS_CSS_VAR]: fontFamily,
|
[APP_FONT_SANS_CSS_VAR]: fontFamily,
|
||||||
fontFamily: `var(${APP_FONT_SANS_CSS_VAR}, ${DEFAULT_UI_FONT_FAMILY})`,
|
fontFamily: `var(${APP_FONT_SANS_CSS_VAR}, ${DEFAULT_UI_FONT_FAMILY})`,
|
||||||
@@ -1189,8 +1191,12 @@ watch(
|
|||||||
);
|
);
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => settingsStore.editorSettings.uiFontFamily,
|
[() => settingsStore.editorSettings.uiFontFamily, uiFontFamilyPreview],
|
||||||
(fontFamily) => {
|
([fontFamily, preview]) => {
|
||||||
|
if (preview) {
|
||||||
|
applyUiFontFamily(preview);
|
||||||
|
return;
|
||||||
|
}
|
||||||
applyUiFontFamily(fontFamily);
|
applyUiFontFamily(fontFamily);
|
||||||
},
|
},
|
||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
|
|||||||
@@ -218,6 +218,7 @@ import { apiUrl, webPath } from "@/lib/common/webPath";
|
|||||||
import { DEFAULT_DATA_GRID_FONT_FAMILY, DEFAULT_UI_FONT_FAMILY, normalizeCustomFontFamilyInput, readableFontFamily, SYSTEM_UI_FONT_FAMILY } from "@/lib/app/appFonts";
|
import { DEFAULT_DATA_GRID_FONT_FAMILY, DEFAULT_UI_FONT_FAMILY, normalizeCustomFontFamilyInput, readableFontFamily, SYSTEM_UI_FONT_FAMILY } from "@/lib/app/appFonts";
|
||||||
import { buildFontFamilyOptions, displayFontFamily, isPresetFontFamily, loadSystemFontNames } from "@/lib/app/fontFamilyOptions";
|
import { buildFontFamilyOptions, displayFontFamily, isPresetFontFamily, loadSystemFontNames } from "@/lib/app/fontFamilyOptions";
|
||||||
import { buildAppSupportInfoRows, formatAppSupportInfoForClipboard, type AppSupportInfoLabels } from "@/lib/app/supportInfo";
|
import { buildAppSupportInfoRows, formatAppSupportInfoForClipboard, type AppSupportInfoLabels } from "@/lib/app/supportInfo";
|
||||||
|
import { useUiFontFamilyPreview } from "@/composables/useUiFontFamilyPreview";
|
||||||
import { DateTimePatterns, normalizeSupportedDateTimePattern } from "@/lib/dataGrid/columnFormatter";
|
import { DateTimePatterns, normalizeSupportedDateTimePattern } from "@/lib/dataGrid/columnFormatter";
|
||||||
import { MAX_RESULT_PAGE_SIZE, MIN_RESULT_PAGE_SIZE } from "@/lib/dataGrid/paginationPageSize";
|
import { MAX_RESULT_PAGE_SIZE, MIN_RESULT_PAGE_SIZE } from "@/lib/dataGrid/paginationPageSize";
|
||||||
import { MAX_QUERY_RESULT_MAX_ROWS } from "@/lib/dataGrid/queryResultRowLimit";
|
import { MAX_QUERY_RESULT_MAX_ROWS } from "@/lib/dataGrid/queryResultRowLimit";
|
||||||
@@ -234,12 +235,37 @@ const connectionStore = useConnectionStore();
|
|||||||
const savedSqlStore = useSavedSqlStore();
|
const savedSqlStore = useSavedSqlStore();
|
||||||
const promptTemplateStore = usePromptTemplateStore();
|
const promptTemplateStore = usePromptTemplateStore();
|
||||||
const tunnelProfileStore = useTunnelProfileStore();
|
const tunnelProfileStore = useTunnelProfileStore();
|
||||||
const { isDark, themeMode, themePalette, activeCustomUiColors, cornerStyle, setThemeMode, setThemePalette, setCustomUiColors, resetCustomUiColors, setCornerStyle } = useTheme();
|
const { isDark, themeMode, themePalette, activeCustomUiColors, cornerStyle, setThemeMode, setThemePalette, previewThemePalette, clearThemePalettePreview, setCustomUiColors, resetCustomUiColors, setCornerStyle } = useTheme();
|
||||||
|
const { previewUiFontFamily, clearUiFontFamilyPreview } = useUiFontFamilyPreview();
|
||||||
|
|
||||||
function updateCustomUiColor(key: keyof AppCustomUiColors, value: string) {
|
function updateCustomUiColor(key: keyof AppCustomUiColors, value: string) {
|
||||||
setCustomUiColors({ ...activeCustomUiColors.value, [key]: value });
|
setCustomUiColors({ ...activeCustomUiColors.value, [key]: value });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onThemePaletteSelect(value: unknown) {
|
||||||
|
if (typeof value === "string") setThemePalette(value as AppThemePalette);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onThemePaletteOpenChange(open: boolean) {
|
||||||
|
if (!open) clearThemePalettePreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
function previewUiFontOption(value: string | undefined) {
|
||||||
|
if (value) previewUiFontFamily(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreUiFontFamilyPreview() {
|
||||||
|
previewUiFontFamily(editUiFontFamily.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onUiFontFamilyOpenChange(open: boolean) {
|
||||||
|
if (open) {
|
||||||
|
void loadSystemFontOptions();
|
||||||
|
} else {
|
||||||
|
restoreUiFontFamilyPreview();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const appThemePaletteOptions = computed(
|
const appThemePaletteOptions = computed(
|
||||||
(): Array<{ value: AppThemePalette; label: string; previewColor: string }> =>
|
(): Array<{ value: AppThemePalette; label: string; previewColor: string }> =>
|
||||||
APP_THEME_PALETTES.map((palette) => ({
|
APP_THEME_PALETTES.map((palette) => ({
|
||||||
@@ -1496,6 +1522,20 @@ watch(
|
|||||||
editMetadataCacheMaxMemoryMb.value = settingsStore.desktopSettings.metadata_cache_max_memory_mb;
|
editMetadataCacheMaxMemoryMb.value = settingsStore.desktopSettings.metadata_cache_max_memory_mb;
|
||||||
editDuckDbWorkerProcessIsolation.value = settingsStore.desktopSettings.duckdb_worker_process_isolation;
|
editDuckDbWorkerProcessIsolation.value = settingsStore.desktopSettings.duckdb_worker_process_isolation;
|
||||||
editSidebarTablePageSize.value = settingsStore.desktopSettings.sidebar_table_page_size ?? DEFAULT_SIDEBAR_TABLE_PAGE_SIZE;
|
editSidebarTablePageSize.value = settingsStore.desktopSettings.sidebar_table_page_size ?? DEFAULT_SIDEBAR_TABLE_PAGE_SIZE;
|
||||||
|
} else {
|
||||||
|
clearThemePalettePreview();
|
||||||
|
clearUiFontFamilyPreview();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => settingsStore.settingsPageActive,
|
||||||
|
(active) => {
|
||||||
|
if (isSettingsPage.value && !active) {
|
||||||
|
clearThemePalettePreview();
|
||||||
|
clearUiFontFamilyPreview();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
@@ -2044,7 +2084,10 @@ function onTableFontFamilyChange(v: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onUiFontFamilyChange(v: any) {
|
function onUiFontFamilyChange(v: any) {
|
||||||
if (typeof v === "string") editUiFontFamily.value = v;
|
if (typeof v === "string") {
|
||||||
|
editUiFontFamily.value = v;
|
||||||
|
previewUiFontFamily(v);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const themeSelectValue = computed(() => {
|
const themeSelectValue = computed(() => {
|
||||||
@@ -3717,6 +3760,8 @@ onMounted(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
|
clearThemePalettePreview();
|
||||||
|
clearUiFontFamilyPreview();
|
||||||
cleanupTableColumnTemplatePointerDrag();
|
cleanupTableColumnTemplatePointerDrag();
|
||||||
cleanupTruncationObservers();
|
cleanupTruncationObservers();
|
||||||
});
|
});
|
||||||
@@ -5682,7 +5727,7 @@ onUnmounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<div class="min-w-0 flex-1">
|
<div class="min-w-0 flex-1">
|
||||||
<Select :model-value="themePalette" @update:model-value="(value) => setThemePalette(value as AppThemePalette)">
|
<Select :model-value="themePalette" @update:model-value="onThemePaletteSelect" @update:open="onThemePaletteOpenChange">
|
||||||
<SelectTrigger class="h-8 w-full gap-1">
|
<SelectTrigger class="h-8 w-full gap-1">
|
||||||
<SelectValue :placeholder="t('settings.selectColorTheme')">
|
<SelectValue :placeholder="t('settings.selectColorTheme')">
|
||||||
<span v-if="selectedThemePaletteOption" class="flex min-w-0 items-center gap-1">
|
<span v-if="selectedThemePaletteOption" class="flex min-w-0 items-center gap-1">
|
||||||
@@ -5696,8 +5741,8 @@ onUnmounted(() => {
|
|||||||
</span>
|
</span>
|
||||||
</SelectValue>
|
</SelectValue>
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent @pointerleave="clearThemePalettePreview">
|
||||||
<SelectItem v-for="option in appThemePaletteOptions" :key="option.value" :value="option.value">
|
<SelectItem v-for="option in appThemePaletteOptions" :key="option.value" :value="option.value" @pointerenter="previewThemePalette(option.value)" @focus="previewThemePalette(option.value)">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span class="h-3 w-3 rounded-full border border-border shadow-xs" :style="{ background: option.previewColor }" />
|
<span class="h-3 w-3 rounded-full border border-border shadow-xs" :style="{ background: option.previewColor }" />
|
||||||
{{ option.label }}
|
{{ option.label }}
|
||||||
@@ -5776,7 +5821,10 @@ onUnmounted(() => {
|
|||||||
:trigger-icon-class="appearanceFontSearchTriggerIconClass"
|
:trigger-icon-class="appearanceFontSearchTriggerIconClass"
|
||||||
content-class="w-[var(--reka-popover-trigger-width)] min-w-[260px]"
|
content-class="w-[var(--reka-popover-trigger-width)] min-w-[260px]"
|
||||||
@update:model-value="onUiFontFamilyChange"
|
@update:model-value="onUiFontFamilyChange"
|
||||||
@update:open="(open: boolean) => open && loadSystemFontOptions()"
|
@update:open="onUiFontFamilyOpenChange"
|
||||||
|
@option-hover="previewUiFontOption"
|
||||||
|
@option-highlight="previewUiFontOption"
|
||||||
|
@option-leave="restoreUiFontFamilyPreview"
|
||||||
>
|
>
|
||||||
<template #trigger-label="{ label, loading }">
|
<template #trigger-label="{ label, loading }">
|
||||||
<span class="truncate" :style="{ fontFamily: editUiFontFamily }">
|
<span class="truncate" :style="{ fontFamily: editUiFontFamily }">
|
||||||
|
|||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
const dialogSource = readFileSync(new URL("../EditorSettingsDialog.vue", import.meta.url), "utf8");
|
||||||
|
const appSource = readFileSync(new URL("../../../App.vue", import.meta.url), "utf8");
|
||||||
|
|
||||||
|
describe("EditorSettingsDialog preview cleanup", () => {
|
||||||
|
it("keeps the existing draft lifecycle independent from page activity", () => {
|
||||||
|
expect(dialogSource).toContain("const settingsVisible = computed(() => isSettingsPage.value || props.open === true);");
|
||||||
|
expect(dialogSource).toContain("syncEditorSettingsDraftFromStore();");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clears previews when a mounted settings page becomes inactive", () => {
|
||||||
|
expect(appSource).toContain('v-if="settingsPageTabOpen"');
|
||||||
|
expect(appSource).toContain('v-show="settingsStore.settingsPageActive"');
|
||||||
|
expect(dialogSource).toContain("() => settingsStore.settingsPageActive");
|
||||||
|
expect(dialogSource).toContain("if (isSettingsPage.value && !active)");
|
||||||
|
expect(dialogSource).toContain("clearThemePalettePreview();");
|
||||||
|
expect(dialogSource).toContain("clearUiFontFamilyPreview();");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -55,6 +55,9 @@ const props = withDefaults(
|
|||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
"update:modelValue": [value: string];
|
"update:modelValue": [value: string];
|
||||||
"update:open": [value: boolean];
|
"update:open": [value: boolean];
|
||||||
|
"option-hover": [value: string];
|
||||||
|
"option-highlight": [value: string | undefined];
|
||||||
|
"option-leave": [];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
defineSlots<{
|
defineSlots<{
|
||||||
@@ -145,6 +148,7 @@ watch(
|
|||||||
|
|
||||||
watch([highlightIndex, filteredOptions], () => {
|
watch([highlightIndex, filteredOptions], () => {
|
||||||
void scrollHighlightedOptionIntoView();
|
void scrollHighlightedOptionIntoView();
|
||||||
|
emit("option-highlight", filteredOptions.value[highlightIndex.value]);
|
||||||
});
|
});
|
||||||
|
|
||||||
const activeHelpContent = computed(() => (activeHelpOption.value ? props.optionTooltip(activeHelpOption.value) : undefined));
|
const activeHelpContent = computed(() => (activeHelpOption.value ? props.optionTooltip(activeHelpOption.value) : undefined));
|
||||||
@@ -155,6 +159,7 @@ function activateHelpForHighlightedOption() {
|
|||||||
|
|
||||||
function activateHelpForOption(option: string) {
|
function activateHelpForOption(option: string) {
|
||||||
activeHelpOption.value = props.optionTooltip(option) ? option : undefined;
|
activeHelpOption.value = props.optionTooltip(option) ? option : undefined;
|
||||||
|
emit("option-hover", option);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateHelpPanelOffset() {
|
async function updateHelpPanelOffset() {
|
||||||
@@ -251,7 +256,7 @@ function handleKeydown(event: KeyboardEvent) {
|
|||||||
<span v-if="!searchText" class="pointer-events-none absolute left-[25px] top-1/2 -translate-y-1/2 text-sm text-muted-foreground">{{ searchPlaceholder }}</span>
|
<span v-if="!searchText" class="pointer-events-none absolute left-[25px] top-1/2 -translate-y-1/2 text-sm text-muted-foreground">{{ searchPlaceholder }}</span>
|
||||||
<Input ref="searchInput" :model-value="searchText" class="h-6 border-0 pl-6 pr-2 text-sm caret-foreground shadow-none focus-visible:ring-0" @update:model-value="(value) => (searchText = String(value))" @keydown="handleKeydown" />
|
<Input ref="searchInput" :model-value="searchText" class="h-6 border-0 pl-6 pr-2 text-sm caret-foreground shadow-none focus-visible:ring-0" @update:model-value="(value) => (searchText = String(value))" @keydown="handleKeydown" />
|
||||||
</div>
|
</div>
|
||||||
<div ref="listContainer" class="dbx-searchable-select-list max-h-64 overflow-y-auto py-1" @scroll="updateHelpPanelOffset">
|
<div ref="listContainer" class="dbx-searchable-select-list max-h-64 overflow-y-auto py-1" @scroll="updateHelpPanelOffset" @pointerleave="emit('option-leave')">
|
||||||
<div v-if="loading" class="px-2 py-2 text-sm text-muted-foreground">
|
<div v-if="loading" class="px-2 py-2 text-sm text-muted-foreground">
|
||||||
{{ loadingText }}
|
{{ loadingText }}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -108,6 +108,33 @@ describe("useTheme on Linux", () => {
|
|||||||
expect(setTheme).toHaveBeenLastCalledWith("light");
|
expect(setTheme).toHaveBeenLastCalledWith("light");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("previews a palette without persisting it and restores the saved palette when cancelled", async () => {
|
||||||
|
const theme = await loadTheme("light");
|
||||||
|
theme.setThemePalette("pearl");
|
||||||
|
await flushDynamicImport();
|
||||||
|
setTheme.mockClear();
|
||||||
|
|
||||||
|
theme.previewThemePalette("cobalt");
|
||||||
|
|
||||||
|
expect(theme.themePalette.value).toBe("cobalt");
|
||||||
|
expect(document.documentElement.classList.contains("theme-cobalt")).toBe(true);
|
||||||
|
expect(window.localStorage.getItem("dbx-theme-palette")).toBe("pearl");
|
||||||
|
expect(setTheme).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
theme.clearThemePalettePreview();
|
||||||
|
|
||||||
|
expect(theme.themePalette.value).toBe("pearl");
|
||||||
|
expect(document.documentElement.classList.contains("theme-cobalt")).toBe(false);
|
||||||
|
expect(window.localStorage.getItem("dbx-theme-palette")).toBe("pearl");
|
||||||
|
|
||||||
|
theme.previewThemePalette("sage");
|
||||||
|
theme.setThemePalette("sage");
|
||||||
|
theme.clearThemePalettePreview();
|
||||||
|
|
||||||
|
expect(theme.themePalette.value).toBe("sage");
|
||||||
|
expect(window.localStorage.getItem("dbx-theme-palette")).toBe("sage");
|
||||||
|
});
|
||||||
|
|
||||||
it("injects custom UI colors as inline CSS variables and removes them when leaving the custom palette", async () => {
|
it("injects custom UI colors as inline CSS variables and removes them when leaving the custom palette", async () => {
|
||||||
const theme = await loadTheme();
|
const theme = await loadTheme();
|
||||||
theme.setThemePalette("custom");
|
theme.setThemePalette("custom");
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { beforeEach, describe, expect, it } from "vitest";
|
||||||
|
import { useUiFontFamilyPreview } from "@/composables/useUiFontFamilyPreview";
|
||||||
|
|
||||||
|
describe("useUiFontFamilyPreview", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
useUiFontFamilyPreview().clearUiFontFamilyPreview();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps a temporary font preview separate from the saved setting", () => {
|
||||||
|
const preview = useUiFontFamilyPreview();
|
||||||
|
|
||||||
|
preview.previewUiFontFamily("'Preview Sans', sans-serif");
|
||||||
|
expect(preview.uiFontFamilyPreview.value).toBe("'Preview Sans', sans-serif");
|
||||||
|
|
||||||
|
preview.clearUiFontFamilyPreview();
|
||||||
|
expect(preview.uiFontFamilyPreview.value).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ignores empty font values", () => {
|
||||||
|
const preview = useUiFontFamilyPreview();
|
||||||
|
|
||||||
|
preview.previewUiFontFamily(" ");
|
||||||
|
expect(preview.uiFontFamilyPreview.value).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -35,7 +35,11 @@ function isLinuxTauriRuntime() {
|
|||||||
const savedThemeMode = safeLocalStorageGet(APP_THEME_STORAGE_KEY);
|
const savedThemeMode = safeLocalStorageGet(APP_THEME_STORAGE_KEY);
|
||||||
const themeMode = ref<AppThemeMode>(normalizeAppThemeMode(savedThemeMode));
|
const themeMode = ref<AppThemeMode>(normalizeAppThemeMode(savedThemeMode));
|
||||||
const savedThemePalette = safeLocalStorageGet(APP_THEME_PALETTE_STORAGE_KEY);
|
const savedThemePalette = safeLocalStorageGet(APP_THEME_PALETTE_STORAGE_KEY);
|
||||||
const themePalette = ref<AppThemePalette>(normalizeAppThemePalette(savedThemePalette));
|
const savedThemePaletteValue = ref<AppThemePalette>(normalizeAppThemePalette(savedThemePalette));
|
||||||
|
const previewedThemePalette = ref<AppThemePalette | null>(null);
|
||||||
|
// Consumers use the effective palette so editor and canvas surfaces preview in
|
||||||
|
// lockstep with the document-level theme classes.
|
||||||
|
const themePalette = computed<AppThemePalette>(() => previewedThemePalette.value ?? savedThemePaletteValue.value);
|
||||||
function parseStoredCustomUiColors(raw: string | null): AppCustomUiColors | null {
|
function parseStoredCustomUiColors(raw: string | null): AppCustomUiColors | null {
|
||||||
if (!raw) return null;
|
if (!raw) return null;
|
||||||
try {
|
try {
|
||||||
@@ -75,6 +79,16 @@ function setupSystemThemeListener() {
|
|||||||
isListeningForSystemTheme = true;
|
isListeningForSystemTheme = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function applyThemePalette() {
|
||||||
|
if (typeof document === "undefined") return;
|
||||||
|
|
||||||
|
const doc = document.documentElement;
|
||||||
|
for (const className of APP_THEME_PALETTE_CLASS_NAMES) doc.classList.remove(className);
|
||||||
|
const paletteClass = getAppThemePaletteClass(themePalette.value);
|
||||||
|
if (paletteClass) doc.classList.add(paletteClass);
|
||||||
|
applyCustomUiColors();
|
||||||
|
}
|
||||||
|
|
||||||
function applyTheme() {
|
function applyTheme() {
|
||||||
if (typeof document === "undefined") return;
|
if (typeof document === "undefined") return;
|
||||||
|
|
||||||
@@ -83,12 +97,9 @@ function applyTheme() {
|
|||||||
|
|
||||||
doc.classList.add("disable-transitions");
|
doc.classList.add("disable-transitions");
|
||||||
doc.classList.toggle("dark", dark);
|
doc.classList.toggle("dark", dark);
|
||||||
for (const className of APP_THEME_PALETTE_CLASS_NAMES) doc.classList.remove(className);
|
applyThemePalette();
|
||||||
const paletteClass = getAppThemePaletteClass(themePalette.value);
|
|
||||||
if (paletteClass) doc.classList.add(paletteClass);
|
|
||||||
doc.dataset.cornerStyle = cornerStyle.value;
|
doc.dataset.cornerStyle = cornerStyle.value;
|
||||||
doc.style.colorScheme = dark ? "dark" : "light";
|
doc.style.colorScheme = dark ? "dark" : "light";
|
||||||
applyCustomUiColors();
|
|
||||||
|
|
||||||
// force reflow so the class toggle takes effect before re-enabling transitions
|
// force reflow so the class toggle takes effect before re-enabling transitions
|
||||||
doc.offsetHeight; // eslint-disable-line @typescript-eslint/no-unused-expressions
|
doc.offsetHeight; // eslint-disable-line @typescript-eslint/no-unused-expressions
|
||||||
@@ -123,11 +134,26 @@ function setThemeMode(mode: AppThemeMode) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setThemePalette(palette: AppThemePalette) {
|
function setThemePalette(palette: AppThemePalette) {
|
||||||
themePalette.value = palette;
|
savedThemePaletteValue.value = palette;
|
||||||
|
previewedThemePalette.value = null;
|
||||||
safeLocalStorageSet(APP_THEME_PALETTE_STORAGE_KEY, palette);
|
safeLocalStorageSet(APP_THEME_PALETTE_STORAGE_KEY, palette);
|
||||||
applyTheme();
|
applyTheme();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function previewThemePalette(palette: AppThemePalette) {
|
||||||
|
if (themePalette.value === palette) return;
|
||||||
|
previewedThemePalette.value = palette;
|
||||||
|
// Palette previews do not change the OS/window mode, so avoid the native
|
||||||
|
// Tauri write performed by applyTheme().
|
||||||
|
applyThemePalette();
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearThemePalettePreview() {
|
||||||
|
if (previewedThemePalette.value === null) return;
|
||||||
|
previewedThemePalette.value = null;
|
||||||
|
applyThemePalette();
|
||||||
|
}
|
||||||
|
|
||||||
function applyCustomUiColors() {
|
function applyCustomUiColors() {
|
||||||
if (typeof document === "undefined") return;
|
if (typeof document === "undefined") return;
|
||||||
const doc = document.documentElement;
|
const doc = document.documentElement;
|
||||||
@@ -180,5 +206,5 @@ export function useTheme() {
|
|||||||
setThemeMode(isDark.value ? "light" : "dark");
|
setThemeMode(isDark.value ? "light" : "dark");
|
||||||
}
|
}
|
||||||
|
|
||||||
return { isDark, themeMode, themePalette, customUiColors, customUiColorsDark, activeCustomUiColors, cornerStyle, applyTheme, setThemeMode, setThemePalette, setCustomUiColors, resetCustomUiColors, setCornerStyle, toggleTheme };
|
return { isDark, themeMode, themePalette, customUiColors, customUiColorsDark, activeCustomUiColors, cornerStyle, applyTheme, setThemeMode, setThemePalette, previewThemePalette, clearThemePalettePreview, setCustomUiColors, resetCustomUiColors, setCornerStyle, toggleTheme };
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { computed, ref } from "vue";
|
||||||
|
|
||||||
|
const previewedUiFontFamily = ref<string | null>(null);
|
||||||
|
|
||||||
|
export function useUiFontFamilyPreview() {
|
||||||
|
const uiFontFamilyPreview = computed(() => previewedUiFontFamily.value);
|
||||||
|
|
||||||
|
function previewUiFontFamily(fontFamily: string) {
|
||||||
|
const next = fontFamily.trim();
|
||||||
|
if (!next) return;
|
||||||
|
previewedUiFontFamily.value = next;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearUiFontFamilyPreview() {
|
||||||
|
previewedUiFontFamily.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { uiFontFamilyPreview, previewUiFontFamily, clearUiFontFamilyPreview };
|
||||||
|
}
|
||||||
Reference in new issue
Block a user