Teaches the enum-based preset category file pattern used in split-file Companion modules. Use when asked to create preset file, add preset category, add preset category file, wire presets, extend presets.ts aggregator, or use enum-based preset IDs in a src/presets/preset-{category}.ts file.
This module splits preset definitions across many files (one per category), then aggregates them in presets.ts. This skill documents the exact structure and wiring required to add a new preset category using the enum-based preset ID pattern — mirroring how action category files work.
src/presets/preset-*.ts filesrc/presets/preset-{category}.ts file from scratchpresets.ts aggregator for the first timesrc/presets/preset-{category}.tsThe rule of thumb: If a file for your category already exists → edit it directly. If no file exists for your category → use this skill to create one and wire it up.
src/
presets.ts ← aggregator (imports + combines all categories)
presets/
preset-{category-a}.ts ← one file per preset category
preset-{category-b}.ts
preset-utils.ts ← CompanionPresetExt / CompanionPresetDefinitionsExt types
index.ts calls:
this.setPresetDefinitions(GetPresetList(this))
GetPresetList() (in presets.ts) calls each category's GetPresets{Category}(), stores the typed result in a local variable, spreads all results into one combined object, and returns it.
import { CompanionPresetExt, btn, colorWhite, colorBlack, colorLightGray } from './preset-utils.js'
import { ActionId{RelatedCategory} } from '../actions/action-{related-category}.js'
Always use
.jsextensions on relative imports — this is ESM. Importbtnfrompreset-utils.jsto use the shared button helper for simple presets. Import color constants when defining a button inline.
Every preset file exports an enum that names all its presets. This enum is the key type for the return object and is re-exported to the aggregator.
export enum PresetIdMyCategory {
myPreset = 'myCategory_myPreset',
anotherPreset = 'myCategory_anotherPreset',
}
Convention: enum member names are camelCase; string values follow
'{category}_{presetName}'to namespace IDs and avoid collisions across categories.
Without instance (static presets — most categories):
export function GetPresetsMyCategory(): {
[id in PresetIdMyCategory]: CompanionPresetExt | undefined
} {
const presets: { [id in PresetIdMyCategory]: CompanionPresetExt | undefined } = {
[PresetIdMyCategory.myPreset]: {
type: 'button',
category: 'My Category',
name: 'My Preset',
style: {
text: 'Button Label',
size: '14',
color: colorBlack,
bgcolor: colorLightGray,
},
steps: [
{
down: [
{
actionId: ActionIdMyCategory.someAction,
options: {},
},
],
up: [],
},
],
feedbacks: [],
},
[PresetIdMyCategory.anotherPreset]: {
// ...
},
}
return presets
}
With instance (when presets need dynamic data — participant lists, config values):
import { InstanceBaseExt } from '../utils.js'
import { ZoomConfig } from '../config.js'
export function GetPresetsMyCategory(instance: InstanceBaseExt<ZoomConfig>): {
[id in PresetIdMyCategory]: CompanionPresetExt | undefined
} {
const presets: { [id in PresetIdMyCategory]: CompanionPresetExt | undefined } = {
[PresetIdMyCategory.myPreset]: {
type: 'button',
category: 'My Category',
name: instance.config.someConfigValue ?? 'Default Name',
// ...
},
}
return presets
}
Use the instance form only when required — preset-recording.ts is a no-instance example; preset-participants.ts is an instance example.
Why enum over plain string keys?
- TypeScript catches duplicate IDs at compile time — two enum members cannot have the same string value without a type error
- Rename-refactoring works across the whole codebase via IDE tooling
- The aggregator's union type enforces that every enum member is covered in the combined object
actionIdreferences inside steps are already typed enums — preset IDs should follow the same discipline
presets.ts)presets.ts has three responsibilities:
import { PresetIdMyCategory, GetPresetsMyCategory } from './presets/preset-my-category.js'
Inside GetPresetList(), alongside the other factory calls:
// no-instance form:
const presetsMyCategory: { [id in PresetIdMyCategory]: CompanionPresetExt | undefined } = GetPresetsMyCategory()
// instance form:
const presetsMyCategory: { [id in PresetIdMyCategory]: CompanionPresetExt | undefined } = GetPresetsMyCategory(instance)
presets objectThe presets const has a mapped type whose key is a union of all category enums. Add the new enum to the union and its spread to the object:
const presets: {
[id in PresetIdCategoryOne | PresetIdCategoryTwo | PresetIdMyCategory]: CompanionPresetExt | undefined // ← add new enum here
} = {
...presetsCategoryOne,
...presetsCategoryTwo,
...presetsMyCategory, // ← add new spread here
}
return presets as CompanionPresetDefinitions
Why the union type is required: Unlike the old
[id: string]index, the mapped type gives TypeScript visibility of every preset ID across the module. Adding an enum to the union without spreading (or vice versa) is a compile-time error.
as CompanionPresetDefinitionscast: The mapped type is more specific thanCompanionPresetDefinitions— the cast is needed on the return value. This is identical to howactions.tsreturnsactionstyped asCompanionActionDefinitions.
src/presets/preset-{category}.ts
Use the template below (copy verbatim, replace all {placeholders}).
import { CompanionPresetExt, btn, colorWhite, colorBlack } from './preset-utils.js'
import { ActionId{RelatedCategory} } from '../actions/action-{related-category}.js'
export enum PresetId{Category} {
firstPreset = '{category}_firstPreset',
secondPreset = '{category}_secondPreset',
}
export function GetPresets{Category}(): {
[id in PresetId{Category}]: CompanionPresetExt | undefined
} {
const presets: { [id in PresetId{Category}]: CompanionPresetExt | undefined } = {
// Simple preset — use the btn() helper
[PresetId{Category}.firstPreset]: btn(
'First Preset',
'{Category Display Name}',
ActionId{RelatedCategory}.someAction,
{ targetType: 'roomIndex', roomIndex: 1 },
),
// Inline preset — use when you need size '18', feedbacks, or multiple steps
[PresetId{Category}.secondPreset]: {
type: 'button',
category: '{Category Display Name}',
name: 'Second Preset',
style: {
text: 'Second Preset',
size: '18',
color: colorWhite,
bgcolor: colorBlack,
},
steps: [
{
down: [
{
actionId: ActionId{RelatedCategory}.anotherAction,
options: {},
},
],
up: [],
},
],
feedbacks: [],
},
}
return presets
}
presets.tsAdd at the top of presets.ts alongside the other imports:
import { PresetId{Category}, GetPresets{Category} } from './presets/preset-{category}.js'
GetPresetList()Add inside GetPresetList(), alongside the other factory calls:
const presets{Category}: { [id in PresetId{Category}]: CompanionPresetExt | undefined } =
GetPresets{Category}()
// or: GetPresets{Category}(instance) ← if the factory needs instance
presetsIn the presets const, add PresetId{Category} to the union and the spread to the object:
const presets: {
[id in
| /* ... existing enums ... */
| PresetId{Category} // ← add here
]: CompanionPresetExt | undefined
} = {
/* ... existing spreads ... */
...presets{Category}, // ← add here
}
yarn build
# or: npm run build
Zero TypeScript errors means your new file is properly typed and wired.
Full annotated reference for a CompanionPresetExt entry:
[PresetIdMyCategory.myPreset]: {
type: 'button', // always 'button' for button presets
category: 'My Category', // groups presets in Companion UI by this label
name: 'My Preset', // display name shown in the preset picker
style: {
text: 'Button Label', // text rendered on the button face
size: '14', // font size as string; use '18' for primary actions
color: colorBlack, // foreground color — import from './preset-utils.js'
bgcolor: colorLightGray, // background color — import from './preset-utils.js'
},
steps: [
{
down: [
{
actionId: ActionIdMyCategory.someAction, // typed ActionId* enum value
options: {}, // action options object
},
],
up: [], // actions fired on button release (usually empty)
},
],
feedbacks: [], // feedback definitions ([] if none needed)
}
Shorthand: use btn() from preset-utils.js for simple presets (size '14', no feedbacks):
[PresetIdMyCategory.myPreset]: btn(
'Button Label',
'My Category',
ActionIdMyCategory.someAction,
{ targetType: 'roomIndex', roomIndex: 1 },
),
All color constants live in src/presets/preset-utils.ts and are imported via './preset-utils.js'.
Currently defined:
| Constant | Hex value | Typical use |
|---|---|---|
colorBlack | 0x000000 | Text color |
colorWhite | 0xffffff | Text color on dark |
colorLightGray | 0xaaaaaa | Button background |
If the color you need does not exist, add it to
src/presets/preset-utils.tsas a new exportedconstbefore using it. Never use raw hex literals in preset files.
| Mistake | Fix |
|---|---|
| Enum string value duplicates an existing preset ID | Check all other PresetId* enums — IDs must be globally unique; namespacing via {category}_ prefix prevents this |
Added spread but forgot to add enum to union type in presets.ts | TypeScript will error — add the enum to the [id in ...] union |
| Added enum to union but forgot to spread the result | TypeScript will error — spread the factory result into the presets object |
Forgot .js extension on import in presets.ts | This is ESM — always use .js extension on relative imports |
Using a string literal for actionId inside steps | Use the typed ActionId* enum value — TypeScript cannot catch typos in raw strings |
| Using a raw hex literal for a color | Add a named constant to preset-utils.ts and import it — never inline 0xffffff or 16777215 |
| Importing color constants from the wrong module | Import from './preset-utils.js', not from '../utils.js' |
Factory returns CompanionPresetDefinitionsExt (old pattern) | Return the enum-mapped type { [id in PresetId{Category}]: CompanionPresetExt | undefined } |
Forgot as CompanionPresetDefinitions cast on return presets | The mapped union type is more specific — cast is required to satisfy the return type |
src/presets.ts — the aggregator (authoritative wiring pattern, mirrors actions.ts)src/actions.ts — the action aggregator this pattern exactly mirrorssrc/presets/preset-join-flow.ts — no-instance preset category file examplesrc/presets/preset-utils.ts — CompanionPresetExt type, colorWhite/colorBlack constants, btn() helpercompanion-action-file-pattern — the parallel pattern this skill mirrorscompanion-add-preset-to-category-file — use when category file already exists