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

  • Change app settings settings.writeHighChanges the app settings.Apply the chosen theme to the window

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

VersionPublishedPlugin APISizePermissionsStatus
0.3.0latestOct 5, 2026>=2 <37.9 KB1 permissionListed

Reviews and comments

0 threads · 0 reviews

No comments yet.