feat(grid): add table font shortcuts

This commit is contained in:
终焉之时 2026-07-22 22:20:42 +08:00 committed by GitHub
parent 43b810fb36
commit 5d1a6e63cd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 198 additions and 77 deletions

View File

@ -85,7 +85,7 @@ import { countAvailableAgentDriverUpdates, type AgentDriverUpdateBadgeState } fr
import type { DriverStoreFocus } from "@/lib/connection/agentDriverInstallHint";
import { safeLocalStorageGet, safeLocalStorageSet } from "@/lib/backend/safeStorage";
import { apiUrl, webPath } from "@/lib/common/webPath";
import { APP_FONT_SANS_CSS_VAR, 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 { rankSavedSqlHistory } from "@/lib/savedSql/savedSqlHistory";
import { countActiveUpdateBlockingTasks } from "@/lib/app/appUpdateTaskGuard";
import { initSavedSqlEditorPositions } from "@/lib/app/savedSqlEditorPosition";
@ -503,6 +503,11 @@ function applyUiFontFamily(fontFamily: string) {
document.body.style.fontFamily = `var(${APP_FONT_SANS_CSS_VAR}, ${DEFAULT_UI_FONT_FAMILY})`;
}
function applyDataGridFontFamily(fontFamily: string) {
if (typeof document === "undefined") return;
document.documentElement.style.setProperty(DATA_GRID_FONT_FAMILY_CSS_VAR, fontFamily || DEFAULT_DATA_GRID_FONT_FAMILY);
}
const appUiFontFamilyStyle = computed<Record<string, string>>(() => {
const fontFamily = settingsStore.editorSettings.uiFontFamily || DEFAULT_UI_FONT_FAMILY;
return {
@ -564,6 +569,14 @@ watch(
{ immediate: true },
);
watch(
() => settingsStore.editorSettings.tableFontFamily,
(fontFamily) => {
applyDataGridFontFamily(fontFamily);
},
{ immediate: true },
);
watch(
[() => settingsStore.isEditorSettingsLoaded, () => settingsStore.editorSettings.toolbarItems.exclusiveRightSidebarPanels],
([loaded, exclusive]) => {

View File

@ -21,7 +21,6 @@ import {
useSettingsStore,
AI_PROVIDER_PRESETS,
EDITOR_THEMES,
FONT_FAMILIES,
DEFAULT_EDITOR_SETTINGS,
DEFAULT_DESKTOP_SETTINGS,
DEFAULT_SIDEBAR_TABLE_PAGE_SIZE,
@ -64,7 +63,6 @@ import {
forgetWebdavSyncSecretsPassphrase,
forgetWebdavSavedPassword,
getAppSupportInfo,
listSystemFonts,
saveWebdavSyncSecretsPreference,
saveWebdavSavedPassword,
saveSnippetSavedToken,
@ -119,7 +117,8 @@ import { currentLocale, setLocale, type Locale } from "@/i18n";
import { LOCALE_OPTIONS } from "@/lib/app/localeOptions";
import { DEFAULT_WEB_DAV_AUTO_UPLOAD_INTERVAL_MINUTES, DEFAULT_WEB_DAV_REMOTE_PATH, normalizedWebDavAutoUploadInterval, writeWebDavAutoUploadFields } from "@/lib/webdav/webdavAutoUploadConfig";
import { apiUrl } from "@/lib/common/webPath";
import { DEFAULT_UI_FONT_FAMILY, 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 { buildAppSupportInfoRows, formatAppSupportInfoForClipboard, type AppSupportInfoLabels } from "@/lib/app/supportInfo";
import { DateTimePatterns, normalizeSupportedDateTimePattern } from "@/lib/dataGrid/columnFormatter";
import { MAX_RESULT_PAGE_SIZE, MIN_RESULT_PAGE_SIZE } from "@/lib/dataGrid/paginationPageSize";
@ -151,9 +150,6 @@ const appThemeModeOptions = computed(() => [
{ value: "system" as AppThemeMode, label: t("toolbar.themeSystem"), icon: SunMoon },
]);
let cachedSystemFonts: string[] | null = null;
let pendingSystemFonts: Promise<string[]> | null = null;
const props = defineProps<{
open?: boolean;
variant?: "dialog" | "page";
@ -629,44 +625,20 @@ function confirmDeleteSnippet(snippet: SqlSnippet) {
}
}
const presetFontLabels = new Map(FONT_FAMILIES.map((font) => [font.value, font.label]));
const presetFontValues = new Set(FONT_FAMILIES.map((font) => font.value));
const uiFontPreviewValues = new Set([DEFAULT_UI_FONT_FAMILY, SYSTEM_UI_FONT_FAMILY]);
function cssFontFamilyForName(name: string): string {
return `'${name.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}', monospace`;
}
function readableFontFamily(value: string): string {
const first = value.split(",")[0]?.trim() ?? value;
return first.replace(/^['"]|['"]$/g, "").replace(/\\'/g, "'");
}
function normalizeCustomFontFamilyInput(value: string): string {
const trimmed = value.trim();
if (!trimmed) return "";
if (trimmed.includes(",") || trimmed.includes("'") || trimmed.includes('"')) return trimmed;
return cssFontFamilyForName(trimmed);
}
const systemFontOptions = computed(() => {
const options = new Set(FONT_FAMILIES.map((font) => font.value));
for (const font of systemFonts.value) options.add(cssFontFamilyForName(font));
if (editFontFamily.value) options.add(editFontFamily.value);
if (editTableFontFamily.value) options.add(editTableFontFamily.value);
return [...options];
return buildFontFamilyOptions(systemFonts.value, [editFontFamily.value]);
});
const tableFontOptions = computed(() => buildFontFamilyOptions(systemFonts.value, [editTableFontFamily.value], [DEFAULT_DATA_GRID_FONT_FAMILY]));
const uiFontOptions = computed(() => {
const options = new Set([SYSTEM_UI_FONT_FAMILY, DEFAULT_UI_FONT_FAMILY, ...systemFontOptions.value]);
if (editUiFontFamily.value) options.add(editUiFontFamily.value);
return [...options];
});
function displayFontFamily(value: string): string {
return presetFontLabels.get(value) ?? readableFontFamily(value);
}
function displayUiFontFamily(value: string): string {
if (value === SYSTEM_UI_FONT_FAMILY) return t("settings.uiFontSystemDefault");
if (value === DEFAULT_UI_FONT_FAMILY) return t("settings.uiFontAppDefault");
@ -674,22 +646,14 @@ function displayUiFontFamily(value: string): string {
}
function fontOptionStyle(value: string, selectedValue = editFontFamily.value) {
return presetFontValues.has(value) || uiFontPreviewValues.has(value) || value === selectedValue ? { fontFamily: value } : undefined;
return isPresetFontFamily(value) || uiFontPreviewValues.has(value) || value === selectedValue ? { fontFamily: value } : undefined;
}
async function loadSystemFontOptions() {
if (systemFontsLoaded.value || systemFontsLoading.value) return;
systemFontsLoading.value = true;
try {
if (cachedSystemFonts) {
systemFonts.value = cachedSystemFonts;
} else {
pendingSystemFonts ??= listSystemFonts().finally(() => {
pendingSystemFonts = null;
});
cachedSystemFonts = await pendingSystemFonts;
systemFonts.value = cachedSystemFonts;
}
systemFonts.value = await loadSystemFontNames();
systemFontsLoaded.value = true;
} catch {
systemFonts.value = [];
@ -3571,7 +3535,7 @@ onUnmounted(cleanupPreviewEditor);
</div>
<SearchableSelect
:model-value="editTableFontFamily"
:options="systemFontOptions"
:options="tableFontOptions"
:placeholder="t('settings.selectFont')"
:search-placeholder="t('settings.searchFont')"
:empty-text="t('settings.noFontsFound')"
@ -3579,8 +3543,8 @@ onUnmounted(cleanupPreviewEditor);
allow-custom
:display-name="displayFontFamily"
:normalize-custom="normalizeCustomFontFamilyInput"
:trigger-class="appearanceFontSearchTriggerClass"
:trigger-icon-class="appearanceFontSearchTriggerIconClass"
trigger-variant="outline"
trigger-class="h-9 w-full max-w-none justify-between"
content-class="w-[var(--reka-popover-trigger-width)] min-w-[260px]"
@update:model-value="onTableFontFamilyChange"
@update:open="(open: boolean) => open && loadSystemFontOptions()"

View File

@ -3846,7 +3846,7 @@ const editorThemeAccessor = () => settingsStore.editorSettings.theme;
const editorAppAppearance = () => (isDark.value ? "dark" : "light") as import("@/lib/app/appTheme").AppThemeAppearance;
const editorAppPalette = () => themePalette.value;
const editorFontSize = () => settingsStore.editorSettings.fontSize;
const editorFontFamily = () => settingsStore.editorSettings.fontFamily;
const detailEditorFontFamily = () => tableFontFamily.value;
const SIDE_DETAIL_EDITOR_MIN_HEIGHT = 160;
const SIDE_DETAIL_EDITOR_MAX_HEIGHT = 360;
const SIDE_DETAIL_EDITOR_LINE_HEIGHT = 20;
@ -3874,7 +3874,7 @@ watch(valueEditorContainer, async (el) => {
appAppearance: editorAppAppearance,
appPalette: editorAppPalette,
fontSize: editorFontSize,
fontFamily: editorFontFamily,
fontFamily: detailEditorFontFamily,
});
await valueDetailEditor.create(el, detailEditValue.value, activeCellDetail.value?.type);
} else if (!el && valueDetailEditor) {
@ -4751,7 +4751,7 @@ function canvasCellContentOverflows(item: RowItem, actualColIdx: number, visible
const displayText = formatCellCached(item.data[actualColIdx], actualColIdx);
const editText = cellEditorTextForValue(item.data[actualColIdx], actualColIdx);
if (editText.includes("\n") || editText.includes("\r") || editText.length > displayText.length) return true;
const textWidth = measureCellTextWidth(displayText, `400 13px ${settingsStore.editorSettings.fontFamily}`);
const textWidth = measureCellTextWidth(displayText, `400 ${tableFontSize.value}px ${tableFontFamily.value}`);
return textWidth > Math.max(0, cellWidth - 24);
}
@ -7761,7 +7761,7 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
<div
v-for="cell in item.values"
:key="`${item.id}:${cell.recordIndex}`"
class="relative flex shrink-0 items-center border-r border-border/70 px-2 py-0 font-mono truncate"
class="relative flex shrink-0 items-center border-r border-border/70 px-2 py-0 truncate"
:class="{
'text-muted-foreground italic': cell.isNull,
'cell-selected': transposeCellIsSelected(cell.recordIndex, cell.valueIndex) && !displayItems[cell.recordIndex]?.isDirtyCol[cell.valueIndex],
@ -9265,6 +9265,7 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
.data-grid-header-row,
.data-grid-transpose-header,
.data-grid-transpose-row {
font-family: var(--dbx-data-grid-font-family);
font-size: var(--dbx-table-font-size, 13px);
}

View File

@ -25,7 +25,7 @@ const emit = defineEmits<{ apply: [] }>();
autocorrect="off"
spellcheck="false"
rows="5"
class="min-h-24 w-full min-w-0 resize-y rounded-[6px] border border-input bg-transparent px-2.5 py-1.5 text-base outline-none transition-colors placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 md:text-sm"
class="dbx-data-grid-value-font min-h-24 w-full min-w-0 resize-y rounded-[6px] border border-input bg-transparent px-2.5 py-1.5 text-base outline-none transition-colors placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 md:text-sm"
:placeholder="t('grid.bulkEditValuePlaceholder')"
@keydown.ctrl.enter.prevent="emit('apply')"
@keydown.meta.enter.prevent="emit('apply')"

View File

@ -88,7 +88,7 @@ watch(jsonPreviewContainer, async (element) => {
appAppearance: () => (isDark.value ? "dark" : "light"),
appPalette: () => themePalette.value,
fontSize: () => settingsStore.editorSettings.fontSize,
fontFamily: () => settingsStore.editorSettings.fontFamily,
fontFamily: () => settingsStore.editorSettings.tableFontFamily,
});
await jsonPreviewEditor.create(element, props.detail?.formattedJson ?? "", "json");
} else if (!element && jsonPreviewEditor) {
@ -181,7 +181,7 @@ watch(
<img :src="detail.imagePreviewUrl" :alt="detail.column" loading="lazy" decoding="async" referrerpolicy="no-referrer" class="max-h-72 w-full object-contain" />
</a>
<div v-if="jsonView && detail.formattedJson" ref="jsonPreviewContainer" data-cell-detail-editor-root class="h-[44vh] min-h-60 overflow-hidden rounded border bg-muted/20 p-3" />
<pre v-else class="max-h-[44vh] overflow-auto rounded border bg-muted/20 p-3 font-mono text-xs whitespace-pre-wrap break-words" :class="{ 'italic text-muted-foreground': detail.value === null }">{{ detail.rawValuePreview }}</pre>
<pre v-else class="dbx-data-grid-value-font max-h-[44vh] overflow-auto rounded border bg-muted/20 p-3 text-xs whitespace-pre-wrap break-words" :class="{ 'italic text-muted-foreground': detail.value === null }">{{ detail.rawValuePreview }}</pre>
<div v-if="detail.isValuePreviewTruncated && !jsonView" class="text-[11px] text-muted-foreground">
{{ t("grid.largeValuePreviewHint", { count: detail.rawValuePreview.length }) }}
</div>

View File

@ -143,7 +143,7 @@ defineExpose({ openSearch });
/></a>
</div>
<template v-if="editing"
><div class="min-h-0 flex-1" :style="editorStyle">
><div class="dbx-data-grid-value-font min-h-0 flex-1" :style="editorStyle">
<TemporalCellEditor v-if="temporalEditorConfig" v-model="detailEditValue" :kind="temporalEditorConfig.kind" :fraction-precision="temporalEditorConfig.fractionPrecision" variant="inline" :commit-on-close="false" @cancel="emit('cancel')" @commit="emit('commit')" />
<div v-else ref="detailsEditorContainer" data-cell-detail-editor-root class="min-h-0 h-full w-full rounded border overflow-hidden" />
</div>
@ -162,7 +162,7 @@ defineExpose({ openSearch });
/>
<pre
v-else
class="overflow-auto rounded border bg-muted/20 p-2 font-mono text-xs whitespace-pre-wrap break-words cursor-pointer hover:border-primary/50"
class="dbx-data-grid-value-font overflow-auto rounded border bg-muted/20 p-2 text-xs whitespace-pre-wrap break-words cursor-pointer hover:border-primary/50"
:class="[{ 'cursor-text': detail.isEditable }, panelIsBottom && detail.imagePreviewUrl ? 'min-h-24 max-h-32 shrink-0' : '', valueFillsHeight && !detail.imagePreviewUrl ? 'min-h-0 flex-1' : '']"
@dblclick="emit('startEdit')"
>{{ detail.rawValuePreview }}</pre

View File

@ -65,11 +65,11 @@ const filteredColumnFields = computed(() => (props.columnDetail ? filterDataGrid
<a v-if="field.imagePreviewUrl" :href="field.imagePreviewUrl" role="button" class="mb-2 block max-h-48 overflow-hidden rounded border bg-muted/20" @click.prevent="openImagePreview(field.imagePreviewUrl, field.column)"
><img :src="field.imagePreviewUrl" :alt="field.column" loading="lazy" decoding="async" referrerpolicy="no-referrer" class="max-h-48 w-full object-contain"
/></a>
<pre class="max-h-44 overflow-auto rounded border bg-muted/20 p-2 font-mono text-xs whitespace-pre-wrap break-words" :class="{ 'italic text-muted-foreground': field.value === null }">{{ field.rawValuePreview }}</pre>
<pre class="dbx-data-grid-value-font max-h-44 overflow-auto rounded border bg-muted/20 p-2 text-xs whitespace-pre-wrap break-words" :class="{ 'italic text-muted-foreground': field.value === null }">{{ field.rawValuePreview }}</pre>
<div v-if="field.isValuePreviewTruncated" class="mt-1 text-[11px] text-muted-foreground">{{ t("grid.largeValuePreviewHint", { count: field.rawValuePreview.length }) }}</div>
<div v-if="field.formattedJson" class="mt-2 space-y-1">
<div class="text-muted-foreground">{{ t("grid.formattedJson") }}</div>
<pre class="max-h-44 overflow-auto rounded border bg-muted/20 p-2 font-mono text-xs whitespace-pre-wrap break-words">{{ field.formattedJson }}</pre>
<pre class="dbx-data-grid-value-font max-h-44 overflow-auto rounded border bg-muted/20 p-2 text-xs whitespace-pre-wrap break-words">{{ field.formattedJson }}</pre>
</div>
</td>
<td class="px-2 py-2">
@ -135,11 +135,11 @@ const filteredColumnFields = computed(() => (props.columnDetail ? filterDataGrid
<a v-if="field.imagePreviewUrl" :href="field.imagePreviewUrl" role="button" class="mb-2 block max-h-40 overflow-hidden rounded border bg-muted/20" @click.prevent="openImagePreview(field.imagePreviewUrl, field.column)"
><img :src="field.imagePreviewUrl" :alt="field.column" loading="lazy" decoding="async" referrerpolicy="no-referrer" class="max-h-40 w-full object-contain"
/></a>
<pre class="max-h-36 overflow-auto rounded border bg-muted/20 p-2 font-mono text-xs whitespace-pre-wrap break-words" :class="{ 'italic text-muted-foreground': field.value === null }">{{ field.rawValuePreview }}</pre>
<pre class="dbx-data-grid-value-font max-h-36 overflow-auto rounded border bg-muted/20 p-2 text-xs whitespace-pre-wrap break-words" :class="{ 'italic text-muted-foreground': field.value === null }">{{ field.rawValuePreview }}</pre>
<div v-if="field.isValuePreviewTruncated" class="mt-1 text-[11px] text-muted-foreground">{{ t("grid.largeValuePreviewHint", { count: field.rawValuePreview.length }) }}</div>
<div v-if="field.formattedJson" class="mt-2 space-y-1">
<div class="text-muted-foreground">{{ t("grid.formattedJson") }}</div>
<pre class="max-h-36 overflow-auto rounded border bg-muted/20 p-2 font-mono text-xs whitespace-pre-wrap break-words">{{ field.formattedJson }}</pre>
<pre class="dbx-data-grid-value-font max-h-36 overflow-auto rounded border bg-muted/20 p-2 text-xs whitespace-pre-wrap break-words">{{ field.formattedJson }}</pre>
</div>
</td>
<td class="px-2 py-2">

View File

@ -0,0 +1,55 @@
<script setup lang="ts">
import { computed, ref } from "vue";
import { useI18n } from "vue-i18n";
import { SearchableSelect } from "@/components/ui/searchable-select";
import { DEFAULT_DATA_GRID_FONT_FAMILY } from "@/lib/app/appFonts";
import { buildFontFamilyOptions, displayFontFamily, loadSystemFontNames } from "@/lib/app/fontFamilyOptions";
import { useSettingsStore } from "@/stores/settingsStore";
const { t } = useI18n();
const settingsStore = useSettingsStore();
const systemFontNames = ref<string[]>([]);
const tableFontFamily = computed(() => settingsStore.editorSettings.tableFontFamily);
const tableFontOptions = computed(() => buildFontFamilyOptions(systemFontNames.value, [tableFontFamily.value], [DEFAULT_DATA_GRID_FONT_FAMILY]));
function setTableFontFamily(value: string) {
settingsStore.updateEditorSettings({ tableFontFamily: value });
}
async function loadSystemFontOptions() {
try {
systemFontNames.value = await loadSystemFontNames();
} catch {
systemFontNames.value = [];
}
}
</script>
<template>
<div class="flex items-center justify-between gap-3 px-3 py-1.5 text-xs">
<div class="min-w-0 flex items-center gap-2 font-medium">
<span class="flex h-3.5 w-3.5 shrink-0 items-center justify-center text-[9px] font-semibold text-muted-foreground">Aa</span>
<span>{{ t("grid.tableFontFamily") }}</span>
</div>
<SearchableSelect
:model-value="tableFontFamily"
:options="tableFontOptions"
:placeholder="t('settings.selectFont')"
:search-placeholder="t('settings.searchFont')"
:empty-text="t('settings.noFontsFound')"
:display-name="displayFontFamily"
trigger-variant="outline"
trigger-class="h-6 w-48 max-w-48 justify-between bg-muted/40 px-2 text-xs"
content-class="w-64 max-w-[calc(100vw-2rem)]"
@update:model-value="setTableFontFamily"
@update:open="(open: boolean) => open && loadSystemFontOptions()"
>
<template #trigger-label="{ label }">
<span class="truncate" :style="{ fontFamily: tableFontFamily }">{{ label }}</span>
</template>
<template #option-label="{ option, label }">
<span class="truncate" :style="{ fontFamily: option }">{{ label }}</span>
</template>
</SearchableSelect>
</div>
</template>

View File

@ -38,7 +38,7 @@ watch(previewContainer, async (element) => {
appAppearance: () => (isDark.value ? "dark" : "light") as import("@/lib/app/appTheme").AppThemeAppearance,
appPalette: () => themePalette.value,
fontSize: () => settingsStore.editorSettings.fontSize,
fontFamily: () => settingsStore.editorSettings.fontFamily,
fontFamily: () => settingsStore.editorSettings.tableFontFamily,
});
await previewEditor.create(element, props.text, "json");
} else if (!element && previewEditor) {
@ -65,7 +65,7 @@ watch(
<div v-if="text" class="flex min-h-0 flex-1 flex-col overflow-hidden p-2">
<div v-if="usesCodeEditor" ref="previewContainer" data-cell-detail-editor-root class="min-h-0 flex-1 overflow-hidden" />
<template v-else>
<pre class="min-h-0 flex-1 overflow-auto rounded border bg-muted/20 p-3 font-mono text-xs whitespace-pre-wrap break-words">{{ text }}</pre>
<pre class="dbx-data-grid-value-font min-h-0 flex-1 overflow-auto rounded border bg-muted/20 p-3 text-xs whitespace-pre-wrap break-words">{{ text }}</pre>
<div class="mt-1 text-[11px] text-muted-foreground">{{ t("grid.largeValuePreviewHint", { count: text.length }) }}</div>
</template>
</div>

View File

@ -19,6 +19,7 @@ import QueryLoadingState from "@/components/common/QueryLoadingState.vue";
import QueryErrorActions from "@/components/common/QueryErrorActions.vue";
import QueryResultToolbarActions from "@/components/layout/QueryResultToolbarActions.vue";
import QueryResultViewSwitcher from "@/components/layout/QueryResultViewSwitcher.vue";
import DataGridFontFamilyControl from "@/components/grid/DataGridFontFamilyControl.vue";
import type { ColumnInfo } from "@/components/editor/ColumnInfoPanel.vue";
let dataGridComponentPromise: Promise<typeof import("@/components/grid/DataGrid.vue")> | undefined;
function loadDataGridComponent() {
@ -977,6 +978,7 @@ defineExpose({ focusSearch, refreshData, refreshQueryEditorCompletionCache, hand
</button>
</div>
</div>
<DataGridFontFamilyControl />
<div class="flex items-center justify-between gap-3 px-3 py-1.5 text-xs">
<div class="min-w-0 flex items-center gap-2 font-medium">
<span class="flex h-3.5 w-3.5 shrink-0 items-center justify-center text-[11px] font-semibold text-muted-foreground">A</span>
@ -1389,6 +1391,7 @@ defineExpose({ focusSearch, refreshData, refreshQueryEditorCompletionCache, hand
</button>
</div>
</div>
<DataGridFontFamilyControl />
<div class="flex items-center justify-between gap-3 px-3 py-1.5 text-xs">
<div class="min-w-0 flex items-center gap-2 font-medium">
<span class="flex h-3.5 w-3.5 shrink-0 items-center justify-center text-[11px] font-semibold text-muted-foreground">A</span>

View File

@ -12,11 +12,13 @@ const mocks = vi.hoisted(() => ({
openSearch: vi.fn(),
focus: vi.fn(),
onChange: undefined as undefined | ((value: string) => void),
fontFamily: undefined as undefined | (() => string),
}));
vi.mock("@/composables/useCellDetailEditor", () => ({
useCellDetailEditor: (options: { onChange?: (value: string) => void }) => {
useCellDetailEditor: (options: { onChange?: (value: string) => void; fontFamily: () => string }) => {
mocks.onChange = options.onChange;
mocks.fontFamily = options.fontFamily;
return {
create: mocks.create,
destroy: mocks.destroy,
@ -29,7 +31,7 @@ vi.mock("@/composables/useCellDetailEditor", () => ({
}));
vi.mock("@/composables/useTheme", () => ({ useTheme: () => ({ isDark: ref(false), themePalette: ref({}) }) }));
vi.mock("@/stores/settingsStore", () => ({
useSettingsStore: () => ({ editorSettings: { theme: "default", fontSize: 13, fontFamily: "monospace" } }),
useSettingsStore: () => ({ editorSettings: { theme: "default", fontSize: 13, fontFamily: "monospace", tableFontFamily: "'Grid Font', sans-serif" } }),
}));
vi.mock("@/lib/dataGrid/geometryPreview", () => ({ renderWktOnCanvas: vi.fn() }));
@ -59,11 +61,12 @@ function detail(): DataGridCellDetail {
beforeEach(() => {
vi.clearAllMocks();
mocks.onChange = undefined;
mocks.fontFamily = undefined;
mocks.getValue.mockReturnValue("");
});
describe("useDataGridCellDetail", () => {
it("focuses CodeMirror after entering cell detail edit mode", async () => {
it("focuses CodeMirror and applies the data grid font in cell detail edit mode", async () => {
const scope = effectScope();
const composable = scope.run(() => useDataGridCellDetail({ detail: ref(detail()), editValue: ref(""), onCancel: vi.fn() }))!;
@ -72,6 +75,7 @@ describe("useDataGridCellDetail", () => {
await Promise.resolve();
expect(mocks.focus).toHaveBeenCalledOnce();
expect(mocks.fontFamily?.()).toBe("'Grid Font', sans-serif");
composable.detailsEditorContainer.value = undefined;
await nextTick();

View File

@ -20,7 +20,7 @@ export function useDataGridCellDetail(options: { detail: Ref<DataGridCellDetail>
appAppearance: () => (isDark.value ? "dark" : "light") as import("@/lib/app/appTheme").AppThemeAppearance,
appPalette: () => themePalette.value,
fontSize: () => settingsStore.editorSettings.fontSize,
fontFamily: () => settingsStore.editorSettings.fontFamily,
fontFamily: () => settingsStore.editorSettings.tableFontFamily,
});
watch(geometryPreviewOpen, async (open) => {

View File

@ -923,6 +923,7 @@ export default {
domRenderMode: "DOM",
canvasRenderMode: "Canvas",
renderModeHint: "Switch between Canvas rendering and the DOM fallback grid.",
tableFontFamily: "Table font",
tableFontSize: "Table font size",
filter: "Filter",
conditionHistoryEmpty: "No history yet",

View File

@ -872,6 +872,7 @@ export default withEnglishFallback({
domRenderMode: "DOM",
canvasRenderMode: "Canvas",
renderModeHint: "Alterna entre el renderizado Canvas y la cuadrícula DOM de respaldo.",
tableFontFamily: "Fuente de la tabla",
tableFontSize: "Tamaño de fuente de la tabla",
filter: "Filtrar",
conditionHistoryEmpty: "Sin historial aún",

View File

@ -870,6 +870,7 @@ export default withEnglishFallback({
domRenderMode: "DOM",
canvasRenderMode: "Canvas",
renderModeHint: "Passa dal rendering Canvas al grid di fallback DOM.",
tableFontFamily: "Font della tabella",
tableFontSize: "Dimensione font tabella",
filter: "Filtra",
conditionHistoryEmpty: "Nessuna cronologia ancora",

View File

@ -867,6 +867,7 @@ export default withEnglishFallback({
domRenderMode: "DOM",
canvasRenderMode: "Canvas",
renderModeHint: "CanvasレンダリングとDOMフォールバックグリッドを切り替えます。",
tableFontFamily: "テーブルのフォント",
tableFontSize: "テーブルのフォントサイズ",
filter: "フィルター",
conditionHistoryEmpty: "履歴はまだありません",

View File

@ -872,6 +872,7 @@ export default withEnglishFallback({
domRenderMode: "DOM",
canvasRenderMode: "Canvas",
renderModeHint: "Alterne entre a renderização Canvas e a grade DOM alternativa.",
tableFontFamily: "Fonte da tabela",
tableFontSize: "Tamanho da fonte da tabela",
filter: "Filtrar",
conditionHistoryEmpty: "Sem histórico ainda",

View File

@ -925,6 +925,7 @@ export default withEnglishFallback({
domRenderMode: "DOM",
canvasRenderMode: "Canvas",
renderModeHint: "在 Canvas 渲染和 DOM 兜底表格之间切换。",
tableFontFamily: "表格字体",
tableFontSize: "表格字号",
filter: "筛选",
conditionHistoryEmpty: "暂无历史记录",

View File

@ -872,6 +872,7 @@ export default withEnglishFallback({
domRenderMode: "DOM",
canvasRenderMode: "Canvas",
renderModeHint: "在 Canvas 渲染和 DOM 備援表格之間切換。",
tableFontFamily: "表格字型",
tableFontSize: "表格字號",
filter: "篩選",
conditionHistoryEmpty: "暫無歷史記錄",

View File

@ -1,8 +1,34 @@
export const APP_FONT_SANS_CSS_VAR = "--font-sans";
export const DATA_GRID_FONT_FAMILY_CSS_VAR = "--dbx-data-grid-font-family";
export const DEFAULT_UI_FONT_FAMILY = `"Geist Variable", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Segoe UI", system-ui, sans-serif`;
export const DEFAULT_DATA_GRID_FONT_FAMILY = `"Geist Variable Tabular", "Geist Variable", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif`;
// Native-feeling UI option without DBX's bundled/brand font at the front of the stack.
export const SYSTEM_UI_FONT_FAMILY = `system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif`;
export const FONT_FAMILIES: { value: string; label: string }[] = [
{ value: "'Fira Code', 'Cascadia Code', 'Cascadia Mono', 'JetBrains Mono', monospace", label: "Fira Code" },
{ value: "'JetBrains Mono', 'Fira Code', monospace", label: "JetBrains Mono" },
{ value: "'Cascadia Code', 'Cascadia Mono', monospace", label: "Cascadia Code" },
{ value: "'Source Code Pro', monospace", label: "Source Code Pro" },
{ value: "'SF Mono', 'Menlo', monospace", label: "SF Mono / Menlo" },
{ value: "'Consolas', 'Courier New', monospace", label: "Consolas" },
{ value: "monospace", label: "System Monospace" },
];
export function cssFontFamilyForName(name: string): string {
return `'${name.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}', monospace`;
}
export function readableFontFamily(value: string): string {
const firstFamily = value.split(",")[0]?.trim() ?? value;
return firstFamily.replace(/^['"]|['"]$/g, "").replace(/\\'/g, "'");
}
export function normalizeCustomFontFamilyInput(value: string): string {
const trimmed = value.trim();
if (!trimmed) return "";
if (trimmed.includes(",") || trimmed.includes("'") || trimmed.includes('"')) return trimmed;
return cssFontFamilyForName(trimmed);
}

View File

@ -0,0 +1,29 @@
import { cssFontFamilyForName, FONT_FAMILIES, readableFontFamily } from "@/lib/app/appFonts";
import { listSystemFonts } from "@/lib/backend/api";
let cachedSystemFontNames: string[] | null = null;
let pendingSystemFontNames: Promise<string[]> | null = null;
const presetFontLabels = new Map(FONT_FAMILIES.map((font) => [font.value, font.label]));
const presetFontValues = new Set(FONT_FAMILIES.map((font) => font.value));
export function buildFontFamilyOptions(systemFontNames: readonly string[], selectedValues: readonly string[] = [], leadingValues: readonly string[] = []): string[] {
return [...new Set([...leadingValues, ...FONT_FAMILIES.map((font) => font.value), ...systemFontNames.map(cssFontFamilyForName), ...selectedValues.filter(Boolean)])];
}
export function displayFontFamily(value: string): string {
return presetFontLabels.get(value) ?? readableFontFamily(value);
}
export function isPresetFontFamily(value: string): boolean {
return presetFontValues.has(value);
}
export async function loadSystemFontNames(): Promise<string[]> {
if (cachedSystemFontNames) return cachedSystemFontNames;
pendingSystemFontNames ??= listSystemFonts().finally(() => {
pendingSystemFontNames = null;
});
cachedSystemFontNames = await pendingSystemFontNames;
return cachedSystemFontNames;
}

View File

@ -108,6 +108,13 @@ describe("normalizeEditorSettings", () => {
expect(invalid.dataGridHideNullColumns).toBe(false);
});
it("defaults the data grid font and preserves a custom font family", () => {
const defaultFontFamily = `"Geist Variable Tabular", "Geist Variable", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif`;
expect(normalizeEditorSettings({}).tableFontFamily).toBe(defaultFontFamily);
expect(normalizeEditorSettings({ tableFontFamily: "'IBM Plex Mono', monospace" }).tableFontFamily).toBe("'IBM Plex Mono', monospace");
expect(normalizeEditorSettings({ tableFontFamily: " " }).tableFontFamily).toBe(defaultFontFamily);
});
it("shows cell detail metadata by default and preserves collapsed state", () => {
expect(normalizeEditorSettings({}).cellDetailMetadataCollapsed).toBe(false);
expect(normalizeEditorSettings({ cellDetailMetadataCollapsed: true }).cellDetailMetadataCollapsed).toBe(true);

View File

@ -535,16 +535,6 @@ export const EDITOR_THEMES: { value: EditorTheme; label: string; dark: boolean }
const EDITOR_THEME_VALUES = new Set<EditorTheme>(EDITOR_THEMES.map((theme) => theme.value));
export const FONT_FAMILIES: { value: string; label: string }[] = [
{ value: "'Fira Code', 'Cascadia Code', 'Cascadia Mono', 'JetBrains Mono', monospace", label: "Fira Code" },
{ value: "'JetBrains Mono', 'Fira Code', monospace", label: "JetBrains Mono" },
{ value: "'Cascadia Code', 'Cascadia Mono', monospace", label: "Cascadia Code" },
{ value: "'Source Code Pro', monospace", label: "Source Code Pro" },
{ value: "'SF Mono', 'Menlo', monospace", label: "SF Mono / Menlo" },
{ value: "'Consolas', 'Courier New', monospace", label: "Consolas" },
{ value: "monospace", label: "System Monospace" },
];
export const EXECUTE_MODE_CURRENT_DEFAULT_VERSION = 1;
export const DEFAULT_EDITOR_SETTINGS: EditorSettings = {

View File

@ -1354,6 +1354,10 @@ body.dbx-table-reference-dragging * {
font-size: 12.5px;
}
.dbx-data-grid-value-font {
font-family: var(--dbx-data-grid-font-family);
}
/* Tailwind CSS v4 targets Safari 16.4+. Keep these legacy WebKit fallbacks
* isolated so supported WebViews continue using the generated component CSS. */
@supports not (color: oklch(0.5 0.1 180)) {

View File

@ -4,6 +4,7 @@ import { test } from "vitest";
const dataGridSource = readFileSync("apps/desktop/src/components/grid/DataGrid.vue", "utf8");
const settingsDialogSource = readFileSync("apps/desktop/src/components/editor/EditorSettingsDialog.vue", "utf8");
const quickControlSource = readFileSync("apps/desktop/src/components/grid/DataGridFontFamilyControl.vue", "utf8");
test("applies the configured result grid font to DOM and canvas renderers", () => {
assert.match(dataGridSource, /const tableFontFamily = computed\(\(\) => settingsStore\.editorSettings\.tableFontFamily\)/);
@ -34,3 +35,20 @@ test("keeps appearance section spacing and help icons aligned without stacked ma
assert.match(settingsDialogSource, /activeSettingsTab === 'appearance'" class="settings-appearance-section flex flex-col gap-4 py-2"/);
assert.match(settingsDialogSource, /<div class="flex min-w-0 items-center gap-1">\s*<Label class="min-w-0 whitespace-normal leading-tight">\{\{ t\("settings\.dataGridFontFamily"\) \}\}<\/Label>/);
});
test("matches the result grid font selector styling with date and time selectors", () => {
const selectorStart = settingsDialogSource.indexOf(`:model-value="editTableFontFamily"`);
const selectorEnd = settingsDialogSource.indexOf(`</SearchableSelect>`, selectorStart);
const selectorSource = settingsDialogSource.slice(selectorStart, selectorEnd);
assert.ok(selectorStart >= 0);
assert.ok(selectorEnd > selectorStart);
assert.match(selectorSource, /trigger-variant="outline"/);
assert.match(selectorSource, /trigger-class="h-9 w-full max-w-none justify-between"/);
});
test("uses a table font label in view options without changing the settings label", () => {
assert.match(quickControlSource, /t\("grid\.tableFontFamily"\)/);
assert.doesNotMatch(quickControlSource, /t\("settings\.dataGridFontFamily"\)/);
assert.match(settingsDialogSource, /t\("settings\.dataGridFontFamily"\)/);
});