- Box (ui.catalog 4, since 1.4.248): direction, gap/padding/paddingX/ paddingY/margin on the gap scale, align, justify, wrap, grow, shrink, width/height/min/max (px up to 4096 or whole percents), columns, scroll, border (true or a tone), radius, background (theme surfaces or tone tints). Strict props: hex colours, CSS lengths and unknown props are dropped with a warning like other components. - Renderer maps props to theme tokens through data attributes; sizes reach inline style only as validated px or percents. - Typings, docs, skill references, gallery Layout tab; orca/testing finds Boxes and summarizes danger or warning Boxes.
12 KiB
Components panel API
Everything a components panel ("ui": "components") imports comes from orca. Components are JSX tags only; props are semantic (no className, style or lowercase DOM tags). Event props are functions. An unknown prop or enum value is dropped with a warning; an unknown component draws a placeholder.
Prop vocabulary
Gap:'none' | 'xs' | 'sm' | 'md' | 'lg'Align:'start' | 'center' | 'end' | 'stretch'Tone:'neutral' | 'accent' | 'success' | 'warning' | 'danger'Size:'xs' | 'sm' | 'md'Variant:'solid' | 'outline' | 'ghost'Press:() => unknownIconName: activity, bar-chart-3, bell, blocks, book, bot, bug, calendar, cloud, code, database, file-text, flag, folder, gauge, globe, hammer, layers, lightbulb, package, plug, puzzle, rocket, star, terminal, wrench, zap, add, remove, delete, edit, refresh, search, settings, check, close, info, warning, error, open-external, copy, download, upload, play, pause, stop, more, expand, collapse, next, filter, branch, file, agent, link. Any other name draws a fallback glyph.
Components
Stack(children: any):gap?: Gap; align?: AlignRow(children: any):gap?: Gap; align?: Align; justify?: 'start' | 'center' | 'end' | 'between'; wrap?: booleanText(children: text, Text, Link, Code, Badge, RelativeTime):tone?: Tone; muted?: boolean; size?: Size; weight?: 'regular' | 'medium' | 'semibold'; truncate?: booleanHeading(children: text, Text, Link, Code, Badge, RelativeTime):level?: 1 | 2 | 3Button(children: text):variant?: Variant; tone?: Tone; size?: Size; icon?: IconName; tooltip?: string; disabled?: boolean; onPress?: PressIconButton(children: none):icon: IconName; label: string; tooltip?: string; variant?: Variant; tone?: Tone; size?: Size; disabled?: boolean; onPress?: PressBadge(children: text):tone?: ToneCard(children: any):title?: string; description?: stringList(children: ListItem only):label?: stringListItem(children: any):title?: string; description?: string; icon?: IconName; current?: boolean; disabled?: boolean; onPress?: PressInput(children: none):label: string; labelHidden?: boolean; value?: string; defaultValue?: string; placeholder?: string; type?: 'text' | 'search' | 'password'; maxLength?: number; invalid?: boolean; disabled?: boolean; onChange?: (value: string) => void; onSubmit?: (value: string) => voidSwitch(children: none):label: string; checked?: boolean; defaultChecked?: boolean; disabled?: boolean; onChange?: (checked: boolean) => voidSelect(children: none):label: string; labelHidden?: boolean; value?: string; defaultValue?: string; placeholder?: string; options: readonly { value: string; label: string; disabled?: boolean }[]; disabled?: boolean; onChange?: (value: string) => voidSeparator(children: none):orientation?: 'horizontal' | 'vertical'Progress(children: none):label: string; labelHidden?: boolean; value?: numberSpinner(children: none):label: string; labelHidden?: booleanEmptyState(children: any):title: string; description?: string; icon?: IconNameLink(children: text):href?: string; onPress?: PressCode(children: text):block?: booleanTextarea(children: none):label: string; labelHidden?: boolean; value?: string; defaultValue?: string; placeholder?: string; rows?: number; maxLength?: number; invalid?: boolean; disabled?: boolean; onChange?: (value: string) => void; onSubmit?: (value: string) => voidCheckbox(children: none):label: string; description?: string; checked?: boolean; defaultChecked?: boolean; disabled?: boolean; onChange?: (checked: boolean) => voidRadioGroup(children: none):label: string; labelHidden?: boolean; value?: string; defaultValue?: string; options: readonly RadioOption[]; orientation?: 'vertical' | 'horizontal'; disabled?: boolean; onChange?: (value: string) => voidSlider(children: none):label: string; labelHidden?: boolean; value?: number; defaultValue?: number; min?: number; max?: number; step?: number; showValue?: boolean; disabled?: boolean; onChange?: (value: number) => voidField(children: any):label: string; description?: string; error?: string; required?: booleanTabs(children: Tab only):label: string; value?: string; defaultValue?: string; onChange?: (value: string) => voidTab(children: any):value: string; label: string; icon?: IconName; disabled?: booleanCollapsible(children: any):title: string; description?: string; open?: boolean; defaultOpen?: boolean; disabled?: boolean; onChange?: (open: boolean) => voidToolbar(children: any):label: string; gap?: GapGrid(children: any):columns?: 1 | 2 | 3 | 4 | 5 | 6; minItemWidth?: 'xs' | 'sm' | 'md' | 'lg'; gap?: GapTable(children: none):label: string; columns: readonly TableColumn[]; rows: readonly TableRow[]; sort?: TableSort; selectionMode?: 'none' | 'single' | 'multiple'; selected?: readonly string[]; defaultSelected?: readonly string[]; hasMore?: boolean; loading?: boolean; emptyText?: string; maxHeight?: MaxHeight; onSortChange?: (sort: TableSort) => void; onSelectionChange?: (selection: { keys: string[] }) => void; onRowAction?: (row: { key: string }) => void; onLoadMore?: () => voidTree(children: none):label: string; items: readonly TreeItem[]; selected?: string; defaultSelected?: string; expanded?: readonly string[]; defaultExpanded?: readonly string[]; maxHeight?: MaxHeight; onSelect?: (item: { key: string }) => void; onToggle?: (item: { key: string; expanded: boolean }) => void; onAction?: (item: { key: string }) => voidMarkdown(children: none):content: stringCodeView(children: none):code: string; language?: string; label?: string; lineNumbers?: boolean; wrap?: boolean; maxHeight?: MaxHeightImage(children: none):src: string; alt: string; size?: 'xs' | 'sm' | 'md' | 'lg' | 'full'; fit?: 'contain' | 'cover'; rounded?: booleanKeyValue(children: none):label?: string; items: readonly KeyValueItem[]Callout(children: any):tone?: Tone; title?: string; icon?: IconNameSkeleton(children: none):shape?: 'text' | 'block' | 'circle'; lines?: number; label?: stringRelativeTime(children: none):at: number; format?: RelativeTimeFormat; tone?: Tone; muted?: booleanBox(children: any):direction?: 'row' | 'column'; gap?: Gap; padding?: Gap; paddingX?: Gap; paddingY?: Gap; margin?: Gap; align?: Align; justify?: 'start' | 'center' | 'end' | 'between'; wrap?: boolean; grow?: boolean; shrink?: boolean; width?: BoxLength; height?: BoxLength; minWidth?: BoxLength; minHeight?: BoxLength; maxWidth?: BoxLength; maxHeight?: BoxLength; columns?: 1 | 2 | 3 | 4 | 5 | 6; scroll?: 'x' | 'y' | 'both'; border?: boolean | Tone; radius?: 'none' | 'sm' | 'md' | 'lg' | 'full'; background?: 'none' | 'muted' | 'card' | Tone
Runtime
h(type: unknown, props: object | null, ...children: OrcaNode[]): OrcaElementatom<T>(initial: T): Atom<T>: State for this panel run.atom<T>(key: string, initial: T, options?: AtomOptions<T>): Atom<T>: Named state, unique per plugin, optionally persisted.derived<T>(compute: () => T): ReadonlyAtom<T>: Computed from other atoms; reading it while rendering subscribes to what it reads.read<T>(atom: ReadonlyAtom<T>): Tset<T>(atom: Atom<T>, value: T): void: Storesvalueas is (functions included). Batched; throws while rendering.update<T>(atom: Atom<T>, updater: (previous: T) => T): void: Replaces the value withupdater(previous). Batched; throws while rendering.mount(component: () => OrcaNode): void: Optional: the module's default export is mounted automatically.on<E extends keyof OrcaEvents>(event: E, handler: (payload: OrcaEvents[E]) => void): Disposablehost.call(method, ...args)host.has(method: string): Whether this Orca offersmethodto this panel.api: { readonly major: number; readonly minor: number }: The host API this Orca implements.env.locale: string: Orca's UI language (BCP-47).env.theme: Theme: Reading it while rendering re-renders on light/dark switches.env.client: 'desktop'ui.catalog: number: Grows with the component catalog.ui.has(component: string, prop?: string): Whether this Orca drawscomponent(and acceptspropon it, when given).shell.openExternal(url: string): Needs a press on one of the panel's controls; see "User gestures".files.saveAs(options: HostMethods['files.saveAs']['params']): Needs a press on one of the panel's controls; see "User gestures".plugin: PluginBridgeApistorage: StorageApisettings: SettingsApisecrets: SecretsApi
Panel ↔ worker contract
Declare requests and worker posts once with declaration merging, in a file both the panel and the worker include: declare module 'orca' { interface PluginRequests { 'todos.list': { input: { done?: boolean }; output: Todo[] } } interface PluginPanelEvents { 'todos.changed': { count: number } } }. Declared names are typed end to end (plugin.request, plugin.on, the worker's orca.panels.handle and orca.panels.post, and orca/testing's requests and panel.post); any other name carries JSON.
Types
PersistScope = 'memory' | 'plugin' | 'workspace' | 'storage': Where a keyed atom survives: this panel run, every run of the plugin, per workspace, or the plugin's own storage under the atom's key (storage:own), shared with its worker and other panels and kept current when they change it.AtomOptions<T> = { persist?: PersistScope; version?: number; migrate?: (stored: JsonValue, fromVersion: number) => T }PanelContext = { subscriptions: Disposable[]; signal: AbortSignal; visible: ReadonlyAtom<boolean>; panel: { id: string } }: Passed to the module's optionalexport function setup(ctx).PanelSetup = (ctx: PanelContext) => void | (() => void): Runs once, after persisted atoms load and before the first render.HostError = Error & { code: string; retryable: boolean; retryAfterMs?: number }: Rejections carry acode(not_permitted,capability_denied,timeout, ...).PluginRequestOptions = { signal?: AbortSignal; timeoutMs?: number }:timeoutMs(at most 120000, default 30000) is how long the worker may take.PluginBridgeApi = { request<K extends keyof PluginRequests>(name: K, ...args: PluginRequestArgs<K>): Promise<PluginRequestOutput<K>>; request<N extends string>(name: Undeclared<N, PluginRequests>, input?: JsonValue, options?: PluginRequestOptions): Promise<JsonValue>; on<K extends keyof PluginPanelEvents>(event: K, handler: (payload: PluginPanelEvents[K]) => void): Disposable; on<N extends string>(event: Undeclared<N, PluginPanelEvents>, handler: (payload: JsonValue) => void): Disposable }: Your plugin's own worker.DataChange = { key: string }: A key someone (you, another panel, the worker or the person in Settings) changed.StorageApi = { get(key: string): Promise<JsonValue | null>; set(key: string, value: JsonValue): Promise<void>; delete(key: string): Promise<void>; keys(): Promise<string[]>; onDidChange(listener: (change: DataChange) => void): Disposable }: The plugin's own JSON store (storage:own), the same one its worker uses.SettingsApi = { get(): Promise<Record<string, JsonValue>>; get(key: string): Promise<JsonValue | undefined>; set(key: string, value: JsonValue): Promise<void>; onDidChange(listener: (change: DataChange) => void): Disposable }: The plugin's settings (settings:own): those it declares incontributes.settingsread as theirdefaultuntil the person changes them in Settings.SecretsApi = { get(key: string): Promise<string | null>; set(key: string, value: string): Promise<void>; delete(key: string): Promise<void> }: The plugin's encrypted secrets (secrets:own).
Limits
A render is refused, keeping the last good one, past 5000 nodes, depth 64, 512 KiB of data, 16 KiB per text, 1 KiB per label prop, 32 props per node or 500 Select options.