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
CompactViewfor sections — it provides a consistent heading and padding - Use
VStackfor vertical layouts within sections - Avoid horizontal scrolling — content must fit within 320px
- Use
HStacksparingly 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:
| Type | Description | Key properties |
|---|---|---|
string | Single-line text input | placeholder |
number | Numeric input | min, max, step |
boolean | Toggle switch | |
color | Color picker with hex input | |
select | Dropdown with predefined options | options |
textarea | Multi-line text input | placeholder, rows |
slider | Range slider | min, max, step, unit |
font | Font family picker (Google Fonts) | |
icon | Icon picker (Lucide, emoji, emotes, uploads) | |
image | Image upload or URL | |
sound | Sound picker from the account's sound library | Includes extension-bundled sounds |
multiselect | Multiple selection from options | options, multiple |
group | Collapsible group of nested fields | fields, collapsed |
divider | Visual separator (no value) | |
info | Read-only informational text (no value) | content |
channel | Multi-platform channel picker | |
designer | Fullpage designer surface field | presets |
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:
| Component | Type | Works in editor |
|---|---|---|
Box | Layout | Yes |
VStack | Layout | Yes |
HStack | Layout | Yes |
CompactView | Layout | Yes |
List | Layout | Yes |
Text | Display | Yes |
Image | Display | Yes |
Button | Input | Yes (interactive) |
Toggle | Input | Yes (interactive) |
TextField | Input | Yes (interactive) |
TextArea | Input | Yes (interactive) |
NumberField | Input | Yes (interactive) |
Dropdown | Input | Yes (interactive) |
Slider | Input | Yes (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:
| Component | Description |
|---|---|
EditorInput | Text, number, or color input with optional unit suffix |
EditorTextarea | Multi-line text input |
EditorSelect | Dropdown select with predefined options |
EditorCheckbox | Toggle checkbox |
EditorSlider | Range slider with optional unit |
EditorColorPicker | Color picker with hex input |
EditorFontPicker | Font family picker with weight and italic controls |
EditorFontRow | Compact font styling row (bold, italic, weight, size, color) |
EditorButton | Action button (primary, default, destructive variants) |
EditorGroup | Collapsible section group |
EditorLabel | Section label/heading |
EditorDivider | Visual separator |
EditorInfo | Informational 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}
/>
);
}