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" });

Available components

All 14 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)

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}
/>
);
}