App Studio is in release 3.0 as Beta: a paid add-on that no edition includes by default. A licence or a trial licence that includes the app_studio feature switches it on. The module is on by default, so the licence decides. This reference may change while App Studio is in Beta; the SDK changelog records each change.
Generated from the SDK sources by scripts/appstudio-sdk-docs.cjs for runtime-v1. Do not edit; run npm run appstudio:docs:sdk.
@maestrohub/app-ui
Beta: look up the App Studio UI kit components, such as cards, tables, charts and form fields, that take their colours from the platform theme.
- Runtime
- runtime-v1
- Exports
- 68 (49 values, 19 types)
The App Studio UI kit: layout, display, input, table and chart components (Beta in 3.0).
Runtime: runtime-v1. 68 exports (49 values, 19 types). Import with:
import { ... } from '@maestrohub/app-ui';
Hooks
usePalette
function usePalette(): ResolvedPalette
usePalette returns the kit colours for the current theme: the host's own background / surface / ink / muted / border / subtle / primary / danger when the shell sent them, the kit's palette otherwise; the status colours are always the kit's.
useTheme
function useTheme(): ThemeMode
useTheme returns the live theme mode the shell set (light when unset).
Components
Attachments
function Attachments({ collection, recordKey, label, accept, hint, removable, readOnly, }: { collection: Collection; recordKey: string | null; label?: string; accept?: string; hint?: string; removable?: boolean; readOnly?: boolean; }): JSX.Element
Attachments shows one stored record's files and attaches more: photos
render as thumbnails, any other file as a named tile; a click opens the
file in a new tab. recordKey null means the record is not saved yet,
and the picker says so instead of failing on the first file. A refused
file (wrong type, over the size limit, the app's total reached) shows
the platform's reason. removable adds a Remove button (with a
confirm step) under each file; readOnly hides the picker.
Avatar
function Avatar({ person, size }: { person: Person; size?: number; }): JSX.Element
A person's picture, or their initials (a group shows its initial on a square). size in px, default 28.
Badge
function Badge({ tone, children }: { tone?: "ok" | "warn" | "critical" | "neutral"; children?: ReactNode; }): JSX.Element
A small rounded pill coloured by tone (default neutral) from the
palette's status colours (status[tone], the same text colour
toneColor returns). A toned badge's text must name the status
("Faulted", "Torque high"), not only a number or a name: colour alone
is not enough for colour-blind users.
BarChart
function BarChart({ items, height, unit, max }: { items: { label: string; value: number; tone?: Tone; }[]; height?: number; unit?: string; max?: number; }): JSX.Element
A responsive SVG bar chart of items (label, value, optional tone).
The scale tops out at max or the largest value; negative values are
drawn as zero-height bars, labels longer than 12 characters are
truncated. A bar with a tone carries the tone's shape under its
label; the chart's accessible description reads each bar's value
(with unit) and tone word, the first 20 and a count of the rest. Renders "No data." for an empty list.
Button
function Button({ intent, disabled, reason, confirm, onClick, children }: ButtonProps): JSX.Element
The kit's action button. When disabled and reason are both set the
reason renders as the button's title and as a caption beneath it, so
a dead control always says why. With confirm, the first click swaps
the label for the confirm text and shows a Cancel button; only the
second click fires onClick.
Card
function Card({ title, subtitle, actions, children }: { title?: string; subtitle?: string; actions?: ReactNode; children?: ReactNode; }): JSX.Element
A bordered, themed panel: an optional title heading (with a muted
subtitle under it and actions on the right) above children.
A card on its own keeps a bottom margin so stacked cards space
themselves; inside a kit layout (Page, Section, Stack, Grid, Row) the
layout's gap spaces it instead (the base stylesheet's .mh-card rule),
so the gaps between cards are even.
CheckboxField
function CheckboxField({ label, checked, onChange, disabled }: { label: string; checked: boolean; onChange: (v: boolean) => void; disabled?: boolean; }): JSX.Element
A checkbox with its label inline; reports onChange(boolean). Unlike the other fields it has no hint.
ConnectionNotice
function ConnectionNotice({ hidePending }: ConnectionNoticeProps): JSX.Element | null
ConnectionNotice shows, above an app's content, that live values are paused (since when, in the app's clock) and how many records are waiting to be sent, with a "Send now" button. Renders nothing while the connection is live and the outbox is empty.
Example
<Page title="Line 2"><ConnectionNotice /> ...</Page>
DateField
function DateField({ label, value, onChange, hint, disabled, withTime }: { label: string; value: string; onChange: (iso: string) => void; hint?: string; disabled?: boolean; withTime?: boolean; }): JSX.Element
DateField / DateTimeField take and return ISO strings (empty = unset).
EmptyState
function EmptyState({ title, description, action }: { title: string; description?: string; action?: ReactNode; }): JSX.Element
A dashed-border placeholder for "nothing here yet": bold title, muted description, and an optional action slot below.
ErrorNotice
function ErrorNotice({ error, title, onRetry, retryLabel }: ErrorNoticeProps): JSX.Element | null
Renders a caught error as an alert: the title (or the platform code
when no title is given), the reason, the remediation when the platform
supplied one, and a Retry button when onRetry is set. Understands
ApiError, LiveError, shell refusals and plain errors via
describeError; renders nothing for null/undefined.
Example
const t = useTopic('plant/line1/oee');
return t.error ? <ErrorNotice error={t.error} /> : <MetricCard label="OEE" value={t.value} />;
FeedStatus
function FeedStatus({ status }: { status: LiveStatus; }): JSX.Element
One live subscription's state (useTopic(...).status) as a toned
badge in plain words: Live, Connecting, Feed error, Disconnected.
Pair it with <ErrorNotice error={topic.error} />, which says why.
FileField
function FileField({ label, onFiles, accept, multiple, hint, disabled, busy }: { label: string; onFiles: (files: File[]) => void; accept?: string; multiple?: boolean; hint?: string; disabled?: boolean; busy?: boolean; }): JSX.Element
A labelled file picker that also takes files dropped on it. Reports
the picked files as an array through onFiles (empty picks are not
reported) and clears itself, so picking the same file again reports
again. accept and multiple pass through to the input; busy
swaps the label for "Uploading…" and refuses new files meanwhile.
The control never checks types or sizes: the platform refuses what it
will not store and says why.
FilterBar
function FilterBar({ children, actions }: { children?: ReactNode; actions?: ReactNode; }): JSX.Element
FilterBar is the horizontal strip that holds a report's parameters.
Gauge
function Gauge({ value, max, threshold, unit }: { value: number; max?: number; threshold?: number; unit?: string; }): JSX.Element
A large numeric readout (value + unit, default %) over a
horizontal fill bar sized by value / max (default max 100, clamped to
0 to 100 %). When threshold is set and value is below it, the
number and the bar turn the critical status colour and a critical
ToneMark precedes the number; otherwise the bar is ok.
Grid
function Grid({ columns, gap, minColumnWidth, children }: { columns?: number; gap?: number; minColumnWidth?: number; children?: ReactNode; }): JSX.Element
Grid lays children out in columns equal columns (1 on narrow screens).
KeyValueList
function KeyValueList({ items }: { items: { key: string; value: ReactNode; }[]; }): JSX.Element
A two-column definition list (dl) of items; each item's key is the muted term and value the description. Keys must be unique; they are the React keys.
Loading
function Loading({ label }: { label?: string; }): JSX.Element
A muted one-line status text (role="status"), default "Loading…". No spinner.
MetricCard
function MetricCard({ label, value, unit, delta, tone, hint }: { label: string; value: ReactNode; unit?: string; delta?: number; tone?: Tone; hint?: string; }): JSX.Element
A KPI tile: uppercase label, a large value with optional unit,
and an optional hint line. tone colours the value (ok / warn / critical)
and puts the tone's ToneMark before it, so the status is not colour alone;
delta renders a badge with an up/down arrow and one padded decimal
(▲ 2.0) whose tone is derived from the sign of the delta as shown,
not from tone.
NumberField
function NumberField({ label, value, onChange, min, max, step, unit, hint, disabled }: { label: string; value: number | null; onChange: (v: number | null) => void; min?: number; max?: number; step?: number; unit?: string; hint?: string; disabled?: boolean; }): JSX.Element
A labelled type="number" input. value is number | null; an empty
field reports null, anything else Number(text). unit is appended
to the label as "(unit)"; min/max/step pass through to the input.
Page
function Page({ title, subtitle, actions, fill, children }: { title?: string; subtitle?: string; actions?: ReactNode; fill?: boolean; children?: ReactNode; }): JSX.Element
The top-level app frame: a centred column (max 1400 px) with the kit
font and ink colour; the document's base stylesheet pads it from the
frame's edges. Renders a header with title (h1), subtitle
and right-aligned actions only when a title or actions are given.
fill makes the page the whole frame instead (no column, no header),
for a canvas whose panels float over it.
PersonField
function PersonField({ label, value, onChange, kind, placeholder, hint, disabled }: { label: string; value: Person | null; onChange: (person: Person | null) => void; kind?: "user" | "group"; placeholder?: string; hint?: string; disabled?: boolean; }): JSX.Element
A labelled picker over the app's people: type part of a name, pick one.
Controlled: value (a Person or null) in, onChange out. kind limits
it to people ('user') or groups ('group'). Pass the pick to
notifications.notify({ to: [subjectOf(person)] }) or store its id.
ProgressBar
function ProgressBar({ value, max, tone, label }: { value: number; max?: number; tone?: Tone; label?: ReactNode; }): JSX.Element
A horizontal bar filled to value / max (clamped to 0 to 100 %), in
the tone's colour (default ok), with an optional label above it
on the left and the percentage on the right: work-order progress, a
tank level, a shift target.
Row
function Row({ gap, align, wrap, children }: { gap?: number; align?: CSSProperties["alignItems"]; wrap?: boolean; children?: ReactNode; }): JSX.Element
A horizontal flex row of children; gap (12), align (center) and wrap (true) map to the flex properties.
Section
function Section({ title, description, actions, children }: { title?: string; description?: string; actions?: ReactNode; children?: ReactNode; }): JSX.Element
A titled block inside a Page: an h2 title, muted description,
and actions on the right, above children. The header row is
omitted when neither title nor actions are set.
SelectField
function SelectField({ label, value, onChange, options, placeholder, hint, disabled }: { label: string; value: string; onChange: (v: string) => void; options: SelectOption[]; placeholder?: string; hint?: string; disabled?: boolean; }): JSX.Element
A labelled select over options. When placeholder is set it is
rendered as a leading option with an empty value, so choosing it
reports ''.
Sparkline
function Sparkline({ values, width, height, tone }: { values: number[]; width?: number; height?: number; tone?: Tone | "primary"; }): JSX.Element
An inline, unlabelled SVG line of values scaled to its own min/max
(width 120 × height 28 by default). Non-finite values are dropped;
fewer than two remaining renders an em dash instead. A status tone
is named in the sparkline's accessible name and title.
Stack
function Stack({ children, gap }: { children?: ReactNode; gap?: number; }): JSX.Element
A vertical flex column of children separated by gap pixels (default 12).
StatusDot
function StatusDot({ tone, label }: { tone?: Tone; label?: string; }): JSX.Element
A 10 px ToneMark for tone (default neutral): the tone's colour
and shape, named by the tone's word, followed by an optional label.
A label that is the tone's own word ("Critical") is not repeated, and
a dot with no tone (a default nobody chose) is not announced.
Table
function Table({ columns, rows, rowKey, pageSize, filterable, sort: initialSort, exportName, emptyText, onRowClick, caption }: TableProps): JSX.Element
The data table: client-side text filter (filterable), click-to-sort
headers (numeric when both values parse as numbers, else locale string
compare), pagination by pageSize, and CSV export of every filtered
and sorted row via the shell when exportName is set. An export the
shell or the size cap refuses is shown inline as an ErrorNotice.
Cells render through formatCell.
Tabs
function Tabs({ items, initial }: { items: TabItem[]; initial?: string; }): JSX.Element
A tab strip over a single panel. Holds the active id in local state,
starting at initial or the first item; only the active item's
content is rendered. Uses tablist/tab/tabpanel roles.
Text
function Text({ tone, size, children }: { tone?: "body" | "muted" | "strong"; size?: "sm" | "md" | "lg"; children?: ReactNode; }): JSX.Element
Text is the kit's paragraph: the one place body copy is styled.
TextArea
function TextArea({ label, value, onChange, rows, placeholder, hint, disabled }: { label: string; value: string; onChange: (v: string) => void; rows?: number; placeholder?: string; hint?: string; disabled?: boolean; }): JSX.Element
A labelled multi-line text input (rows default 3, vertically resizable). Controlled via value / onChange(string).
TextField
function TextField({ label, value, onChange, placeholder, hint, disabled }: { label: string; value: string; onChange: (v: string) => void; placeholder?: string; hint?: string; disabled?: boolean; }): JSX.Element
A labelled single-line text input. Controlled: value in, onChange(string) out; optional placeholder, hint caption and disabled.
TimeRangeField
function TimeRangeField({ label, value, onChange }: { label?: string; value: TimeRange; onChange: (r: TimeRange) => void; }): JSX.Element
TimeRangeField: the "last 24 h / last 7 d / custom" control every report needs.
TimeSeriesChart
function TimeSeriesChart({ series, bands, height, yLabel, yMin, yMax, unit }: { series: Series[]; bands?: Band[]; height?: number; yLabel?: string; yMin?: number; yMax?: number; unit?: string; }): JSX.Element
A responsive SVG line chart of one or more Series with optional
shaded bands, four y grid lines, a legend, and the first and last x
values as axis labels (formatted as times when any x is not a number).
The y range auto-fits the data plus the bands unless yMin/yMax are
given; point markers are drawn only for series with 60 points or
fewer. Its accessible description names each series and band with
its tone's word (the first 20, then a count of the rest). Renders "No data." when there are no points.
Timeline
function Timeline({ items }: { items: { at: string; title: ReactNode; detail?: ReactNode; tone?: Tone; }[]; }): JSX.Element
Timeline lists dated events newest first: the log/handover/alarm
history shape every operations app ends up drawing.
Each entry's time reads in the app's clock (formatDateTime); its
tone (default neutral) is a ToneMark, shape and colour; an
entry without a tone gets the neutral ring, not announced.
ToneMark
function ToneMark({ tone, size, label, decorative }: { tone: Tone; size?: number; label?: string; decorative?: boolean; }): JSX.Element
ToneMark draws tone as its shape in its colour, size px (default
10), named for assistive technology and on hover by the tone's word
(label overrides it). With decorative it is hidden from assistive
technology: for a mark whose tone the adjacent text already says, or
a default tone nobody chose (an untoned Timeline entry). StatusDot and
Timeline draw their dot with it; use it beside toneColor in a status
widget of your own.
Functions
contrastRatio
function contrastRatio(fg: string, bg: string): number
contrastRatio is the WCAG 2 contrast between two colours (1 to 21). A
translucent fg is composited over bg first. NaN when either
colour cannot be read.
describeError
function describeError(error: unknown): ErrorDescription | null
describeError turns any caught value into {reason, remediation?, code?}. Exported so an app (or a test) can read the explanation without the DOM.
formatCell
function formatCell(value: unknown, column: Column, row: TableRow, context: { columnValues: number[]; }, dark: boolean): { text: string; node?: ReactNode; style?: React.CSSProperties; }
Applies a column's CellFormatter to one value and returns the plain
text (used for filtering, sorting and CSV), an optional node to
render instead of the text (badges, progress bars), and an optional
cell style (heatmap background). context.columnValues supplies the
column's numeric values for the percentile heatmap; dark picks the
heat scale. Non-numeric input to a numeric formatter renders "—".
Example
formatCell(0.8734, { key: 'oee', label: 'OEE', format: { kind: 'number', digits: 1, unit: '%' } }, row, { columnValues: [] }, false)
// → { text: '0.9 %' }
heatColor
function heatColor(t: number, dark: boolean): string
heatColor maps 0..1 to a cool→warm scale readable on both themes.
mixColors
function mixColors(a: string, b: string, t: number): string
mixColors blends a toward b by t (0 = a, 1 = b) and returns
opaque hex, a form every consumer takes (inline styles, SVG, three.js).
An unreadable input returns the other colour as given.
percentileOf
function percentileOf(v: number, all: number[]): number
percentileOf returns the fraction of all that is ≤ v (0..1).
toCSV
function toCSV(columns: Column[], rows: TableRow[], format: (value: unknown, column: Column, row: TableRow) => string): string
toCSV renders rows with the columns' formatted text (RFC 4180 quoting).
toneColor
function toneColor(p: Palette, tone: Tone): string
toneColor is the one map from a Tone to its colour, for every
component that draws a status as a colour (a value, a dot, a bar, a
line): p.status[tone].text, the same colour a Badge of that tone
writes its text in. Colour is never the only cue: pair it with
ToneMark (a shape per tone) or a word.
Constants
TONE_WORD
const TONE_WORD: Record<Tone, string>
The word each tone is announced and titled with.
Members
critical:stringneutral:stringok:stringwarn:string
Types
Band
interface Band
A horizontal shaded band on a TimeSeriesChart (a target range, an alarm limit).
Members
from:number: Lower y bound.label?:string | undefined: Text drawn at the band's top-right.to:number: Upper y bound.tone?:Tone | undefined: Fill colour (defaultok); the band's title names the tone.
ButtonProps
interface ButtonProps
Props for Button.
Members
children?:React.ReactNode: Button label.confirm?:string | undefined: confirm asks before firing: the G3 rung-2 affordance.disabled?:boolean | undefined: Disables the control; pair withreasonso the button explains itself.intent?:"primary" | "danger" | "neutral" | undefined: Colour intent:primary(accent),danger, orneutral(subtle fill). Defaultprimary.onClick?:(() => void) | undefined: Fired on click, after the confirm step whenconfirmis set; never while disabled.reason?:string | undefined: reason renders the disabled explanation (FR-42's honest-UI half): a disabled control always says why (house rule 36).
CellFormatter
type CellFormatter = {
kind: 'number';
digits?: number;
unit?: string;
} | {
kind: 'time';
} | {
kind: 'badge';
map?: Record<string, Tone>;
} | {
kind: 'percentile-heatmap';
invert?: boolean;
} | {
kind: 'threshold';
bands: {
max: number;
tone: Tone;
}[];
above?: Tone;
} | {
kind: 'category-color';
map: Record<string, Tone>;
} | {
kind: 'compare';
baseline: string;
percent?: boolean;
digits?: number;
} | {
kind: 'progress-bar';
max: number;
tone?: Tone;
};
The formatter catalogue: closed, schema-shaped, pure.
Column
interface Column
One column of a Table.
Members
align?:"left" | "right" | undefined: Cell alignment; when omitted numeric cells align right, others left.format?:CellFormatter | undefined: Declarative cell formatter. Omitted, a number reads asshownNumberwrites it (at most two decimals) and anything else asString(value).key:string: The row property to read; also the sort key.label:string: Header text.sortable?:boolean | undefined: Set tofalseto make the header inert; every other value keeps click-to-sort on.width?:string | number | undefined: Header width, passed to thethstyle.
ConnectionNoticeProps
interface ConnectionNoticeProps
Props for ConnectionNotice.
Members
hidePending?:boolean | undefined: Say nothing about pending records (an app that writes none).
ErrorDescription
interface ErrorDescription
The rendered explanation of an error, independent of the DOM.
Members
code?:string | undefined: The platform's code (APP_RATE_LIMITED, ACTION_NOT_DECLARED, …).reason:string: The headline: what was refused or what failed.remediation?:string | undefined: What to do next, when the platform said.retryAfterSeconds?:number | undefined: Seconds the platform asked the app to wait (429 only).
ErrorNoticeProps
interface ErrorNoticeProps
Props for ErrorNotice.
Members
error:unknown: Whatever was caught;null/undefinedrenders nothing.onRetry?:(() => void) | undefined: Offers a "Retry" button; the notice does not retry by itself.retryLabel?:string | undefined: Label for the retry button (default "Retry").title?:string | undefined: Optional heading above the explanation.
Palette
interface Palette
A colour set for one ThemeMode. usePalette returns a
ResolvedPalette (every field set); the fields added in 2026-10
(background, dangerInk, status) are optional here so an app's own
palette literal written before them still type-checks, and
toneColor still answers for it.
Members
background?:string | undefined: Page background: the host's--backgroundwhen tokens were sent.badge:Record<Tone, CSSProperties>: Background + text pair perTone, asBadgerenders them:statusas CSS.border:string: Borders and rules.chart:string: Data accent for charts and sparklines: the kit's own, never the host's button colour (a shell whose primary is near-white on dark would draw every series in white).danger:string: The destructive FILL (a danger button's background; the host's--destructivewhen tokens were sent). Not a status colour: a critical value, dot or message readsstatus.critical, which is the same in every host theme. The shell's dark themes use a light red with a dark label (dangerInk), so never hardcode white on it.dangerInk?:string | undefined: Text ONdanger: the host's--destructive-foreground, else the readable one of white / near-black.ink:string: Primary text.muted:string: Secondary text.ok:string: Positive colour:status.ok.text.primary:string: Accent (primary buttons, active tab). The host's button colour when tokens were sent.primaryInk:string: Text ON the accent: the host's--primary-foregroundwhen tokens were sent. Never hardcode white onprimary: the shell's default dark theme uses an inverted-mono accent (near-white primary, dark navy text), so white-on-primary renders an unreadable button.status?:Record<Tone, StatusColour> | undefined: The one status palette (the kit's own, in every host theme): whattoneColorreturns and whatBadgepaints, so the two cannot differ. Eachtextreads at 4.5:1 or more onsurfaceandsubtlein all six shell themes and the kit's fallback, and on its ownbackground(app-ui-tones.test.tsx stamps every theme).subtle:string: Subtle fill (table header, neutral button, track backgrounds).surface:string: Card / control background.warn:string: Warning colour (a value past its limit, a stale feed):status.warn.text.
Point
interface Point
One sample in a Series.
Members
x:string | number | Date: A number, or a date / date string (parsed withnew Date); mixing kinds makes the chart treat the axis as time, labelled in the app's clock.y:number | null: The value;nullleaves a gap in the line.
ResolvedPalette
type ResolvedPalette = Palette & {
background: string;
dangerInk: string;
status: Record<Tone, StatusColour>;
};
The palette usePalette returns: every field set, status included.
SelectOption
interface SelectOption
One choice in a SelectField: the value reported by onChange and the label shown.
Members
label:string: Text shown in the dropdown.value:string: Reported byonChangewhen chosen; also the React key, so values must be unique.
Series
interface Series
One line in a TimeSeriesChart.
Members
dashed?:boolean | undefined: Draw the line dashed.name:string: Legend label; also the React key, so names must be unique.points:Point[]: The samples, in x order.tone?:"primary" | Tone | undefined: Line colour (defaultprimary). A status tone also marks the points and the legend with the tone's shape.
StatusColour
interface StatusColour
One status colour: the text / mark colour and the fill a Badge sits it on.
Members
background:string: The badge background behindtext.text:string: Text, dots, bars and lines in this tone.
TabItem
interface TabItem
One tab for Tabs: a stable id, the button label, and the panel content.
Members
content:React.ReactNode: Rendered in the panel while the tab is active.id:string: Identifies the tab;Tabs.initialrefers to it.label:string: Text on the tab button.
TableProps
interface TableProps
Props for Table.
Members
caption?:React.ReactNode: Muted text in the toolbar, left of the filter.columns:Column[]: Column definitions, in display order.emptyText?:string | undefined: Shown in the body when no rows are visible (default "No rows.").exportName?:string | undefined: Export: the file name (CSV of ALL rows after filter+sort, not the page).filterable?:boolean | undefined: Text filter over every displayed cell.onRowClick?:((row: TableRow) => void) | undefined: Makes rows clickable and receives the clicked row. Cells hold no buttons: a row's own action (print its label, open its detail) is a Button beside the table for the row this selects.pageSize?:number | undefined: Rows per page (default 25); a pager appears when there is more than one page.rowKey?:string | undefined: Stable row id: a column key (default: the row index).rows:TableRow[]: All rows; the table filters, sorts and pages them itself.sort?:{ key: string; dir: "asc" | "desc"; } | undefined: Initial sort.
TableRow
type TableRow = Record<string, unknown>;
One data row: a plain object keyed by Column.key. Cell values are formatted by formatCell.
ThemeMode
type ThemeMode = 'light' | 'dark';
The two colour modes; read from html[data-theme], which the SDK runtime stamps from the shell.
TimeRange
interface TimeRange
An ISO-8601 from / to pair, as TimeRangeField reads and reports it.
Members
from:string: Start instant, ISO-8601.to:string: End instant, ISO-8601.
Tone
type Tone = 'ok' | 'warn' | 'critical' | 'neutral';
The status a component shows (ok, warn, critical, neutral): a colour from Palette.status plus a ToneMark shape.