feat(grid): add table font shortcuts
This commit is contained in:
parent
43b810fb36
commit
5d1a6e63cd
|
|
@ -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]) => {
|
||||
|
|
|
|||
|
|
@ -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()"
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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')"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -867,6 +867,7 @@ export default withEnglishFallback({
|
|||
domRenderMode: "DOM",
|
||||
canvasRenderMode: "Canvas",
|
||||
renderModeHint: "CanvasレンダリングとDOMフォールバックグリッドを切り替えます。",
|
||||
tableFontFamily: "テーブルのフォント",
|
||||
tableFontSize: "テーブルのフォントサイズ",
|
||||
filter: "フィルター",
|
||||
conditionHistoryEmpty: "履歴はまだありません",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -925,6 +925,7 @@ export default withEnglishFallback({
|
|||
domRenderMode: "DOM",
|
||||
canvasRenderMode: "Canvas",
|
||||
renderModeHint: "在 Canvas 渲染和 DOM 兜底表格之间切换。",
|
||||
tableFontFamily: "表格字体",
|
||||
tableFontSize: "表格字号",
|
||||
filter: "筛选",
|
||||
conditionHistoryEmpty: "暂无历史记录",
|
||||
|
|
|
|||
|
|
@ -872,6 +872,7 @@ export default withEnglishFallback({
|
|||
domRenderMode: "DOM",
|
||||
canvasRenderMode: "Canvas",
|
||||
renderModeHint: "在 Canvas 渲染和 DOM 備援表格之間切換。",
|
||||
tableFontFamily: "表格字型",
|
||||
tableFontSize: "表格字號",
|
||||
filter: "篩選",
|
||||
conditionHistoryEmpty: "暫無歷史記錄",
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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)) {
|
||||
|
|
|
|||
|
|
@ -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"\)/);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue