A surface is a named place in an app’s UI that plugins fill with components. The host asks for “the article for this
object” and does not know which plugin will answer.
A role names a place: Article is the main content area, Section is an object inside a stack, CardContent is
the body of a card, and StatusIndicator is the status bar. Each role declares the shape of the data it passes, such as
{ subject, attendableId } for an article.
The roles a plugin fills most often, such as Article for the main content area, come from AppSurface in
@dxos/app-toolkit. App capabilities lists them.
Surface lists the rest and shows how to narrow a role’s subject to one type.
A surface contribution is a component plus a filter. The filter names one or more roles and a guard on the data,
for example “the article role, when the subject is a Note”. Plugins contribute surfaces through the ReactSurface
capability.
Role token for the article role. Subject defaults to any until narrowed
via
object
,
literal
, or
subject
. Extra passthrough
props are permitted.
Article,
classNote
Note),
8
component: React.ComponentType<{
role: string;
subject: Note & OfKind<EntityKind.Object>;
attendableId: string;
}>
Accepts any component type (not just a function) so a container re-exported through a
lazy() barrel as ComponentType<any> can be registered without a cast.
Maps the surface props onto the component's own props, so a plain container can be registered
directly instead of being wrapped in an adapter that unpacks data.
Prefer this over an inline component: ({ data }) => <Container … />: the mapper's input type
derives from the same filter that defines the surface's data shape, so the unpacking is
checked against the filter rather than restated by hand.
The resolved role NSID string (e.g. org.dxos.role.article). Present on
matched components only — multi-role containers can branch on this value
by comparing against a token's .role (e.g. role === AppSurface.Section.role).
The host renders <Surface.Surface type={role} data={data} />. The framework collects every contribution whose filter
names that role and whose guard accepts the data.
Contributions are ordered by their position, then by plugin order. The host renders the first one, or the first
limit of them when it wants several, as a toolbar does.
A filter can match several roles at once with AppSurface.oneOf, or require two conditions with AppSurface.allOf.
A component can render another plugin’s surface by rendering Surface.Surface itself. This is how a stack shows
each object with whatever plugin owns its type, and how a markdown document embeds a table.
1
import*as
import Surface
Surfacefrom'@dxos/app-framework/Surface';
2
import*as
import AppSurface
AppSurfacefrom'@dxos/app-toolkit/AppSurface';
3
4
// Renders whatever surface another plugin contributed for this object in the
A surface module loads lazily. The manifest lists the roles the module serves, and the plugin manager imports the module the
first time one of those roles is requested.
A surface component gets only the data its role passes. It must not assume which container it is in, how wide it is,
or that it is the only surface on screen; the same article can show in a plank, a stack or a dialog.
The props mapper of a surface runs during the host’s render, so it cannot call hooks. A component that needs hooks
is its own component.
A surface does not own the object it renders. Other surfaces, devices and agents edit the same object, so the
component reads it reactively.