Skip to main content

Editor Panel

The editor surface renders in the dashboard overlay editor sidebar. It is the primary configuration UI for your extension — where streamers adjust settings, enter text, toggle features, and manage data.

Entry point

Every extension that includes the "editor" target must have a src/editor.tsx file that calls Lumio.render():

// src/editor.tsx
import { Lumio, CompactView, TextField, Toggle, useExtensionStorage } from "@zaflun/lumio-sdk";

function Settings() {
const [storage, setStorage] = useExtensionStorage();

return (
<CompactView title="Sports Scoreboard">
<Toggle
label="Show scores"
checked={storage.visible ?? true}
onChange={(checked) => setStorage({ ...storage, visible: checked })}
/>
<TextField
label="Home team"
value={storage.homeTeam ?? ""}
onChange={(value) => setStorage({ ...storage, homeTeam: value })}
/>
<TextField
label="Away team"
value={storage.awayTeam ?? ""}
onChange={(value) => setStorage({ ...storage, awayTeam: value })}
/>
</CompactView>
);
}

Lumio.render(<Settings />, { target: "editor" });

Layout constraints

The editor panel renders inside a fixed-width sidebar (approximately 320px wide). Follow these guidelines:

  • Use CompactView for sections — it provides a consistent heading and padding
  • Use VStack for vertical layouts within sections
  • Avoid horizontal scrolling — content must fit within 320px
  • Use HStack sparingly and only when the two children are very compact (e.g., a label + short input)
  • Keep labels concise — long labels wrap awkwardly in the narrow sidebar

Multiple sections

Use multiple CompactView components for logical groups of settings:

import { Lumio, CompactView, VStack, TextField, NumberField, Dropdown, Toggle, useExtensionStorage } from "@zaflun/lumio-sdk";

function Settings() {
const [storage, setStorage] = useExtensionStorage();

return (
<VStack>
<CompactView title="Teams">
<TextField
label="Home team"
value={storage.homeTeam ?? ""}
onChange={(value) => setStorage({ ...storage, homeTeam: value })}
/>
<TextField
label="Away team"
value={storage.awayTeam ?? ""}
onChange={(value) => setStorage({ ...storage, awayTeam: value })}
/>
</CompactView>

<CompactView title="Display">
<Dropdown
label="Position"
value={storage.position ?? "top-right"}
options={[
{ value: "top-left", label: "Top left" },
{ value: "top-right", label: "Top right" },
{ value: "bottom-left", label: "Bottom left" },
{ value: "bottom-right", label: "Bottom right" },
]}
onChange={(value) => setStorage({ ...storage, position: value })}
/>
<Slider
label="Opacity"
min={0}
max={100}
value={storage.opacity ?? 100}
onChange={(value) => setStorage({ ...storage, opacity: value })}
/>
</CompactView>
</VStack>
);
}

Lumio.render(<Settings />, { target: "editor" });

Configuration approaches

Three valid approaches exist for building extension settings:

1. config_schema only (no editor.tsx)

Define a config_schema in lumio.config.json and the host auto-generates a settings panel from the schema. No custom code needed -- the host renders each field using the same @lumio/ui components your editor.tsx would produce.

{
"config_schema": {
"primaryColor": {
"type": "color",
"label": "Primary color",
"default": "#6366f1"
},
"fontSize": {
"type": "slider",
"label": "Font size",
"default": 16,
"min": 8,
"max": 72,
"unit": "px"
}
}
}

This is the recommended approach for simple settings. See config_schema for the 17 supported field types.

2. editor.tsx only (no config_schema)

Build a fully custom settings UI using SDK components. Use this when you need dynamic behavior, conditional fields, server function calls, or layouts that go beyond what the schema can express.

3. Both config_schema and editor.tsx

When both are present, the auto-generated schema fields render first, followed by the custom editor panel below. This lets you define simple settings declaratively in the schema while adding custom interactive elements in editor.tsx.

Config schema field types

The following 17 field types are supported in config_schema:

TypeDescriptionKey properties
stringSingle-line text inputplaceholder
numberNumeric inputmin, max, step
booleanToggle switch
colorColor picker with hex input
selectDropdown with predefined optionsoptions
textareaMulti-line text inputplaceholder, rows
sliderRange slidermin, max, step, unit
fontFont family picker (Google Fonts)
iconIcon picker (Lucide, emoji, emotes, uploads)
imageImage upload or URL
soundSound picker from the account's sound libraryIncludes extension-bundled sounds
multiselectMultiple selection from optionsoptions, multiple
groupCollapsible group of nested fieldsfields, collapsed
dividerVisual separator (no value)
infoRead-only informational text (no value)content
channelMulti-platform channel picker
designerFullpage designer surface fieldpresets

designer_schema for fullpage design editors

