Official
settings
Built-in settings UI (JavaScript UI plugin).
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/settings@0.2.0
Permissions in 0.2.0
Files
views/layout.ts4.5 KB
// The page's building blocks: sections, cards, settings rows and the small
// controls (a segmented picker, a preset limit) every section shares.
// HTM whitespace is significant; keep these templates out of Prettier's HTML formatter.
import { html as htm } from "convergence";
import type { Element } from "convergence";
export const READING_WIDTH = 760;
// Layout for the templates below: a flex column, a row that centres its
// children across, a child that takes the free space (and may shrink below
// its content), the whole of the parent, and a column of the reading width
// centred in its parent.
export const column = { display: "flex", flexDirection: "column" } as const;
export const row = { display: "flex", flexDirection: "row", alignItems: "center" } as const;
export const fill = { flexGrow: 1, flexShrink: 1, flexBasis: "0%", minWidth: 0, minHeight: 0 } as const;
export const full = { width: "100%", height: "100%" } as const;
export const reading = { maxWidth: READING_WIDTH, width: "100%", marginX: "auto" } as const;
// The colour of a risk dot, from its tone.
export const DOT = { muted: "mutedForeground", warning: "warning", danger: "danger" } as const;
// A section: its title and one line of help in the type of the rows below
// (the kernel's setting heading), then its card.
export function section(title: string, hint: string | null, card: Element) {
return htm`
<div ...${column} gap=${0} padding="small">
<setting heading title=${title} description=${hint ?? undefined} />
${card}
</div>`;
}
// A card of rows, a line between them: the kernel's card.
export function card(rows: ReadonlyArray<Element | null>) {
return htm`<card divided>${rows.filter(Boolean)}</card>`;
}
export interface SettingParts {
title: string;
description?: string | null;
control?: Element | null;
wide?: boolean;
}
// One setting: what it is on the left, what it is set to on the right
// (the kernel's setting; a wide control is a text field's 260px).
export function setting({ title, description, control, wide }: SettingParts) {
return htm`<setting title=${title} description=${description ?? undefined} wide=${wide}>${control ?? null}</setting>`;
}
// A row of a list inside a card: a switch or a mark on the left, then
// what the row is about.
export const listRow = (...children: Element[]) =>
htm`<div ...${row} gap="medium" padding="small" paddingX="medium" paddingY="medium" alignItems="start">${children}</div>`;
/// The height of a list row's title line. A row's leading switch and
/// trailing button sit in a box of this height, so they centre on the
/// title rather than on the top of the row.
export const TITLE_LINE = 24;
/// A switch or button beside a list row's title line, centred on it.
export const besideTitle = (control: Element | null, width?: number) =>
htm`<div ...${row} height=${TITLE_LINE} width=${width} flexShrink=${0} justifyContent="center">${control}</div>`;
// Three choices are a segmented control, not a dropdown; the kernel draws it.
export function segmented<V extends string>(
id: string,
value: V,
choices: ReadonlyArray<{ value: V; label: string }>,
onChange: (value: V) => void,
) {
return htm`<tabs id=${id} segmented active=${value} tabs=${choices.map((c) => ({ id: c.value, label: c.label }))}
onChange=${(e: { value: string }) => onChange(e.value as V)} />`;
}
// A number setting picked from presets, where `null` means no limit. A
// value set by hand in settings.json is offered too, so it shows.
export function limit(
id: string,
value: number | null,
presets: readonly number[],
label: (value: number) => string,
unlimited: string,
onChange: (value: number | null) => void,
) {
const values = value == null || presets.includes(value) ? presets : [...presets, value].sort((a, b) => a - b);
return htm`
<select id=${id} value=${value == null ? "" : String(value)} small
options=${[...values.map((v) => ({ value: String(v), label: label(v) })), { value: "", label: unlimited }]}
onChange=${(e: { value: string }) => onChange(e.value === "" ? null : Number(e.value))} />`;
}
export function pluginChip(name: string) {
return htm`
<div ...${column} gap="small" padding="small" paddingX="small" background="subtle" borderRadius="sm">
<text style="body" size="xs" tone="muted">${name}</text>
</div>`;
}
export function notes(text: string | null) {
return text
? htm`<div ...${column} gap="small" padding="small" paddingX="medium" paddingY="small"><markdown>${text}</markdown></div>`
: null;
}Versions
| Version | Published | Plugin API | Size | Permissions | Status |
|---|---|---|---|---|---|
| 0.2.0latest | Oct 5, 2026 | >=2 <3 | 24.7 KB | 7 permissions | Listed |
No comments yet.