Modules
About 785 wordsAbout 3 min
2026-03-28
Modules are the visual building blocks of CeeGee overlays. Each module is a Vue component paired with a manifest that describes its configuration, actions, and animations.
Module structure
All modules live in packages/modules/src/. Each module has its own directory:
packages/modules/src/
├── index.ts # Exports all manifests (server-safe)
├── registry.ts # Maps module keys to Vue components + manifests
├── lower-third/
│ └── basic/
│ ├── manifest.ts # ModuleManifest
│ └── LowerThirdBasic.vue # Vue component
├── bug/basic/
├── billboard/basic/
├── clock/basic/
└── countdown/basic/Step 1: Create the manifest
The manifest describes what the module is, how it is configured, and what it can do.
// packages/modules/src/my-module/basic/manifest.ts
import type { ModuleManifest, JsonSchemaLike } from 'engine-core';
export const myModuleManifest: ModuleManifest = {
id: 'my-module.basic', // stable key: category.variant
label: 'My Module',
version: '1.0.0',
category: 'my-module',
configSchema: {
type: 'object',
properties: {
text: { type: 'string', title: 'Display Text' },
color: { type: 'string', title: 'Color', default: '#ffffff' },
},
required: ['text'],
} satisfies JsonSchemaLike,
dataSchema: {
type: 'object',
properties: {
text: { type: 'string' },
},
required: ['text'],
} satisfies JsonSchemaLike,
actions: [
{ id: 'show', label: 'Show' },
{ id: 'hide', label: 'Hide' },
],
animationHooks: {
enter: 'fadeIn',
exit: 'fadeOut',
},
capabilities: {
supportsLayerRegions: false,
supportsMultipleInstancesPerLayer: true,
},
};Manifest fields
| Field | Description |
|---|---|
id | Stable key in category.variant format (e.g., lower-third.basic). Stored as module_key in the database. |
label | Human-readable name shown in the UI. |
version | Semver string. Updated manifests are upserted on startup. |
category | Groups modules in the UI (e.g., lower-third, bug, billboard). |
configSchema | JSON Schema defining the element configuration fields. The Producer UI generates forms from this. |
dataSchema | JSON Schema describing dynamic data fields. In MVP these are part of config; in v1.1+ they can be bound to external datasources. |
actions | Array of { id, label } for module-specific controls (e.g., show, hide, emphasize, start, stop, reset). |
animationHooks | Named GSAP animation hooks: enter, exit, emphasize. |
capabilities | Optional flags for layer region support and multi-instance behavior. |
Step 2: Create the Vue component
The component receives ModuleComponentProps and is responsible for rendering the overlay graphic and driving GSAP animations.
<!-- packages/modules/src/my-module/basic/MyModule.vue -->
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import gsap from 'gsap';
import type { ModuleComponentProps } from 'engine-core';
const props = defineProps<ModuleComponentProps>();
const config = computed(() => props.config as { text: string; color: string });
const rootEl = ref<HTMLElement | null>(null);
// Enter animation
const playEnter = () => {
if (!rootEl.value) return;
gsap.fromTo(rootEl.value,
{ opacity: 0 },
{ opacity: 1, duration: 0.4, ease: 'power2.out' }
);
};
// Exit animation
const playExit = () => {
if (!rootEl.value) return;
gsap.to(rootEl.value, {
opacity: 0, duration: 0.3, ease: 'power2.in',
});
};
// React to visibility changes from the engine
watch(
() => props.runtimeState.visibility,
(vis) => {
if (vis === 'entering' || vis === 'visible') playEnter();
else if (vis === 'exiting' || vis === 'hidden') playExit();
},
{ immediate: true },
);
// React to module actions (e.g., emphasize)
watch(
() => props.runtimeState.runtimeData,
(rd) => {
if (rd && (rd as any).lastAction?.actionId === 'emphasize') {
// play emphasis animation
}
},
);
</script>
<template>
<div ref="rootEl" class="my-module">
<span :style="{ color: config.color }">{{ config.text }}</span>
</div>
</template>
<style scoped>
.my-module {
position: absolute;
/* position and size using CSS vars and relative units */
}
</style>Component props
The ModuleComponentProps type provides everything the component needs:
| Prop | Type | Description |
|---|---|---|
workspace | Workspace | Current workspace (display config, theme tokens) |
channel | Channel | Current channel |
layer | Layer | Layer this element belongs to (z-index, region) |
element | Element | Element metadata (name, sort order) |
config | unknown | Element config JSON (cast to your module's config type) |
runtimeState | ElementRuntimeState | Visibility and runtime data |
Styling guidelines
- Use scoped CSS and CSS variables for theming. Workspace theme tokens are injected as CSS vars on the overlay root.
- Use relative units (
%,vw,vh,rem) for layout. The base resolution is 1920x1080 but overlays should be resolution-flexible. - Do not use Tailwind CSS in module components. Tailwind is reserved for the control UI.
Step 3: Register the module
Add to the manifest index
// packages/modules/src/index.ts
import { myModuleManifest } from './my-module/basic/manifest';
export const allManifests: ModuleManifest[] = [
// ... existing manifests
myModuleManifest,
];Add to the component registry
// packages/modules/src/registry.ts
export const moduleComponents: Record<string, () => Promise<Component>> = {
// ... existing entries
'my-module.basic': () => import('./my-module/basic/MyModule.vue'),
};
export const moduleManifests: Record<string, ModuleManifest> = {
// ... existing entries
'my-module.basic': myModuleManifest,
};Auto-registration
On engine startup, the Nitro plugin server/plugins/register-modules.ts imports allManifests and upserts each into the modules database table. No manual database inserts needed -- just add the manifest and restart.
Built-in modules
| Module Key | Category | Description |
|---|---|---|
lower-third.basic | lower-third | Name/title/role overlay with slide animations |
bug.basic | bug | Corner brand bug (logo/icon) |
billboard.basic | billboard | Full-width text display |
clock.basic | clock | Real-time clock |
countdown.basic | countdown | Countdown timer with start/stop/reset actions |
Animation pattern
All modules follow the same pattern for GSAP animations:
- Watch
runtimeState.visibilityfor enter/exit triggers. - Watch
runtimeState.runtimeData.lastActionfor action-triggered animations. - Use GSAP timelines for coordinated multi-step animations.
- Kill conflicting timelines before starting new ones (e.g., kill the enter timeline before playing exit).