Add a surface
By the end of this page a link has a view, where a person can edit its title, address and note, and see whether they have read it.
Background
Section titled “Background”A surface is a named place in an app’s UI, such as the main article area, a card or the status bar. Plugins fill surfaces by contributing a component and a filter that says which data the component can render. When Composer opens a link in the article area, it asks for every component that matches the article role and a link, and renders the best one. Surfaces explains the matching.
-
Write the component. The article surface passes the object as
subject.useObjectreturns a snapshot of it and re-renders the component when it changes, whether the change came from this tab, another device or the assistant. Writes go throughObj.update, like any ECHO write.src/containers/StashedLinkArticle.tsx import type * asAppSurface from '@dxos/app-toolkit/AppSurface';import AppSurfaceimport {Obj } from '@dxos/echo';import Objimport {useObject } from '@dxos/echo-react';const useObject: {<T extends Obj.Unknown>(ref: Ref<T>): [Obj.Snapshot<T> | undefined, ObjectUpdateCallback<T>];<T extends Obj.Unknown>(ref: Ref<T> | undefined): [Obj.Snapshot<T> | undefined, ObjectUpdateCallback<T>];<T extends Obj.Unknown>(ref: Ref<T> | undefined, options: LoadOptions): [Obj.Snapshot<T> | undefined, ObjectUpdateCallback<T>];<T extends Obj.Unknown>(obj: T): [Obj.Snapshot<T>, ObjectUpdateCallback<T>];<T extends Obj.Unknown>(obj: T | undefined): [Obj.Snapshot<T> | undefined, ObjectUpdateCallback<T>];<T extends Obj.Unknown>(objOrRef: T | Ref<T> | undefined): [Obj.Snapshot<T> | undefined, ObjectUpdateCallback<T>];<T extends Obj.Unknown, K extends keyof T>(obj: T, property: K): [T[K], ObjectPropUpdateCallback<T[K]>];<T extends Obj.Unknown, K extends keyof T>(obj: T | undefined, property: K): [T[K] | undefined, ObjectPropUpdateCallback<T[K]>];<T, K extends keyof T>(ref: Ref<T>, property: K): [T[K] | undefined, ObjectPropUpdateCallback<T[K]>];<T, K extends keyof T>(ref: Ref<T> | undefined, property: K): [T[K] | undefined, ObjectPropUpdateCallback<T[K]>];}import * asInput from '@dxos/react-ui/Input';import Inputimport * asLayout from '@dxos/react-ui/Layout';import Layoutimport * asPanel from '@dxos/react-ui/Panel';import Panelimport { typeStashedLink } from '../types/StashedLink.ts';class StashedLinkexport typeStashedLinkArticleProps =type StashedLinkArticleProps = {attendableId: string;nodeId?: string;subject: StashedLink;properties?: Record<string, any>;variant?: string;path?: string[];popoverAnchorId?: string;extrinsic?: boolean;} & {companionTo?: unknown;} & {role?: string;}AppSurface.import AppSurfaceObjectArticleProps<type ObjectArticleProps<Subject extends Obj.Unknown | undefined = Obj.Unknown, Props extends {} = {}, CompanionTo = unknown> = {attendableId: string;nodeId?: string;subject: Subject;properties?: Record<string, any>;variant?: string;path?: string[];popoverAnchorId?: string;extrinsic?: boolean;} & (unknown extends CompanionTo ? {companionTo?: CompanionTo | undefined;} : {companionTo: CompanionTo;}) & Props & {role?: string;}Component props for article-role ECHO object.
StashedLink>;class StashedLinkexport constStashedLinkArticle = ({const StashedLinkArticle: ({ role, subject, }: StashedLinkArticleProps) => React.JSX.Elementrole,role: string | undefinedsubject,subject: StashedLink}:StashedLinkArticleProps) => {type StashedLinkArticleProps = {attendableId: string;nodeId?: string;subject: StashedLink;properties?: Record<string, any>;variant?: string;path?: string[];popoverAnchorId?: string;extrinsic?: boolean;} & {companionTo?: unknown;} & {role?: string;}// A snapshot that re-renders the component when the object changes, here or// on another peer.const [link] =const link: Obj.Snapshot<StashedLink>useObject(useObject<StashedLink>(obj: StashedLink): [Obj.Snapshot<StashedLink>, ObjectUpdateCallback<StashedLink>] (+9 overloads)Hook to subscribe to an entire Echo object. Returns a snapshot of the current object value and automatically re-renders when the object changes.
subject);subject: StashedLinkreturn (<Panel.import PanelRootconst Root: React.ForwardRefExoticComponent<Omit<Panel.RootProps & {asChild?: boolean;}, "className"> & {classNames?: import("@dxos/ui-types").ClassNameValue;} & Pick<React.HTMLAttributes<Element>, "role" | "children" | "style"> & React.RefAttributes<HTMLDivElement>>export RootThe plank host: fills its parent, sets
data-sizeand a level for its subtree, and is the pane's query container, so its header, body and footer collapse at the same pane width.role={role?: React.AriaRole | undefinedrole}>role: string | undefined<Panel.import PanelBodyconst Body: React.ForwardRefExoticComponent<Omit<{asChild?: boolean;}, "className"> & {classNames?: import("@dxos/ui-types").ClassNameValue;} & Pick<React.HTMLAttributes<Element>, "role" | "children" | "style"> & React.RefAttributes<HTMLDivElement>>export BodyThe growing middle row, a plain slot: it neither scrolls nor adds a gutter, so content composes its own frame (
asChildonto aScrollArea.Rootaround a Container, or a canvas that fills the row). The first Container under it takes the panel's gutter unless it names one. Like the header and footer it is no query container, so its content collapses against the panel.asChild>asChild?: boolean | undefined<Layout.import LayoutFlexconst Flex: React.ForwardRefExoticComponent<Omit<Layout.FlexProps & {asChild?: boolean;}, "className"> & {classNames?: import("@dxos/ui-types").ClassNameValue;} & Pick<React.HTMLAttributes<Element>, "role" | "children" | "style"> & React.RefAttributes<HTMLDivElement>>One-axis layout box: the primitive that replaces hand-written
flex …wrapper divs.gapis restricted to the named steps of the theme spacing ramp (seeGap
), which is the point of the prop — an off-ramp
gap-2/gap-3literal in a container is the drift it prevents. Anything the props don't cover (padding, sizing, overflow, colour) goes throughclassNames; the component deliberately grows no padding or colour props, since components own their own spacing.There is no implicit
align. Row-centering is the common case, but defaulting it would silently restyle any consumer relying on the CSSstretchinitial value.Use
asChildto project the layout onto a semantic element (<header>,<ul>,<a>).columncolumn?: boolean | undefinedStack on the block axis instead of the inline axis.
gap='sm'gap?: Layout.Gap | undefinedclassNames='p-2'>classNames?: ClassNameValue<Input.import InputRootconst Root: React.ForwardRefExoticComponent<Omit<Input.RootProps, "className"> & {classNames?: import("@dxos/ui-types").ClassNameValue;} & Pick<React.HTMLAttributes<Element>, "role" | "children" | "style"> & React.RefAttributes<HTMLInputElement>>export RootText input at control size; inside a
Field.Rootit takes the field's id, label and description wiring. Withstartorendit renders aControlFrameholding the adornments and a bare input;data-testidand classes then go to the frame, the ref to the input.placeholder='Title'placeholder?: string | undefinedvalue={value?: string | number | readonly string[] | undefinedlink.const link: Obj.Snapshot<StashedLink>title ?? ''}title?: string | undefinedonChange={(onChange?: React.ChangeEventHandler<HTMLInputElement, HTMLInputElement> | undefinedevent) =>event: React.ChangeEvent<HTMLInputElement, HTMLInputElement>Obj.import Objupdate(const update: <StashedLink>(obj: StashedLink, callback: ChangeCallback<StashedLink>) => StashedLinkPerform mutations on an echo object within a controlled context.
All mutations within the callback are batched and trigger a single notification when the callback completes. Direct mutations outside of
Obj.updatewill throw an error for echo objects.This function also works with nested objects within echo objects (e.g., Template structs) that are reactive at runtime.
subject, (subject: StashedLinksubject) => {subject: {title?: string | undefined;url?: string | undefined;note?: string | undefined;read?: boolean | undefined;id: EntityId;"~@dxos/echo/Kind": EntityKind.Object;}subject.subject: {title?: string | undefined;url?: string | undefined;note?: string | undefined;read?: boolean | undefined;id: EntityId;"~@dxos/echo/Kind": EntityKind.Object;}title =title?: string | undefinedevent.event: React.ChangeEvent<HTMLInputElement, HTMLInputElement>target.ChangeEvent<HTMLInputElement, HTMLInputElement>.target: EventTarget & HTMLInputElementvalue;HTMLInputElement.value: stringThe
valueproperty of the HTMLInputElement interface represents the current value of the element as a string.})}/><Input.import InputRootconst Root: React.ForwardRefExoticComponent<Omit<Input.RootProps, "className"> & {classNames?: import("@dxos/ui-types").ClassNameValue;} & Pick<React.HTMLAttributes<Element>, "role" | "children" | "style"> & React.RefAttributes<HTMLInputElement>>export RootText input at control size; inside a
Field.Rootit takes the field's id, label and description wiring. Withstartorendit renders aControlFrameholding the adornments and a bare input;data-testidand classes then go to the frame, the ref to the input.placeholder='https://'placeholder?: string | undefinedvalue={value?: string | number | readonly string[] | undefinedlink.const link: Obj.Snapshot<StashedLink>url ?? ''}url?: string | undefinedonChange={(onChange?: React.ChangeEventHandler<HTMLInputElement, HTMLInputElement> | undefinedevent) =>event: React.ChangeEvent<HTMLInputElement, HTMLInputElement>Obj.import Objupdate(const update: <StashedLink>(obj: StashedLink, callback: ChangeCallback<StashedLink>) => StashedLinkPerform mutations on an echo object within a controlled context.
All mutations within the callback are batched and trigger a single notification when the callback completes. Direct mutations outside of
Obj.updatewill throw an error for echo objects.This function also works with nested objects within echo objects (e.g., Template structs) that are reactive at runtime.
subject, (subject: StashedLinksubject) => {subject: {title?: string | undefined;url?: string | undefined;note?: string | undefined;read?: boolean | undefined;id: EntityId;"~@dxos/echo/Kind": EntityKind.Object;}subject.subject: {title?: string | undefined;url?: string | undefined;note?: string | undefined;read?: boolean | undefined;id: EntityId;"~@dxos/echo/Kind": EntityKind.Object;}url =url?: string | undefinedevent.event: React.ChangeEvent<HTMLInputElement, HTMLInputElement>target.ChangeEvent<HTMLInputElement, HTMLInputElement>.target: EventTarget & HTMLInputElementvalue;HTMLInputElement.value: stringThe
valueproperty of the HTMLInputElement interface represents the current value of the element as a string.})}/><Input.import InputTextareaconst Textarea: React.ForwardRefExoticComponent<Omit<Input.TextareaProps, "className"> & {classNames?: import("@dxos/ui-types").ClassNameValue;} & Pick<React.HTMLAttributes<Element>, "role" | "children" | "style"> & React.RefAttributes<HTMLTextAreaElement>>export TextareaMulti-line text at control width; inside a
Field.Rootit takes the field's id, label and description wiring.placeholder='Why is this worth keeping?'placeholder?: string | undefinedvalue={value?: string | number | readonly string[] | undefinedlink.const link: Obj.Snapshot<StashedLink>note ?? ''}note?: string | undefinedonChange={(onChange?: React.ChangeEventHandler<HTMLTextAreaElement, HTMLTextAreaElement> | undefinedevent) =>event: React.ChangeEvent<HTMLTextAreaElement, HTMLTextAreaElement>Obj.import Objupdate(const update: <StashedLink>(obj: StashedLink, callback: ChangeCallback<StashedLink>) => StashedLinkPerform mutations on an echo object within a controlled context.
All mutations within the callback are batched and trigger a single notification when the callback completes. Direct mutations outside of
Obj.updatewill throw an error for echo objects.This function also works with nested objects within echo objects (e.g., Template structs) that are reactive at runtime.
subject, (subject: StashedLinksubject) => {subject: {title?: string | undefined;url?: string | undefined;note?: string | undefined;read?: boolean | undefined;id: EntityId;"~@dxos/echo/Kind": EntityKind.Object;}subject.subject: {title?: string | undefined;url?: string | undefined;note?: string | undefined;read?: boolean | undefined;id: EntityId;"~@dxos/echo/Kind": EntityKind.Object;}note =note?: string | undefinedevent.event: React.ChangeEvent<HTMLTextAreaElement, HTMLTextAreaElement>target.ChangeEvent<HTMLTextAreaElement, HTMLTextAreaElement>.target: EventTarget & HTMLTextAreaElementvalue;HTMLTextAreaElement.value: stringThe
valueproperty of the HTMLTextAreaElement interface represents the value of the element as a string, which is an empty string if the widget contains no content. It returns or sets the raw value contained in the control.})}/><pJSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>className='text-description'>HTMLAttributes<HTMLParagraphElement>.className?: string | undefined{link.const link: Obj.Snapshot<StashedLink>read ? 'Read' : 'Not read yet'}read?: boolean | undefined</p>JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement></Layout.import LayoutFlex>const Flex: React.ForwardRefExoticComponent<Omit<Layout.FlexProps & {asChild?: boolean;}, "className"> & {classNames?: import("@dxos/ui-types").ClassNameValue;} & Pick<React.HTMLAttributes<Element>, "role" | "children" | "style"> & React.RefAttributes<HTMLDivElement>>One-axis layout box: the primitive that replaces hand-written
flex …wrapper divs.gapis restricted to the named steps of the theme spacing ramp (seeGap
), which is the point of the prop — an off-ramp
gap-2/gap-3literal in a container is the drift it prevents. Anything the props don't cover (padding, sizing, overflow, colour) goes throughclassNames; the component deliberately grows no padding or colour props, since components own their own spacing.There is no implicit
align. Row-centering is the common case, but defaulting it would silently restyle any consumer relying on the CSSstretchinitial value.Use
asChildto project the layout onto a semantic element (<header>,<ul>,<a>).</Panel.import PanelBody>const Body: React.ForwardRefExoticComponent<Omit<{asChild?: boolean;}, "className"> & {classNames?: import("@dxos/ui-types").ClassNameValue;} & Pick<React.HTMLAttributes<Element>, "role" | "children" | "style"> & React.RefAttributes<HTMLDivElement>>export BodyThe growing middle row, a plain slot: it neither scrolls nor adds a gutter, so content composes its own frame (
asChildonto aScrollArea.Rootaround a Container, or a canvas that fills the row). The first Container under it takes the panel's gutter unless it names one. Like the header and footer it is no query container, so its content collapses against the panel.</Panel.import PanelRoot>const Root: React.ForwardRefExoticComponent<Omit<Panel.RootProps & {asChild?: boolean;}, "className"> & {classNames?: import("@dxos/ui-types").ClassNameValue;} & Pick<React.HTMLAttributes<Element>, "role" | "children" | "style"> & React.RefAttributes<HTMLDivElement>>export RootThe plank host: fills its parent, sets
data-sizeand a level for its subtree, and is the pane's query container, so its header, body and footer collapse at the same pane width.);};Panel.Rootwith the surface’srolegives the view the standard article layout. -
Contribute the surface.
AppSurface.object(AppSurface.Article, StashedLink)matches the article role when the subject is a link.propsmaps the surface’s data onto the component’s props, and is type-checked against the filter.src/capabilities/react-surface.ts import * asEffect from 'effect/Effect';import Effectimport * asCapabilities from '@dxos/app-framework/Capabilities';import Capabilitiesimport * asCapability from '@dxos/app-framework/Capability';import Capabilityimport * asSurface from '@dxos/app-framework/Surface';import Surfaceimport * asAppSurface from '@dxos/app-toolkit/AppSurface';import AppSurfaceimport {StashedLinkArticle } from '../containers/StashedLinkArticle.tsx';const StashedLinkArticle: ({ role, subject, }: StashedLinkArticleProps) => JSX.Elementimport {StashedLink } from '../types/StashedLink.ts';class StashedLinkexport defaultCapability.import CapabilitymakeModule(() =>const makeModule: <void, Capability.Contribution<Capability.CapabilityIdentifier<"org.dxos.app-framework.capability.reactSurface", "multi">>, never, never>(fn: (props: void) => Effect.Effect<Capability.Contribution<Capability.CapabilityIdentifier<"org.dxos.app-framework.capability.reactSurface", "multi">>, never, Scope>) => (props: void) => Effect.Effect<Capability.Contribution<Capability.CapabilityIdentifier<"org.dxos.app-framework.capability.reactSurface", "multi">>, never, Scope>Helper to define a capability module with explicit typing. Wraps the default export function to provide better type inference and make the pattern explicit.
This helper provides explicit typing for the module activation function, making it clear that the function should:
- Access declared
requiresviayield*(or the ambientCapability.Service/Plugin.Service) - Return an array of typed
Contribution
s (see
contribute
/
contributeAll
)
Effect.import Effectsucceed(const succeed: <Capability.Contribution<Capability.CapabilityIdentifier<"org.dxos.app-framework.capability.reactSurface", "multi">>>(value: Capability.Contribution<Capability.CapabilityIdentifier<"org.dxos.app-framework.capability.reactSurface", "multi">>) => Effect.Effect<Capability.Contribution<Capability.CapabilityIdentifier<"org.dxos.app-framework.capability.reactSurface", "multi">>, never, never>Creates an
Effectthat always succeeds with a given value.When to use
Use when an effect should complete successfully with a specific value without any errors or external dependencies.
Example (Creating a successful effect)
import { Effect } from "effect"// Creating an effect that represents a successful scenario//// ┌─── Effect<number, never, never>// ▼const success = Effect.succeed(42)Effect.runSync(success) // => 42Capability.import Capabilitycontribute(const contribute: <Capability.MultiTag<any, "org.dxos.app-framework.capability.reactSurface">>(capability: Capability.MultiTag<any, "org.dxos.app-framework.capability.reactSurface">, implementation: any) => Capability.Contribution<Capability.CapabilityIdentifier<"org.dxos.app-framework.capability.reactSurface", "multi">> (+1 overload)Capabilities.import CapabilitiesReactSurface,const ReactSurface: Capability.MultiTag<any, "org.dxos.app-framework.capability.reactSurface">Surface definitions that can be either React components or Web Components.
Surface.import Surfacecreate({const create: <Omit<NonNullable<AppSurface.ArticleData<any, {}, unknown>>, "subject"> & {subject: StashedLink & OfKind<EntityKind.Object>;}, {role: string;subject: StashedLink & OfKind<EntityKind.Object>;attendableId: string;}, "article">(definition: Readonly<{id: "article";filter: Filter<Omit<NonNullable<AppSurface.ArticleData<any, {}, unknown>>, "subject"> & {subject: StashedLink & OfKind<EntityKind.Object>;}>;position?: Position;component: ComponentType<...>;props?: ((props: ComponentProps<...>) => {...;}) | undefined;}>) => ReactDefinition<Omit<...> & {subject: StashedLink & OfKind<EntityKind.Object>;}>Creates a React surface definition from a typed filter.
id: 'article',id: "article"filter:filter: Filter<Omit<NonNullable<AppSurface.ArticleData<any, {}, unknown>>, "subject"> & {subject: StashedLink & OfKind<EntityKind.Object>;}>AppSurface.import AppSurfaceobject(const object: <Role<AppSurface.ArticleData<any, {}, unknown>>, typeof StashedLink>(token: Role<AppSurface.ArticleData<any, {}, unknown>>, schema: typeof StashedLink, predicate?: ((data: NonNullable<AppSurface.ArticleData<any, {}, unknown>>) => boolean) | undefined) => Surface.Filter<Omit<NonNullable<AppSurface.ArticleData<any, {}, unknown>>, "subject"> & {subject: StashedLink & OfKind<EntityKind.Object>;}> (+1 overload)AppSurface.import AppSurfaceArticle,const Article: Role<AppSurface.ArticleData<any, {}, unknown>>Role token for the
articlerole. Subject defaults toanyuntil narrowed viaobject
,
literal
, or
subject
. Extra passthrough props are permitted.
StashedLink),class StashedLinkcomponent:component: ComponentType<{role: string;subject: StashedLink & OfKind<EntityKind.Object>;attendableId: string;}>Accepts any component type (not just a function) so a container re-exported through a
lazy()barrel asComponentType<any>can be registered without a cast.StashedLinkArticle,const StashedLinkArticle: ({ role, subject, }: StashedLinkArticleProps) => JSX.Elementprops: ({props?: ((props: ComponentProps<Omit<NonNullable<AppSurface.ArticleData<any, {}, unknown>>, "subject"> & {subject: StashedLink & OfKind<EntityKind.Object>;}>) => {role: string;subject: StashedLink & OfKind<EntityKind.Object>;attendableId: string;}) | undefinedMaps 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 samefilterthat defines the surface's data shape, so the unpacking is checked against the filter rather than restated by hand.role,role: stringThe 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).data: {data: Omit<NonNullable<AppSurface.ArticleData<any, {}, unknown>>, "subject"> & {subject: StashedLink & OfKind<EntityKind.Object>;}subject,subject: StashedLink & OfKind<EntityKind.Object>attendableId } }) => ({attendableId: stringrole,role: stringsubject,subject: StashedLink & OfKind<EntityKind.Object>attendableId,attendableId: string}),}),),),); - Access declared
-
Add the module to the manifest. The
roleslist lets the plugin manager load the module only when it first needs an article surface.dx.config.ts import * asAppCapability from '@dxos/app-toolkit/AppCapability';import AppCapabilityimport * asAppSurface from '@dxos/app-toolkit/AppSurface';import AppSurface42 collapsed linesimport * asSpaceCapability from '@dxos/plugin-space/SpaceCapability';import SpaceCapabilityimport {StashNamespace } from './src/namespace.ts';const StashNamespace: {key: "com.example.plugin.stash";make: <T>() => <const N extends string>(name: N) => MultiTag<T, `com.example.plugin.stash.capability.${N}`>;makeSingleton: <T>() => <const N extends string>(name: N) => Tag<T, `com.example.plugin.stash.capability.${N}`>;}constconfig:const config: Plugin.PluginConfig<void>Plugin.import PluginPluginConfig =type PluginConfig<T = void> = {readonly plugin: {readonly key: string;readonly name: string;readonly description?: string | undefined;readonly author?: string | undefined;readonly homePage?: string | undefined;readonly source?: string | undefined;readonly screenshots?: readonly {readonly light?: string | undefined;readonly dark?: string | undefined;}[] | undefined;readonly tags?: readonly string[] | undefined;readonly icon?: {readonly key: string;readonly hue?: string | undefined;} | undefined;readonly spec?: string | undefined;readonly dependsOn?: readonly string[] | undefined;};readonly publish?: {...;} | undefined;} & {...;}A plugin's
dx.config.tsdefault export: its config data, runtimeMeta
and factory.
Plugin.import Pluginconfig({const config: <void>({ modules, ...data }: Plugin.ConfigOptions) => Plugin.PluginConfig<void>Defines a plugin entirely from its
dx.config.ts.plugin: {plugin: {readonly key: string;readonly name: string;readonly description?: string | undefined;readonly author?: string | undefined;readonly homePage?: string | undefined;readonly source?: string | undefined;readonly screenshots?: readonly {readonly light?: string | undefined;readonly dark?: string | undefined;}[] | undefined;readonly tags?: readonly string[] | undefined;readonly icon?: {readonly key: string;readonly hue?: string | undefined;} | undefined;readonly spec?: string | undefined;readonly dependsOn?: readonly string[] | undefined;}key:key: stringReverse-domain NSID — the plugin's globally-unique key (e.g.
org.dxos.plugin.excalidraw).StashNamespace.const StashNamespace: {key: "com.example.plugin.stash";make: <T>() => <const N extends string>(name: N) => MultiTag<T, `com.example.plugin.stash.capability.${N}`>;makeSingleton: <T>() => <const N extends string>(name: N) => Tag<T, `com.example.plugin.stash.capability.${N}`>;}key,key: "com.example.plugin.stash"name: 'Stash',name: stringauthor: 'Example',author?: string | undefinedAuthor or organization name. Only used for bundled plugins. For plugins published to the registry this field is ignored — the verified publisher (handle ?? did) is used instead.
description:description?: string | undefined'A reading list: save links with a note, and mark them as read.',icon: {icon?: {readonly key: string;readonly hue?: string | undefined;} | undefinedkey: 'ph--tray-arrow-down--regular',key: stringhue: 'indigo' },hue?: string | undefinedtags: ['labs'],tags?: readonly string[] | undefined},modules: [modules: readonly (Module<any> | undefined)[]Module specs only; each body is a dynamic import, so reading the config loads no plugin code.
AppCapability.import AppCapabilitytranslations(const translations: (resources: Resource | Resource[] | (() => Promise<{translations: Resource | Resource[];}>), options?: {name?: string;environments?: readonly Environment[];}) => Module<void>Module contributing translations.
() => import('./src/translations.ts', {with: {ImportCallOptions.with?: ImportAttributes | undefinedtype: 'module-url' } }),type: string),AppCapability.import AppCapabilityschema(const schema: (types: ReadonlyArray<AnyEntity> | (() => Promise<{default: ReadonlyArray<AnyEntity>;}>), options?: {name?: string;environments?: readonly Environment[];}) => Module<void>Module contributing schemas. Prefer the loader form — schema objects ride whatever barrels declare them, so an inline list drags those barrels into the plugin definition's static closure (the boot evaluation floor); a loader keeps them in the module body chunk.
() =>import('./src/capabilities/schema.ts', {with: {ImportCallOptions.with?: ImportAttributes | undefinedtype: 'module-url' },type: string}),),AppCapability.import AppCapabilityoperationHandler(const operationHandler: <void, any, readonly [], readonly []>(loader: LoadModule<void, readonly [], readonly [MultiTag<OperationHandlerSet, "org.dxos.app-framework.capability.operationHandler">]>, options?: MakerOptions<readonly [], readonly [], void, any> | undefined) => Module<any>Module maker contributing operation handlers. Handler sets register eagerly by default: a keyed set is a definition→loader map with a thin closure (definitions carry no implementations), so the operation registry is complete at boot while each handler's BODY still loads per invocation. The deferral axis is the operation, not the module. A feature plugin whose operations cannot be invoked before the app is interactive declares
activatesOn: ActivationEvents.Idleto register in that wave instead of on the startup pass; the boot path (deck, layout, space, client) invokes operations during startup and stays eager.Stated explicitly rather than inherited: omitting
activatesOnnow normalizes to Idle, which would leave the registry incomplete for exactly those boot-path invocations.() =>import('./src/capabilities/operation-handler.ts', {with: {ImportCallOptions.with?: ImportAttributes | undefinedtype: 'module-url' },type: string}),),SpaceCapability.import SpaceCapabilitycreateObject(const createObject: <void, any, readonly [], readonly []>(loader: LoadModule<void, readonly [], readonly [MultiTag<Readonly<{id: string;createObject: import("@dxos/plugin-space/SpaceSchema").CreateObject;inputSchema?: import("effect/Schema").Codec<any, any>;customPanel?: import("react").ComponentType<CreateObjectCustomPanelProps>;}>, "org.dxos.plugin.space.capability.createObject">]>, options?: MakerOptions<readonly [], readonly [], void, any> | undefined) => Module<any>Module maker contributing a typed create-object entry. Gated by default on the create flow opening (all consumers read the entries reactively); declare
activatesOnto override.() =>import('./src/capabilities/create-object.ts', {with: {ImportCallOptions.with?: ImportAttributes | undefinedtype: 'module-url' },type: string}),),AppCapability.import AppCapabilityappGraphBuilder(const appGraphBuilder: <void, any, readonly [], readonly []>(loader: LoadModule<void, readonly [], readonly [MultiTag<BuilderExtensions, "org.dxos.app-framework.capability.appGraphBuilder">]>, options?: MakerOptions<readonly [], readonly [], void, any> | undefined) => Module<any>Module maker contributing app-graph node builders. Gated by default on the host's idle event rather than the contributing plugin's own start: a builder is what puts a plugin's items in the navtree, so gating it on the plugin's own start (fired when its surface renders) is a deadlock — the item never appears, so it can never be opened. Declare
activatesOnto override.() =>import('./src/capabilities/app-graph-builder.ts', {with: {ImportCallOptions.with?: ImportAttributes | undefinedtype: 'module-url' },type: string}),),AppCapability.import AppCapabilitysurface(const surface: <void, any, readonly [], readonly []>(loader: LoadModule<void, readonly [], readonly [MultiTag<any, "org.dxos.app-framework.capability.reactSurface">]>, options?: (MakerOptions<readonly [], readonly [], void, any> & {roles?: readonly Role<any>[];}) | undefined) => Module<any>Module maker contributing React surfaces. Declaring
roles(the role tokens the module's surfaces bind, e.g.AppSurface.Articleor a plugin's<Plugin>Surfacetokens) gates the module on those roles' demand events (ActivationEvents.SurfacesRequested
) — it loads when a
Surfacefor one of its roles first renders instead of at startup. Modules without declared roles stay eager; an explicitactivatesOnwins over the derived gate.() =>import('./src/capabilities/react-surface.ts', {with: {ImportCallOptions.with?: ImportAttributes | undefinedtype: 'module-url' },type: string}),{roles: [roles?: readonly Role<any>[] | undefinedAppSurface.import AppSurfaceArticle],const Article: Role<AppSurface.ArticleData<any, {}, unknown>>Role token for the
articlerole. Subject defaults toanyuntil narrowed viaobject
,
literal
, or
subject
. Extra passthrough props are permitted.
},), -
See the view. Load or reload your plugin below, click New in the header to create a sample link, and select Surface. The inspector renders each of the module’s surfaces whose filter accepts the sample: here, the article.
Edit the title in the article’s input, and the name in the header changes as you type. In Composer the same view fills the main area when a person opens a link.
What you can do now
Section titled “What you can do now”- Read an object reactively in a component and write it with
Obj.update. - Contribute an article surface for a type.
- Load a surface module only when its role is needed.
Run it in Composer loads the plugin into Composer from your machine, and gives the assistant Mark as read.