Extensions that include the "designer" target can define a designer_schema in lumio.config.json. The designer provides a split-pane view with a live preview alongside configuration fields. Fields use the same type system as config_schema, but the schema is an array (with explicit key properties) rather than an object:

{
"targets": ["layer", "designer"],
"designer_schema": [
{
"key": "layout",
"type": "select",
"label": "Layout",
"default": "horizontal",
"options": [
{ "value": "horizontal", "label": "Horizontal" },
{ "value": "vertical", "label": "Vertical" }
]
},
{
"key": "bgColor",
"type": "color",
"label": "Background",
"default": "#1a1a2e"
}
]
}

See lumio.config.json reference and Targets for details.

Using useLumioConfig in editor.tsx

When building a custom editor, use the useLumioConfig hook with a functional updater to modify individual settings without overwriting the entire config object:

import { useLumioConfig, EditorInput, EditorSlider } from "@zaflun/lumio-sdk";

function Settings() {
const [config, setConfig] = useLumioConfig();

// Functional updater -- recommended for editor.tsx
const update = (key: string, value: unknown) =>
setConfig((prev) => ({
...prev,
settings: { ...(prev.settings ?? {}), [key]: value },
}));

return (
<>
<EditorInput
label="Title"
value={String(config.settings?.title ?? "")}
onChange={(v) => update("title", v)}
/>
<EditorSlider
label="Font Size"
value={Number(config.settings?.fontSize ?? 16)}
onChange={(v) => update("fontSize", v)}
min={8}
max={72}
unit="px"
/>
</>
);
}

The functional updater (prev) => next prevents race conditions when multiple fields update concurrently.

Available components

All SDK components work on the editor surface. Input components are fully interactive on this surface:

ComponentTypeWorks in editor
BoxLayoutYes
VStackLayoutYes
HStackLayoutYes
CompactViewLayoutYes
ListLayoutYes
TextDisplayYes
ImageDisplayYes
ButtonInputYes (interactive)
ToggleInputYes (interactive)
TextFieldInputYes (interactive)
TextAreaInputYes (interactive)
NumberFieldInputYes (interactive)
DropdownInputYes (interactive)
SliderInputYes (interactive)

Editor-specific components

The SDK provides 13 additional components designed specifically for editor panels. These serialize via the Worker Reconciler and render as @lumio/ui components on the host side:

ComponentDescription
EditorInputText, number, or color input with optional unit suffix
EditorTextareaMulti-line text input
EditorSelectDropdown select with predefined options
EditorCheckboxToggle checkbox
EditorSliderRange slider with optional unit
EditorColorPickerColor picker with hex input
EditorFontPickerFont family picker with weight and italic controls
EditorFontRowCompact font styling row (bold, italic, weight, size, color)
EditorButtonAction button (primary, default, destructive variants)
EditorGroupCollapsible section group
EditorLabelSection label/heading
EditorDividerVisual separator
EditorInfoInformational text block

See Editor Components for the full API reference with props and examples.

Calling server functions

The editor can call server functions — both queries and mutations. Mutations with editorOnly: true can only be called from the editor surface; the layer and interactive surfaces cannot call them.

import { Lumio, CompactView, Button, List, Text, useQuery, useMutation } from "@zaflun/lumio-sdk";

function RuleManager() {
const { data: rules, refetch } = useQuery("getRules");
const { mutate: addRule } = useMutation("addRule");
const { mutate: deleteRule } = useMutation("deleteRule");

return (
<CompactView title="Challenge Rules">
<Button
label="Add rule"
onClick={async () => {
await addRule({ text: "New rule" });
refetch();
}}
/>
<List
items={(rules ?? []).map((rule) => ({
id: rule.id,
label: rule.text,
action: (
<Button
label="Delete"
variant="destructive"
onClick={async () => {
await deleteRule({ id: rule.id });
refetch();
}}
/>
),
}))}
/>
</CompactView>
);
}

Lumio.render(<RuleManager />, { target: "editor" });

Storage write propagation

Every call to setStorage() in the editor is immediately propagated to the layer and interactive surfaces via WebSocket. There is no debounce — each call triggers a storage update. For text inputs that update on every keystroke, consider debouncing in your component:

import { useEffect, useState } from "react";
import { TextField, useExtensionStorage } from "@zaflun/lumio-sdk";

function DebouncedTextField({ storageKey }: { storageKey: string }) {
const [storage, setStorage] = useExtensionStorage();
const [local, setLocal] = useState(storage[storageKey] ?? "");

useEffect(() => {
const timer = setTimeout(() => {
setStorage({ ...storage, [storageKey]: local });
}, 300);
return () => clearTimeout(timer);
}, [local]);

return (
<TextField
label="Team name"
value={local}
onChange={setLocal}
/>
);
}