Files
Neil 36f17a04a7 plugins: Box layout container for components panels (catalog 4) (#37420)
- 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.
2026-11-02 01:05:57 -08:00

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: () => unknown
  • IconName: 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?: Align
  • Row (children: any): gap?: Gap; align?: Align; justify?: 'start' | 'center' | 'end' | 'between'; wrap?: boolean
  • Text (children: text, Text, Link, Code, Badge, RelativeTime): tone?: Tone; muted?: boolean; size?: Size; weight?: 'regular' | 'medium' | 'semibold'; truncate?: boolean
  • Heading (children: text, Text, Link, Code, Badge, RelativeTime): level?: 1 | 2 | 3
  • Button (children: text): variant?: Variant; tone?: Tone; size?: Size; icon?: IconName; tooltip?: string; disabled?: boolean; onPress?: Press
  • IconButton (children: none): icon: IconName; label: string; tooltip?: string; variant?: Variant; tone?: Tone; size?: Size; disabled?: boolean; onPress?: Press
  • Badge (children: text): tone?: Tone
  • Card (children: any): title?: string; description?: string
  • List (children: ListItem only): label?: string
  • ListItem (children: any): title?: string; description?: string; icon?: IconName; current?: boolean; disabled?: boolean; onPress?: Press
  • Input (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) => void
  • Switch (children: none): label: string; checked?: boolean; defaultChecked?: boolean; disabled?: boolean; onChange?: (checked: boolean) => void
  • Select (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) => void
  • Separator (children: none): orientation?: 'horizontal' | 'vertical'
  • Progress (children: none): label: string; labelHidden?: boolean; value?: number
  • Spinner (children: none): label: string; labelHidden?: boolean
  • EmptyState (children: any): title: string; description?: string; icon?: IconName
  • Link (children: text): href?: string; onPress?: Press
  • Code (children: text): block?: boolean
  • Textarea (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) => void
  • Checkbox (children: none): label: string; description?: string; checked?: boolean; defaultChecked?: boolean; disabled?: boolean; onChange?: (checked: boolean) => void
  • RadioGroup (children: none): label: string; labelHidden?: boolean; value?: string; defaultValue?: string; options: readonly RadioOption[]; orientation?: 'vertical' | 'horizontal'; disabled?: boolean; onChange?: (value: string) => void
  • Slider (children: none): label: string; labelHidden?: boolean; value?: number; defaultValue?: number; min?: number; max?: number; step?: number; showValue?: boolean; disabled?: boolean; onChange?: (value: number) => void
  • Field (children: any): label: string; description?: string; error?: string; required?: boolean
  • Tabs (children: Tab only): label: string; value?: string; defaultValue?: string; onChange?: (value: string) => void
  • Tab (children: any): value: string; label: string; icon?: IconName; disabled?: boolean
  • Collapsible (children: any): title: string; description?: string; open?: boolean; defaultOpen?: boolean; disabled?: boolean; onChange?: (open: boolean) => void
  • Toolbar (children: any): label: string; gap?: Gap
  • Grid (children: any): columns?: 1 | 2 | 3 | 4 | 5 | 6; minItemWidth?: 'xs' | 'sm' | 'md' | 'lg'; gap?: Gap
  • Table (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?: () => void
  • Tree (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 }) => void
  • Markdown (children: none): content: string
  • CodeView (children: none): code: string; language?: string; label?: string; lineNumbers?: boolean; wrap?: boolean; maxHeight?: MaxHeight
  • Image (children: none): src: string; alt: string; size?: 'xs' | 'sm' | 'md' | 'lg' | 'full'; fit?: 'contain' | 'cover'; rounded?: boolean
  • KeyValue (children: none): label?: string; items: readonly KeyValueItem[]
  • Callout (children: any): tone?: Tone; title?: string; icon?: IconName
  • Skeleton (children: none): shape?: 'text' | 'block' | 'circle'; lines?: number; label?: string
  • RelativeTime (children: none): at: number; format?: RelativeTimeFormat; tone?: Tone; muted?: boolean
  • Box (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[]): OrcaElement
  • atom<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>): T
  • set<T>(atom: Atom<T>, value: T): void: Stores value as is (functions included). Batched; throws while rendering.
  • update<T>(atom: Atom<T>, updater: (previous: T) => T): void: Replaces the value with updater(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): Disposable
  • host.call(method, ...args)
  • host.has(method: string): Whether this Orca offers method to 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 draws component (and accepts prop on 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: PluginBridgeApi
  • storage: StorageApi
  • settings: SettingsApi
  • secrets: 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 optional export 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 a code (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 in contributes.settings read as their default until 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.