Official
themes
The stock themes: presets of every colour, type size, corner and density token, chosen per light and dark mode and edited in Settings.
The app opens the listing; nothing installs until an agent in your Plugins workspace has read the files and you enable the plugin. In a terminal: cvg install convergence/themes@0.3.0
Permissions in 0.3.0
Files
presets.ts8.6 KB
// The stock presets and the rules that turn the settings into the two token
// sets the kernel draws with. Pure: no plugin API here, so the tests and the
// settings view share it.
import type { ThemeTokens } from "convergence";
import { CONVERGENCE_DARK, CONVERGENCE_LIGHT } from "./convergence.ts";
/** A preset: one mode, a name, and every token it sets. */
export interface Preset {
readonly id: string;
readonly name: string;
readonly mode: "light" | "dark";
readonly stock?: boolean;
readonly tokens: ThemeTokens;
}
/** The defaults the kernel keeps for anything a preset leaves out (`crates/kernel/src/tokens.rs`). */
export const DEFAULT_SIZES = {
type: {
baseSize: 15,
textSize: 12.5,
labelSize: 11.72,
labelLine: 16.4,
sublabelSize: 9.4,
sublabelLine: 12,
strongSize: 13,
treeSize: 13,
descriptionSize: 11.25,
monoSize: 13,
},
shape: { radius: 9, radiusLarge: 11, cardRadius: 13, fieldRadius: 8, paneRadius: 18, treeRowRadius: 6 },
density: {
fieldHeight: 28,
fieldInsetX: 10,
rowMinHeight: 28,
rowPaddingX: 9,
rowPaddingY: 6,
rowLead: 17,
rowLeadGap: 7,
settingPadding: 10,
treeRowHeight: 26,
treeLevel: 14,
},
} as const;
const preset = (id: string, name: string, mode: "light" | "dark", tokens: ThemeTokens): Preset => ({
id,
name,
mode,
stock: true,
tokens: { ...tokens, name, mode },
});
// VS Code's Modern themes: flat greys, the blue accent, square-ish corners,
// 13px type and tight rows.
const VSCODE_SIZES: ThemeTokens = {
groundAlpha: 1,
type: {
baseSize: 14,
textSize: 13,
labelSize: 13,
labelLine: 18,
sublabelSize: 11.5,
sublabelLine: 15,
strongSize: 13,
descriptionSize: 12,
treeSize: 13,
},
// A card's corner is its rows' plus its 4px inset, so the two share a centre.
shape: { radius: 4, radiusLarge: 6, cardRadius: 8, fieldRadius: 4, paneRadius: 0, treeRowRadius: 0 },
density: {
fieldHeight: 28,
fieldInsetX: 8,
rowMinHeight: 28,
rowPaddingX: 8,
rowPaddingY: 5,
rowLead: 16,
rowLeadGap: 7,
settingPadding: 12,
treeRowHeight: 22,
treeLevel: 8,
},
};
// The Convergence colours a little smaller and tighter.
const COMPACT_SIZES: ThemeTokens = {
type: {
baseSize: 13.5,
textSize: 11.5,
labelSize: 11,
labelLine: 15,
sublabelSize: 9,
sublabelLine: 11,
strongSize: 12,
treeSize: 11.5,
},
shape: { radius: 7, radiusLarge: 9, fieldRadius: 6, paneRadius: 12, treeRowRadius: 5 },
density: {
fieldHeight: 24,
fieldInsetX: 8,
rowMinHeight: 24,
rowPaddingX: 8,
rowPaddingY: 3,
rowLead: 15,
rowLeadGap: 6,
treeRowHeight: 22,
treeLevel: 12,
},
};
export const STOCK: readonly Preset[] = [
preset("convergence-dark", "Divergence Dark", "dark", { colors: CONVERGENCE_DARK }),
preset("convergence-light", "Divergence Light", "light", { colors: CONVERGENCE_LIGHT }),
preset("vscode-dark", "VS Code Dark Modern", "dark", {
...VSCODE_SIZES,
colors: {
...CONVERGENCE_DARK,
background: "#1f1f1f",
foreground: "#cccccc",
border: "#2b2b2b",
"input.border": "#3c3c3c",
// Fields and dropdowns (input.background); cards are `secondary`.
"muted.background": "#313131",
"muted.foreground": "#9d9d9d",
"secondary.background": "#252526",
"secondary.hover.background": "#2a2d2e",
"popover.background": "#1f1f1f",
"list.background": "#181818",
"list.hover.background": "#2a2d2e",
"list.active.background": "#04395e",
"list.active.border": "#0078d4",
"accent.background": "#04395e",
"accent.foreground": "#ffffff",
"sidebar.background": "#181818",
"sidebar.border": "#2b2b2b",
"sidebar.accent.background": "#04395e",
link: "#4daafc",
"switch.background": "#0078d4",
"title_bar.background": "#181818",
"title_bar.border": "#2b2b2b",
"status_bar.background": "#181818",
"window.border": "#2b2b2b",
"tab.active.background": "#1f1f1f",
"tab_bar.segmented.background": "#181818",
ring: "#0078d4",
"primary.background": "#0078d4",
"primary.foreground": "#ffffff",
"primary.hover.background": "#026ec1",
"selection.background": "#264f78",
},
}),
preset("vscode-light", "VS Code Light Modern", "light", {
...VSCODE_SIZES,
colors: {
...CONVERGENCE_LIGHT,
background: "#ffffff",
foreground: "#3b3b3b",
border: "#e5e5e5",
"input.border": "#cecece",
"muted.background": "#f2f2f2",
"muted.foreground": "#616161",
"secondary.background": "#f8f8f8",
"secondary.hover.background": "#f2f2f2",
"popover.background": "#ffffff",
"list.background": "#f8f8f8",
"list.hover.background": "#e8e8e8",
"list.active.background": "#e4e6f1",
"accent.background": "#e8e8e8",
"sidebar.background": "#f8f8f8",
"sidebar.border": "#e5e5e5",
"sidebar.accent.background": "#e4e6f1",
"title_bar.background": "#f8f8f8",
"title_bar.border": "#e5e5e5",
"status_bar.background": "#f8f8f8",
"window.border": "#e5e5e5",
"tab.active.background": "#ffffff",
"tab_bar.segmented.background": "#f8f8f8",
ring: "#005fb8",
"primary.background": "#005fb8",
"primary.foreground": "#ffffff",
"primary.hover.background": "#0258a8",
"selection.background": "#add6ff",
},
}),
preset("high-contrast-dark", "High Contrast Dark", "dark", {
groundAlpha: 1,
colors: {
...CONVERGENCE_DARK,
background: "#000000",
foreground: "#ffffff",
border: "#6fc3df",
"input.border": "#6fc3df",
"muted.background": "#0c0c0c",
"muted.foreground": "#ffffff",
"secondary.background": "#000000",
"popover.background": "#000000",
"list.background": "#000000",
"list.hover.background": "#0f4a85",
"list.active.background": "#063b72",
"accent.background": "#0f4a85",
"sidebar.background": "#000000",
"sidebar.border": "#6fc3df",
"title_bar.background": "#000000",
"status_bar.background": "#000000",
"window.border": "#6fc3df",
ring: "#f38518",
"primary.background": "#ffffff",
"primary.foreground": "#000000",
},
}),
preset("high-contrast-light", "High Contrast Light", "light", {
groundAlpha: 1,
colors: {
...CONVERGENCE_LIGHT,
background: "#ffffff",
foreground: "#000000",
border: "#0f4a85",
"input.border": "#0f4a85",
"muted.background": "#f2f2f2",
"muted.foreground": "#000000",
"secondary.background": "#ffffff",
"popover.background": "#ffffff",
"list.background": "#ffffff",
"list.hover.background": "#9cc5f0",
"list.active.background": "#cfe3f8",
"accent.background": "#9cc5f0",
"sidebar.background": "#ffffff",
"sidebar.border": "#0f4a85",
"title_bar.background": "#ffffff",
"status_bar.background": "#ffffff",
"window.border": "#0f4a85",
ring: "#b5200d",
"primary.background": "#000000",
"primary.foreground": "#ffffff",
},
}),
preset("compact-dark", "Compact Dark", "dark", { ...COMPACT_SIZES, colors: CONVERGENCE_DARK }),
preset("compact-light", "Compact Light", "light", { ...COMPACT_SIZES, colors: CONVERGENCE_LIGHT }),
];
/** The settings the plugin keeps (`contributes.settings`): user presets by id, and the preset of each mode. */
export interface ThemeSettings {
readonly presets: Readonly<Record<string, Omit<Preset, "id" | "stock">>>;
readonly active: { readonly light?: string; readonly dark?: string };
}
/** Every preset: the stock ones, then the user's. */
export function allPresets(settings: ThemeSettings): Preset[] {
const own = Object.entries(settings.presets).map(([id, preset]) => ({ ...preset, id }));
return [...STOCK, ...own];
}
/** The preset of `mode` the user chose, else the Convergence one. */
export function chosen(settings: ThemeSettings, mode: "light" | "dark"): Preset {
const id = settings.active[mode];
const presets = allPresets(settings).filter((preset) => preset.mode === mode);
return presets.find((preset) => preset.id === id) ?? presets.find((preset) => preset.stock) ?? STOCK[0]!;
}
/** A copy of `source` as a new user preset. */
export function duplicate(source: Preset, name: string, existing: readonly string[]): Preset {
const base =
name
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-|-$/g, "") || "theme";
let id = base;
for (let n = 2; existing.includes(id) || STOCK.some((preset) => preset.id === id); n++) id = `${base}-${n}`;
return { id, name, mode: source.mode, tokens: { ...structuredClone(source.tokens), name, mode: source.mode } };
}Versions
| Version | Published | Plugin API | Size | Permissions | Status |
|---|---|---|---|---|---|
| 0.3.0latest | Oct 5, 2026 | >=2 <3 | 7.9 KB | 1 permission | Listed |
No comments yet.