Plugin Development Guide
Panels supports custom plugins written in Kotlin to extend its functionality. Here's a guide to help you understand how plugins are structured, how they work, and how to get started building your own.
You can find full working examples in the ftcontrol/plugins/example folder.
Defining a Plugin
To create a plugin, you define a config class and a plugin class:
open class MyConfig : BasePluginConfig() {
open var test = "test"
}
class MyClass : PanelsPlugin<MyConfig>(MyConfig()) {
// Plugin logic here
}
About open
and Configs
- Plugins require a configuration class that extends
BasePluginConfig
. open
variables can be overridden later by users (i.e., teams) who use your plugin.- This allows plugins to be customizable per team without changing the plugin's code.
Team-Specific Plugin Configurations
Teams using your plugin can customize it by overriding the config:
class MyConfig : MyConfig() {
override var isDev = true
override var test = "custom value"
}
isDev
is a built-in field inBasePluginConfig
, useful for toggling development-only features.
Global Variables
Plugins can expose reactive global variables, which can be displayed dynamically in the UI:
override val globalVariables = mutableMapOf<String, () -> Any>(
"test" to { 6 },
"timestamp" to { System.currentTimeMillis() }
)
- Use
dynamic("key")
in HTML templates to bind these values.
Plugin Actions
You can define actions (e.g., for buttons):
override val actions = mutableMapOf<String, () -> Unit>(
"test" to { println("DASH: TEST ACTION") }
)
- Actions are triggered from UI elements like
button(action = "test") { text("Run Action") }
.
Plugin Identity
Every plugin must define a unique ID and a name:
override var id: String = "com.bylazar.myplugin"
override val name: String = "Lazar's Example Plugin"
- The
id
should be unique to avoid conflicts.
Plugin Lifecycle Hooks
Plugins can respond to lifecycle events:
override fun onEnable() {
// Called when Panels is enabled
}
override fun onDisable() {
// Called when Panels is disabled
}
override fun onAttachEventLoop(eventLoop: FtcEventLoop) {
// Called when FTC SDK's event loop is available
}
override fun onRegister(context: ModContext) {
// Called once during initialization
}
ModContext
is currently empty but planned for future extensibility.
Creating Pages
You can create custom UI pages using raw HTML or Kotlin DSL:
HTML String Example:
createPage(
Page(
id = "3",
title = "Test HTML",
html = text(
//language=HTML
"""
<h1>Test Page</h1>
<p style="color: var(--primary)">Primary colored</p>
<button onclick="alert('Hello World!')">Click Me!</button>
""".trimIndent()
)
)
)
HTML DSL Example:
createPage(
Page(
id = "4",
title = "Test HTML Builders",
html = div {
p(styles = "color:red;") { dynamic("timestamp") }
p(styles = "color:blue;") { dynamic("timestamp2") }
h1 {
text("Heading1")
text("Heading2")
dynamic("test")
}
button(action = "test") {
text("Run Action")
}
button(styles = "all: unset; cursor: pointer;") {
text(iconSVG) // variable with svg icon
}
}
)
)
Each page must have a unique
id
.
Special Note on <script>
Tags
You can add JavaScript to your pages using text()
:
text("""
<script>
document.getElementById("dynamicIFrame").src = "http://" + window.location.hostname + ":5801";
</script>
""".trimIndent())
⚠️ Scripts are treated as ES Modules — they don’t share state across
<script>
tags.
Also,document
refers to the shadow DOM of the rendered page.
Panels HTML Engine Features
The HTML DSL includes:
button(action = "...") { ... }
— clickable buttonsdynamic("key")
— reactive content bindingtext("...")
— plain text or HTML contentdiv { ... }
— container elementsp
,h1
toh5
,span
,img
,iframe
widgetHeader()
— renders the widget-style panel headerempty { ... }
— for grouping multiple HTML elements
Styling and Theming
Each page supports CSS variables like --primary
, --background
, etc., allowing consistent theming across plugins.
You can also add custom CSS with text("")
.
Full styling reference:
// @noErrors
export const baseStyles = `
.wrapper {
--bg: #f6f6f6;
--card: #ffffff;
--cardTransparent: #ffffff50;
--text: #1b1b131414;
--primary: #e60012;
--primary: #005bac;
}
.wrapper.dark-mode {
--bg: #1b1b1b;
--card: #131314;
--cardTransparent: #13131450;
--text: #c4c7c5;
}
.wrapper.instant {
--multiplier: 0;
}
.wrapper.fast {
--multiplier: 0.1;
}
.wrapper.normal {
--multiplier: 0.15;
}
.wrapper.slow {
--multiplier: 0.225;
}
.wrapper {
--d: calc(var(--multiplier) * 1s);
--d1: calc(var(--multiplier) * 1s);
--d2: calc(var(--multiplier) * 2s);
--d3: calc(var(--multiplier) * 3s);
}
.wrapper.blue {
--primary: #005bac;
}
.wrapper.red {
--primary: #e60012;
}
.wrapper{
width: 100%;
height: 100%;
}
iframe{
outline: none;
border: none;
background-color:white;
}
.widget-header {
display: flex;
gap: 1rem;
justify-content: space-between;
align-items: center;
position: relative;
margin-inline: 1rem;
padding-top: 0.5rem;
margin-bottom: 0.5rem;
}
.widget-header > p {
margin: 0;
text-align: center;
flex-grow: 1;
font-size: 1.25rem;
font-weight: bold;
}
`
A library by Lazar from 19234 ByteForce.