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" });
Available components
All 14 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) |
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}
/>
);
}