mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-19 16:16:45 +00:00
feat(desktop): deliver font settings to plugin sandboxes
This commit is contained in:
+23
-11
@@ -128,7 +128,7 @@ import type { DriverStoreFocus, DriverStoreTab } from "@/lib/connection/agentDri
|
||||
import { safeLocalStorageGet, safeLocalStorageSet } from "@/lib/backend/safeStorage";
|
||||
import { apiUrl, webPath } from "@/lib/common/webPath";
|
||||
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_MONO_FONT_FAMILY, DEFAULT_UI_FONT_FAMILY, FONT_MONO_CSS_VAR } from "@/lib/app/appFonts";
|
||||
import { DATA_GRID_TYPE_COLOR_KEYS, dataGridTypeColorCssVar, resolveActiveDataGridTypeColors } from "@/lib/dataGrid/dataGridTypeColorScheme";
|
||||
import { rankSavedSqlHistory } from "@/lib/savedSql/savedSqlHistory";
|
||||
import { useUiFontFamilyPreview } from "@/composables/useUiFontFamilyPreview";
|
||||
@@ -216,7 +216,7 @@ connectionStore.setBeforeConnectHandler(async (config) => {
|
||||
}
|
||||
});
|
||||
const { message: toastMessage, visible: toastVisible, action: toastAction, toast } = useToast();
|
||||
const { isDark, themeMode, applyTheme, setThemeMode } = useTheme();
|
||||
const { isDark, themeMode, applyTheme, setThemeMode, writeRootToken } = useTheme();
|
||||
const { activeCount: activeBackgroundTaskCount } = useExportTracker();
|
||||
const trackedUpdateTaskCount = computed(() => countActiveUpdateBlockingTasks(activeBackgroundTaskCount.value, queryStore.tabs));
|
||||
const {
|
||||
@@ -1395,17 +1395,23 @@ function resetUiZoom() {
|
||||
showUiScaleToast();
|
||||
}
|
||||
|
||||
function applyUiFontFamily(fontFamily: string) {
|
||||
function applyUiFontFamily(fontFamily: string, options?: { debouncePluginBumpMs?: number }) {
|
||||
if (typeof document === "undefined") return;
|
||||
const next = fontFamily || DEFAULT_UI_FONT_FAMILY;
|
||||
// Override Tailwind's shared sans variable so app chrome and existing UI classes stay in sync.
|
||||
document.documentElement.style.setProperty(APP_FONT_SANS_CSS_VAR, next);
|
||||
// The plugin-facing bump is debounced while the preview slider/keystrokes fire.
|
||||
writeRootToken(APP_FONT_SANS_CSS_VAR, next, options?.debouncePluginBumpMs ? { debounceMs: options.debouncePluginBumpMs } : undefined);
|
||||
document.body.style.fontFamily = `var(${APP_FONT_SANS_CSS_VAR}, ${DEFAULT_UI_FONT_FAMILY})`;
|
||||
}
|
||||
|
||||
// Mirrors the editor font onto the global mono token so host mono surfaces and
|
||||
// plugin sandboxes (via the plugin bridge's token push) follow the same setting.
|
||||
function applyMonoFontFamily(fontFamily: string) {
|
||||
writeRootToken(FONT_MONO_CSS_VAR, fontFamily || DEFAULT_MONO_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);
|
||||
writeRootToken(DATA_GRID_FONT_FAMILY_CSS_VAR, fontFamily || DEFAULT_DATA_GRID_FONT_FAMILY);
|
||||
}
|
||||
|
||||
// Both grid renderers read these variables, so overriding them here recolors the
|
||||
@@ -1490,11 +1496,17 @@ watch(
|
||||
watch(
|
||||
[() => settingsStore.editorSettings.uiFontFamily, uiFontFamilyPreview],
|
||||
([fontFamily, preview]) => {
|
||||
if (preview) {
|
||||
applyUiFontFamily(preview);
|
||||
return;
|
||||
}
|
||||
applyUiFontFamily(fontFamily);
|
||||
// Preview keystrokes write immediately (host feels instant) but coalesce
|
||||
// the plugin-bridge bump; committed changes bump right away.
|
||||
applyUiFontFamily(preview || fontFamily, preview ? { debouncePluginBumpMs: 150 } : undefined);
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
watch(
|
||||
() => settingsStore.editorSettings.fontFamily,
|
||||
(fontFamily) => {
|
||||
applyMonoFontFamily(fontFamily);
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
@@ -5,6 +5,7 @@ import * as api from "@/lib/backend/api";
|
||||
import { isTauriRuntime } from "@/lib/backend/tauriRuntime";
|
||||
import { copyToClipboard } from "@/lib/common/clipboard";
|
||||
import { PluginHostBridge, pluginSandboxDocument, type PluginBridgeTheme, type PluginSaveFileRequest, type PluginSaveFileResult, type PluginWorkbenchContext } from "@/lib/plugins/pluginHostBridge";
|
||||
import { buildPluginEditorAppearance } from "@/lib/plugins/pluginAppearance";
|
||||
import type { InstalledPlugin, PluginWorkbenchContribution } from "@/types/database";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useTheme } from "@/composables/useTheme";
|
||||
@@ -59,7 +60,11 @@ function currentBridgeTheme(): PluginBridgeTheme {
|
||||
}
|
||||
}
|
||||
}
|
||||
return { appearance: isDark.value ? "dark" : "light", tokens };
|
||||
return {
|
||||
appearance: isDark.value ? "dark" : "light",
|
||||
tokens,
|
||||
editor: buildPluginEditorAppearance(settingsStore.editorSettings),
|
||||
};
|
||||
}
|
||||
|
||||
function createBridge() {
|
||||
@@ -249,11 +254,11 @@ watch(appLocale, (locale) => bridge?.updateLocale(locale));
|
||||
// path — dark/light, palette switch, custom colors — re-pushes the resolved
|
||||
// tokens; watching isDark/custom colors alone misses palette-only switches.
|
||||
watch(themeRevision, () => bridge?.updateTheme(currentBridgeTheme()));
|
||||
// Font settings are applied outside applyTheme() (see App.vue applyUiFontFamily)
|
||||
// and therefore never bump themeRevision; watch them explicitly so font token
|
||||
// changes reach live plugin bridges without waiting for the next theme switch.
|
||||
// Font families are mirrored onto root tokens by App.vue (writeRootToken) and
|
||||
// reach live bridges through the themeRevision bump above. fontSize and the
|
||||
// SQL editor syntax theme have no CSS-token carrier — watch them explicitly.
|
||||
watch(
|
||||
() => [settingsStore.editorSettings.uiFontFamily, settingsStore.editorSettings.fontFamily, settingsStore.editorSettings.fontSize],
|
||||
() => [settingsStore.editorSettings.fontSize, settingsStore.editorSettings.theme],
|
||||
() => bridge?.updateTheme(currentBridgeTheme()),
|
||||
);
|
||||
|
||||
|
||||
@@ -175,4 +175,55 @@ describe("useTheme on Linux", () => {
|
||||
expect(theme.customUiColors.value.background).toBe("#ffffff");
|
||||
expect(theme.customUiColorsDark.value.background).toBe("#1a1b1e");
|
||||
});
|
||||
|
||||
it("bumps the theme revision so out-of-band token writers (font settings) reach plugin bridges", async () => {
|
||||
const theme = await loadTheme("light");
|
||||
const before = theme.themeRevision.value;
|
||||
|
||||
theme.bumpThemeRevision();
|
||||
|
||||
expect(theme.themeRevision.value).toBe(before + 1);
|
||||
// No theme classes are touched: this is a pure revision bump.
|
||||
expect(document.documentElement.classList.contains("disable-transitions")).toBe(false);
|
||||
});
|
||||
|
||||
it("writeRootToken writes the inline override and bumps the revision", async () => {
|
||||
const theme = await loadTheme("light");
|
||||
const before = theme.themeRevision.value;
|
||||
|
||||
theme.writeRootToken("--dbx-test-token", "1px");
|
||||
|
||||
expect(document.documentElement.style.getPropertyValue("--dbx-test-token")).toBe("1px");
|
||||
expect(theme.themeRevision.value).toBe(before + 1);
|
||||
document.documentElement.style.removeProperty("--dbx-test-token");
|
||||
});
|
||||
|
||||
it("writeRootToken coalesces rapid writes into one debounced bump", async () => {
|
||||
const theme = await loadTheme("light");
|
||||
const before = theme.themeRevision.value;
|
||||
|
||||
theme.writeRootToken("--dbx-test-token", "1px", { debounceMs: 30 });
|
||||
theme.writeRootToken("--dbx-test-token", "2px", { debounceMs: 30 });
|
||||
// Neither bump has fired yet, but the write itself is immediate.
|
||||
expect(theme.themeRevision.value).toBe(before);
|
||||
expect(document.documentElement.style.getPropertyValue("--dbx-test-token")).toBe("2px");
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 60));
|
||||
expect(theme.themeRevision.value).toBe(before + 1);
|
||||
document.documentElement.style.removeProperty("--dbx-test-token");
|
||||
});
|
||||
|
||||
it("writeRootToken with an immediate write supersedes a pending debounced bump", async () => {
|
||||
const theme = await loadTheme("light");
|
||||
const before = theme.themeRevision.value;
|
||||
|
||||
theme.writeRootToken("--dbx-test-token", "1px", { debounceMs: 30 });
|
||||
theme.writeRootToken("--dbx-test-token", "2px");
|
||||
expect(theme.themeRevision.value).toBe(before + 1);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 60));
|
||||
// The superseded timer must not fire a second bump.
|
||||
expect(theme.themeRevision.value).toBe(before + 1);
|
||||
document.documentElement.style.removeProperty("--dbx-test-token");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -63,6 +63,39 @@ const isDark = computed(() => resolveAppThemeAppearance(themeMode.value, systemP
|
||||
// resolved token set (plugin iframe bridge) watch this revision instead.
|
||||
const themeRevision = ref(0);
|
||||
|
||||
// Token mutations on the root that happen outside applyTheme() — e.g. the UI
|
||||
// and editor font settings write --font-sans / --font-mono inline — bump this
|
||||
// so open plugin workbench bridges re-read and re-push the resolved tokens.
|
||||
function bumpThemeRevision() {
|
||||
themeRevision.value += 1;
|
||||
}
|
||||
|
||||
let pendingBumpTimer: ReturnType<typeof setTimeout> | undefined;
|
||||
|
||||
// The one sanctioned way to write a design token onto the root outside
|
||||
// applyTheme(): writes the inline override AND bumps the theme revision.
|
||||
// Writing root tokens directly (documentElement.style.setProperty) skips the
|
||||
// bump and open plugin bridges keep stale tokens — always go through here.
|
||||
// `debounceMs` coalesces rapid writes (font-family preview keystrokes) into
|
||||
// one bump; an immediate write afterwards supersedes the pending one.
|
||||
function writeRootToken(cssVar: string, value: string, options?: { debounceMs?: number }) {
|
||||
if (typeof document === "undefined") return;
|
||||
document.documentElement.style.setProperty(cssVar, value);
|
||||
if (options?.debounceMs) {
|
||||
if (pendingBumpTimer) clearTimeout(pendingBumpTimer);
|
||||
pendingBumpTimer = setTimeout(() => {
|
||||
pendingBumpTimer = undefined;
|
||||
bumpThemeRevision();
|
||||
}, options.debounceMs);
|
||||
return;
|
||||
}
|
||||
if (pendingBumpTimer) {
|
||||
clearTimeout(pendingBumpTimer);
|
||||
pendingBumpTimer = undefined;
|
||||
}
|
||||
bumpThemeRevision();
|
||||
}
|
||||
|
||||
let mediaQuery: MediaQueryList | null = null;
|
||||
let isListeningForSystemTheme = false;
|
||||
let cachedTauriWindow: typeof import("@tauri-apps/api/window") | null = null;
|
||||
@@ -212,5 +245,25 @@ export function useTheme() {
|
||||
setThemeMode(isDark.value ? "light" : "dark");
|
||||
}
|
||||
|
||||
return { isDark, themeMode, themePalette, customUiColors, customUiColorsDark, activeCustomUiColors, themeRevision, cornerStyle, applyTheme, setThemeMode, setThemePalette, previewThemePalette, clearThemePalettePreview, setCustomUiColors, resetCustomUiColors, setCornerStyle, toggleTheme };
|
||||
return {
|
||||
isDark,
|
||||
themeMode,
|
||||
themePalette,
|
||||
customUiColors,
|
||||
customUiColorsDark,
|
||||
activeCustomUiColors,
|
||||
themeRevision,
|
||||
bumpThemeRevision,
|
||||
writeRootToken,
|
||||
cornerStyle,
|
||||
applyTheme,
|
||||
setThemeMode,
|
||||
setThemePalette,
|
||||
previewThemePalette,
|
||||
clearThemePalettePreview,
|
||||
setCustomUiColors,
|
||||
resetCustomUiColors,
|
||||
setCornerStyle,
|
||||
toggleTheme,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
export const APP_FONT_SANS_CSS_VAR = "--font-sans";
|
||||
export const FONT_MONO_CSS_VAR = "--font-mono";
|
||||
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`;
|
||||
// Keep in sync with the editor font default in settingsStore; also the baseline
|
||||
// `--font-mono` value in tokens.css, which plugin sandboxes consume via the bridge.
|
||||
export const DEFAULT_MONO_FONT_FAMILY = `'Fira Code', 'Cascadia Code', 'Cascadia Mono', 'JetBrains Mono', monospace`;
|
||||
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.
|
||||
|
||||
+20
-1
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildPluginAppearance, FALLBACK_APPEARANCE_COLORS, readAppearanceTokens } from "./pluginAppearance";
|
||||
import { buildPluginAppearance, buildPluginEditorAppearance, FALLBACK_APPEARANCE_COLORS, readAppearanceTokens } from "./pluginAppearance";
|
||||
|
||||
describe("buildPluginAppearance", () => {
|
||||
it("uses the pearl (white) fallback tokens for light and the .dark tokens for dark", () => {
|
||||
@@ -32,3 +32,22 @@ describe("buildPluginAppearance", () => {
|
||||
expect(() => readAppearanceTokens()).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildPluginEditorAppearance", () => {
|
||||
it("carries the editor font family, size and SQL syntax theme", () => {
|
||||
expect(buildPluginEditorAppearance({ fontFamily: "Fira Code", fontSize: 13, theme: "one-dark" })).toEqual({
|
||||
fontFamily: "Fira Code",
|
||||
fontSize: 13,
|
||||
theme: "one-dark",
|
||||
});
|
||||
});
|
||||
|
||||
it("returns undefined unless every field is usable (no partial snapshots)", () => {
|
||||
expect(buildPluginEditorAppearance({})).toBeUndefined();
|
||||
expect(buildPluginEditorAppearance({ fontFamily: "Fira Code" })).toBeUndefined();
|
||||
expect(buildPluginEditorAppearance({ fontFamily: "Fira Code", fontSize: 13 })).toBeUndefined();
|
||||
expect(buildPluginEditorAppearance({ fontFamily: "Fira Code", fontSize: 0, theme: "one-dark" })).toBeUndefined();
|
||||
expect(buildPluginEditorAppearance({ fontFamily: "Fira Code", fontSize: Number.NaN, theme: "one-dark" })).toBeUndefined();
|
||||
expect(buildPluginEditorAppearance({ fontFamily: " ", fontSize: 13, theme: "one-dark" })).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
// 插件沙箱外观快照:读取 DBX 根节点的设计令牌(随 .dark / 调色板类
|
||||
// 实时变化)加编辑器字体设置,组装成插件契约里的 appearance 对象,
|
||||
// 供 PluginHostBridge 在 init 下发、主题变化时实时推送。
|
||||
// 插件沙箱外观契约与快照工具:PluginAppearance 描述宿主下发的 appearance
|
||||
// 对象(1.0 部分下发、1.1 theme 通道只带颜色令牌);buildPluginAppearance
|
||||
// 组装快照,buildPluginEditorAppearance 产出结构化 editor 字段。实际推送
|
||||
// 链路在 PluginWorkbenchHost.currentBridgeTheme → PluginHostBridge.updateTheme。
|
||||
|
||||
import type { PluginEditorAppearance } from "./pluginHostBridge";
|
||||
|
||||
export type { PluginEditorAppearance } from "./pluginHostBridge";
|
||||
|
||||
export interface PluginAppearanceColors {
|
||||
background: string;
|
||||
@@ -16,8 +21,26 @@ export interface PluginAppearanceColors {
|
||||
export interface PluginAppearance {
|
||||
colorScheme: "light" | "dark";
|
||||
colors: PluginAppearanceColors;
|
||||
// terminal.fontFamily comes from the host's --font-mono token. It is a
|
||||
// mirror of the editor font for read-only following; terminal.fontSize is
|
||||
// NOT delivered and stays plugin-owned (the SSH terminal, for one, has its
|
||||
// own zoom that persists per workbench). The full editor settings — family,
|
||||
// size and SQL syntax theme — are delivered structured as `editor`.
|
||||
terminal: { fontFamily: string; fontSize: number };
|
||||
ui?: { fontFamily: string };
|
||||
editor?: PluginEditorAppearance;
|
||||
}
|
||||
|
||||
/** Structured editor appearance for the bridge theme: SQL editor settings that
|
||||
* have no CSS-token carrier (the size is a number, the syntax theme an id).
|
||||
* Returns undefined unless every field is usable, so snapshots for hosts with
|
||||
* incomplete settings stay unchanged instead of carrying partial data. */
|
||||
export function buildPluginEditorAppearance(editor: { fontFamily?: string; fontSize?: number; theme?: string }): PluginEditorAppearance | undefined {
|
||||
const fontFamily = typeof editor.fontFamily === "string" && editor.fontFamily.trim() ? editor.fontFamily : undefined;
|
||||
const fontSize = typeof editor.fontSize === "number" && Number.isFinite(editor.fontSize) && editor.fontSize > 0 ? editor.fontSize : undefined;
|
||||
const theme = typeof editor.theme === "string" && editor.theme.trim() ? editor.theme : undefined;
|
||||
if (!fontFamily || fontSize === undefined || !theme) return undefined;
|
||||
return { fontFamily, fontSize, theme };
|
||||
}
|
||||
|
||||
// camelCase 字段 → DBX globals.css 的 CSS 令牌名。
|
||||
|
||||
@@ -490,6 +490,11 @@ describe("PluginHostBridge", () => {
|
||||
|
||||
bridge.updateTheme({ appearance: "light", tokens: {} });
|
||||
expect(messages[1]).toMatchObject({ type: "env", locale: "en", theme: { appearance: "light", tokens: {} } });
|
||||
|
||||
// The structured editor snapshot rides along on every theme push.
|
||||
const editor = { fontFamily: "Fira Code", fontSize: 13, theme: "one-dark" };
|
||||
bridge.updateTheme({ appearance: "light", tokens: {}, editor });
|
||||
expect(messages[2]).toMatchObject({ type: "env", locale: "en", theme: { appearance: "light", tokens: {}, editor } });
|
||||
});
|
||||
|
||||
it("routes host.saveFile transfers through the host save dialog and reports cancellation", async () => {
|
||||
|
||||
@@ -10,10 +10,21 @@ const MAX_BRIDGE_BINARY_BYTES = 8 * 1024 * 1024;
|
||||
// plugin iframe to disk and never traverse plugin frames.
|
||||
const MAX_BRIDGE_SAVE_BYTES = 512 * 1024 * 1024;
|
||||
|
||||
/** Structured editor appearance: SQL editor settings that have no CSS-token
|
||||
* carrier (font size is a number, the syntax theme an id). Font families are
|
||||
* additionally mirrored as `--font-sans` / `--font-mono` root tokens. */
|
||||
export interface PluginEditorAppearance {
|
||||
fontFamily: string;
|
||||
fontSize: number;
|
||||
theme: string;
|
||||
}
|
||||
|
||||
export interface PluginBridgeTheme {
|
||||
appearance: "light" | "dark";
|
||||
/** Resolved DBX design tokens (`--color-*`, `--radius-*`, ...) for the current theme. */
|
||||
/** Resolved DBX design tokens (`--color-*`, `--radius-*`, `--font-*`, ...) for the current theme. */
|
||||
tokens: Record<string, string>;
|
||||
/** Editor appearance snapshot. Optional: older in-flight snapshots omit it. */
|
||||
editor?: PluginEditorAppearance;
|
||||
}
|
||||
|
||||
export interface PluginWorkbenchContext {
|
||||
|
||||
@@ -40,6 +40,9 @@
|
||||
@theme inline {
|
||||
--font-sans: "Geist Variable", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Segoe UI", system-ui, sans-serif;
|
||||
--font-heading: var(--font-sans);
|
||||
/* Baseline mono token; App.vue overrides it inline with the editor font
|
||||
setting, and plugin sandboxes receive the resolved value via the bridge. */
|
||||
--font-mono: 'Fira Code', 'Cascadia Code', 'Cascadia Mono', 'JetBrains Mono', monospace;
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
|
||||
@@ -316,6 +316,24 @@ button {
|
||||
|
||||
使用 `window.addEventListener("dbx-plugin-env", handler)` 响应语言或主题变化。先读取 `dbxPlugin.locale`/`theme` 初始化,再在事件中刷新自己的状态。DBX 提供轻量 `dbx-btn`、`dbx-input` 等样式,但开发宿主不模拟完整组件 kit,自定义 token 样式在两者中更一致。
|
||||
|
||||
#### 主题负载携带什么
|
||||
|
||||
env 消息的 `theme` 结构为 `{ appearance, tokens, editor? }`。`appearance` 是 `"light" | "dark"`;`tokens` 是按前缀扫取的根节点设计变量(`--color-*`、`--radius-*`、`--font-*`);`editor` 是无 CSS 令牌载体的 SQL 编辑器设置结构化快照。投递矩阵:
|
||||
|
||||
| DBX 设置 | 投递形式 |
|
||||
| --- | --- |
|
||||
| 浅色/深色/跟随系统 | `theme.appearance` |
|
||||
| 调色板 + 自定义配色 | `--color-*` tokens |
|
||||
| 圆角风格 | `--radius-*` tokens |
|
||||
| UI 字体 | `--font-sans` token |
|
||||
| 编辑器(等宽)字体 | `--font-mono` token + `theme.editor.fontFamily` |
|
||||
| 编辑器字号 | `theme.editor.fontSize` |
|
||||
| SQL 编辑器语法主题 | `theme.editor.theme` |
|
||||
| UI 缩放(宿主窗口级) | 不投递——插件自管缩放 |
|
||||
| 数据网格字体/类型配色、编辑器背景图 | 不投递——DB 域(`--dbx-*` 不在扫取范围) |
|
||||
|
||||
字体"字号"类关注点归插件所有:宿主不规定你的字号;如适合你的 UI,可把 `theme.editor.fontSize` 当默认值消费。所有负载变化都经 env 通道实时推送——字体族变更走主题修订号,字号/语法主题走显式监听——无需刷新。
|
||||
|
||||
UI 必须构建为包内静态资源,不要把 Vite 开发服务器或 CDN 的脚本地址写入发行包。宿主把入口及资源转换成沙箱可加载内容;CSP 报错时检查构建产物是否存在、路径是否落在 `ui.root` 内,并使用匹配版本的 CLI 复现。用插件自己的对话框组件处理删除/重命名,不要依赖沙箱中的原生 `alert`/`confirm`/`prompt`。
|
||||
|
||||
## 原生 Sidecar 和 RPC
|
||||
|
||||
@@ -316,6 +316,24 @@ button {
|
||||
|
||||
Listen with `window.addEventListener("dbx-plugin-env", handler)` for locale/theme changes. Initialize from `dbxPlugin.locale`/`theme`, then refresh your state on events. DBX includes lightweight `dbx-btn` and `dbx-input` styles, but the development host does not emulate the complete component kit; custom token-based styles work more consistently in both.
|
||||
|
||||
#### What the theme payload carries
|
||||
|
||||
The env message's `theme` is `{ appearance, tokens, editor? }`. `appearance` is `"light" | "dark"`; `tokens` are the resolved root design variables swept by prefix (`--color-*`, `--radius-*`, `--font-*`); `editor` is a structured snapshot of the SQL editor settings that have no CSS-token carrier. Delivered settings:
|
||||
|
||||
| DBX setting | Delivered as |
|
||||
| --- | --- |
|
||||
| Light/dark/system mode | `theme.appearance` |
|
||||
| Palette + custom color sets | `--color-*` tokens |
|
||||
| Corner style | `--radius-*` tokens |
|
||||
| UI font family | `--font-sans` token |
|
||||
| Editor (mono) font family | `--font-mono` token + `theme.editor.fontFamily` |
|
||||
| Editor font size | `theme.editor.fontSize` |
|
||||
| SQL editor syntax theme | `theme.editor.theme` |
|
||||
| UI scale (host window zoom) | not delivered — plugins own their own zoom |
|
||||
| Data-grid font / type colors, editor background image | not delivered — DB-domain (`--dbx-*` tokens are excluded from the sweep) |
|
||||
|
||||
Treat `terminal.fontSize`-style concerns as plugin-owned: the host never dictates your font size; consume `theme.editor.fontSize` as a default only if it suits your UI. All payload changes push live on the env channel — font-family edits via the theme revision, size/theme via the explicit editor watcher — so no refresh is needed.
|
||||
|
||||
Build the UI as packaged static assets, not references to a Vite server or CDN scripts. The host converts the entry and assets into sandbox-loadable content. For CSP errors, verify build outputs and confinement under `ui.root`, and reproduce with the matching CLI version. Use plugin-owned dialogs for delete/rename rather than relying on native `alert`/`confirm`/`prompt` inside the sandbox.
|
||||
|
||||
## Native Sidecars and RPC
|
||||
|
||||
@@ -1,11 +1,41 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import { mkdir, writeFile, chmod } from "node:fs/promises";
|
||||
import { mkdir, writeFile, chmod, readFile } from "node:fs/promises";
|
||||
import { resolve, dirname } from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import { createMockHost } from "./server.mjs";
|
||||
import { Diagnostics } from "./diagnostics.mjs";
|
||||
import { stopProcessTree } from "./process-tree.mjs";
|
||||
|
||||
// A go.work `go` directive must not be older than any workspace module's own
|
||||
// directive, or go rejects the build outright ("requires go >= X, but go.work
|
||||
// lists go Y"). Derive the directive from the listed modules' go.mod files and
|
||||
// fall back to the historical base version when nothing is parseable.
|
||||
const GO_WORK_BASE_VERSION = "1.22";
|
||||
|
||||
function compareGoVersions(left, right) {
|
||||
const a = left.split(".").map(Number);
|
||||
const b = right.split(".").map(Number);
|
||||
for (let i = 0; i < Math.max(a.length, b.length); i += 1) {
|
||||
const delta = (a[i] || 0) - (b[i] || 0);
|
||||
if (delta !== 0) return delta;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
export async function resolveGoWorkVersion(directories) {
|
||||
let version = GO_WORK_BASE_VERSION;
|
||||
for (const directory of directories) {
|
||||
try {
|
||||
const goMod = await readFile(resolve(directory, "go.mod"), "utf8");
|
||||
const match = goMod.match(/^go\s+(\d+(?:\.\d+){1,2})/m);
|
||||
if (match && compareGoVersions(match[1], version) > 0) version = match[1];
|
||||
} catch {
|
||||
// No readable go.mod here; another workspace module decides the floor.
|
||||
}
|
||||
}
|
||||
return version;
|
||||
}
|
||||
|
||||
export async function startDevelopment(options) {
|
||||
const diagnostics = options.diagnostics || new Diagnostics();
|
||||
if (Number(process.versions.node.split(".")[0]) < 22) throw new Error("dbx-plugin dev requires Node.js 22+");
|
||||
@@ -24,7 +54,8 @@ export async function startDevelopment(options) {
|
||||
const env = { ...process.env };
|
||||
if (spec.goWorkspace) {
|
||||
const work = resolve(dataDir, "go.work");
|
||||
await writeFile(work, `go 1.22\n\nuse (\n${spec.goWorkspace.map((p) => "\t" + JSON.stringify(p)).join("\n")}\n)\n`, { mode: 0o600 });
|
||||
const goVersion = await resolveGoWorkVersion(spec.goWorkspace);
|
||||
await writeFile(work, `go ${goVersion}\n\nuse (\n${spec.goWorkspace.map((p) => "\t" + JSON.stringify(p)).join("\n")}\n)\n`, { mode: 0o600 });
|
||||
env.GOWORK = work;
|
||||
}
|
||||
let command = spec.command === "node" ? process.execPath : spec.command;
|
||||
|
||||
@@ -4,7 +4,7 @@ import { mkdtemp, mkdir, writeFile, readFile, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { setTimeout as delay } from "node:timers/promises";
|
||||
import { startDevelopment } from "../runtime.mjs";
|
||||
import { startDevelopment, resolveGoWorkVersion } from "../runtime.mjs";
|
||||
import { Diagnostics } from "../diagnostics.mjs";
|
||||
|
||||
async function project(t) {
|
||||
@@ -27,6 +27,33 @@ async function project(t) {
|
||||
return { project: root, port: 0, shellHtml: join(root, "ui/index.html"), diagnostics: new Diagnostics(() => {}) };
|
||||
}
|
||||
|
||||
test("go.work go directive tracks the newest module requirement", async (t) => {
|
||||
const root = await mkdtemp(join(tmpdir(), "dbx-dev-gowork-"));
|
||||
t.after(() => rm(root, { recursive: true, force: true }));
|
||||
await mkdir(join(root, "newer"), { recursive: true });
|
||||
await mkdir(join(root, "older"), { recursive: true });
|
||||
await writeFile(join(root, "newer/go.mod"), "module example.com/newer\n\ngo 1.25.0\n");
|
||||
await writeFile(join(root, "older/go.mod"), "module example.com/older\n\ngo 1.21\n");
|
||||
assert.equal(await resolveGoWorkVersion([join(root, "newer"), join(root, "older")]), "1.25.0");
|
||||
// No readable go.mod: fall back to the historical base version.
|
||||
assert.equal(await resolveGoWorkVersion([join(root, "missing")]), "1.22");
|
||||
});
|
||||
|
||||
test("backend build go.work is written with the module go directive", async (t) => {
|
||||
const options = await project(t);
|
||||
const moduleDirectory = join(options.project, "go-sdk");
|
||||
await mkdir(moduleDirectory, { recursive: true });
|
||||
await writeFile(join(moduleDirectory, "go.mod"), "module example.com/go-sdk\n\ngo 1.25.0\n");
|
||||
const host = await startDevelopment({
|
||||
...options,
|
||||
commands: { backend: { command: process.execPath, args: ["-e", "process.exit(0)"], goWorkspace: [moduleDirectory] } },
|
||||
});
|
||||
t.after(() => host.close());
|
||||
const work = await readFile(join(options.project, ".dbx-dev", "go.work"), "utf8");
|
||||
assert.match(work, /^go 1\.25\.0$/m);
|
||||
assert.ok(work.includes(JSON.stringify(moduleDirectory)), "workspace lists the SDK module");
|
||||
});
|
||||
|
||||
test("failed build does not start a host or retain signal listeners", async (t) => {
|
||||
const options = await project(t);
|
||||
const before = process.listenerCount("SIGTERM");
|
||||
|
||||
@@ -104,6 +104,8 @@ const themes = () => ({
|
||||
"--color-primary-foreground": "#18181b",
|
||||
"--color-destructive": "#f3625f",
|
||||
"--color-destructive-foreground": "#18181b",
|
||||
"--font-sans": '-apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif',
|
||||
"--font-mono": "'Fira Code', 'Cascadia Code', 'Cascadia Mono', 'JetBrains Mono', monospace",
|
||||
"--radius-md": "6px",
|
||||
"--radius-lg": "8px",
|
||||
}
|
||||
@@ -121,6 +123,8 @@ const themes = () => ({
|
||||
"--color-primary-foreground": "#ffffff",
|
||||
"--color-destructive": "#e7000b",
|
||||
"--color-destructive-foreground": "#ffffff",
|
||||
"--font-sans": '-apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif',
|
||||
"--font-mono": "'Fira Code', 'Cascadia Code', 'Cascadia Mono', 'JetBrains Mono', monospace",
|
||||
"--radius-md": "6px",
|
||||
"--radius-lg": "8px",
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user