Customizing the Default Editor

This page gathers every modification available without abandoning the default implementation, ordered from least to most invasive: the style configuration, the theme, buttons added to the sidebar and to nodes, replacing an individual renderer, and finally where to take over if you drop the layer entirely.

Style configuration

The config prop takes a partial configuration: write only what you want changed, the rest keeps its default. The editor's configuration has three parts:

FieldContents
marginsbackground (between the sheet and its contents), paragraph (between paragraphs), small (minor spacing)
widthseditable_drawer (width of pop-out drawers), minimum_content (minimum width of an element with content)
fontsbody (running text), structure (structural text such as node labels), info (hints)

Each font is a set of fontFamily, fontSize, lineHeight and the like. body is what the author types, structure is text the interface itself produces (node labels, words on buttons), and info is auxiliary hints.

<DefaultEditorComponent
    editorcore={editor_core}
    config={{
        margins: {paragraph: "1rem"},
        fonts  : {body: {fontFamily: "Georgia", fontSize: "1.1rem"}},
    }}
/>

This configuration is separate from the printer's, because the editing interface and the printed output ought to look different: while editing you may want generous line spacing to make clicking easier, whereas the output should be compact and well set. For the printer side see The Printer Side.

Interface text

Every string the default implementation puts on screen (button hints, panel titles, the messages raised after an operation) lives in one text table. It defaults to Chinese and is overridden partially through the texts prop, exactly like config:

<DefaultEditorComponent
    editorcore={editor_core}
    texts={{
        buttons: {delete_node: "Delete node", unwrap_node: "Unwrap node"},
        areas  : {concept_title: "Insert Concept", parameter_title: "Edit Parameters"},
    }}
/>

The full set of fields is the EditorTexts type, in three parts: buttons for button hints, areas for panel titles and concept type labels, and messages for toasts. The library ships no i18n framework of its own, so manage translations however you like and hand the finished strings over. The live demo does exactly this: Chinese uses the library defaults, English passes an override, in demo/src/editor_texts.ts.

Theme

The default implementation uses MUI's theme throughout: colours, corner radii and button styles all come from the theme, and none are hard-coded. Changing the palette therefore means wrapping a ThemeProvider around it, dark mode included, and both editor and printer follow:

<ThemeProvider theme={createTheme({palette: {mode: "dark"}})}>
    <DefaultEditorComponent editorcore={editor_core} />
</ThemeProvider>

The split between theme and config is that the theme governs colour and control appearance while the config governs fonts, margins and widths. To change the background colour of a theorem block, look at the theme; to change how much space sits between paragraphs, look at the config.

Sidebar buttons and a trailing element

sidebar_extras takes an array of components appended after the four built-in sidebar buttons, with a divider inserted automatically. Appended buttons join the keyboard navigation on their own (hold Alt+E, then move with the up and down arrows); nothing needs registering. Inside a button, useEditor() gives you the editor:

import { useEditor, AutoIconButton } from "@project-callio/calliotext"
import { Download as DownloadIcon } from "lucide-react"

function ExportButton(){
    const editor = useEditor()
    return <AutoIconButton
        icon={DownloadIcon} title="Export" size="medium"
        onClick={() => {
            const root = editor.get_root()
            console.log(JSON.stringify(root))
        }}
    />
}

<DefaultEditorComponent editorcore={editor_core} sidebar_extras={[ExportButton]} />

AutoIconButton is the icon button the default implementation uses for itself; it hooks into the key hints and the theme automatically, so a button built with it looks like the built-in ones. You can of course return a plain MUI IconButton instead, at the cost of handling hints and styling yourself.

end_element is arbitrary content placed below the sheet, suitable for a status bar, a word count and similar.

Buttons on a node

Sidebar buttons act on the whole document. For an operation that acts on one node, the place to add it is that node's own button group, through the buttons_extra option of the renderer factory. Such a button can reach its node with useNode() and useParameters():

function MarkDoneButton(){
    const editor = useEditor()
    const node   = useNode()
    return <AutoIconButton
        icon={CheckIcon} title="Mark done" size="medium"
        onClick={() => {
            editor.set_node(node, {parameters: {
                ...node.parameters,
                done: {val: true, type: "boolean"},
            }})
        }}
    />
}

const theorem_editor = get_deafult_group_editor_with_appbar({
    get_label    : () => useParameters().category,
    buttons_extra: [MarkDoneButton],
})

Replacing a single renderer

If one concept's appearance is too far from what the factories offer, replace just that renderer and leave the rest alone. A renderer is an ordinary React component receiving three props, editor, node and children:

import { NodeInfoProvider, EditorRendererProps } from "@project-callio/calliotext"

const bare_editor = ({node, children}: EditorRendererProps) => (
    <NodeInfoProvider node={node}>
        <div style={{borderLeft: "3px solid #888", paddingLeft: "1rem"}}>
            {children}
        </div>
    </NodeInfoProvider>
)

Writing your own renderer comes with two hard requirements. First, children must reach the screen, or the child content is lost and Slate raises an error for the DOM it cannot find. Second, if you want to call hooks such as useParameters() inside, or to place buttons from the default implementation, you have to wrap a NodeInfoProvider yourself, since that layer was previously supplied by the factory.

One more thing, not required but worth doing: put the parts that should not be selectable (buttons, labels) inside an EditorUnselecableBox. Without it, a Backspace at the start of the node may delete these interface elements.

Dropping the default implementation

The deepest level is to skip this layer and build an interface on EditorComponent. What you then have to handle yourself is exactly the list in the table on the overview page: the style configuration context, the toast container, index conflict repair, key dispatch, and handing the editor instance to downstream components. All of these are publicly exported, and the source of DefaultEditorComponent is itself a template you can copy from.