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
}
kotlin

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"
}
kotlin

isDev is a built-in field in BasePluginConfig, 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() }
)
kotlin
  • 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") }
)
kotlin
  • 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"
kotlin
  • 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
}
kotlin

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()
        )
    )
)
kotlin

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
            }
        }
    )
)
kotlin

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())
kotlin

⚠️ 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 buttons
  • dynamic("key") — reactive content binding
  • text("...") — plain text or HTML content
  • div { ... } — container elements
  • p, h1 to h5, span, img, iframe
  • widgetHeader() — renders the widget-style panel header
  • empty { ... } — 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;
      }
    `
ts

A library by Lazar from 19234 ByteForce.