Integrate
The React components
@alivelabs/troupe-react is the set of themable React components that Troupe Studio is built from, for
showing and driving devices in your own page. This page covers DeviceScreen, the other components and
theming.
Install
bun add --exact @alivelabs/troupe-react @alivelabs/troupe-clientLike the rest of Troupe, the packages are not on npm yet; Install has the status.
reactandreact-dom19.2 or newer are peer dependencies.- The components run in a browser with
WebSocketand WebCodecs. Server rendering is not supported. - The package ships TypeScript sources, so your bundler must compile TypeScript inside
node_modules: Vite,Bun.buildand esbuild do. - Import the stylesheet once. Your app needs no Tailwind or other CSS framework.
A live device
DeviceScreen shows a session's live stream and lets the viewer drive it.
import { createClient } from '@alivelabs/troupe-client'
import { DeviceScreen } from '@alivelabs/troupe-react'
import '@alivelabs/troupe-react/styles.css'
const troupe = createClient({ baseUrl, token })
const stream = await troupe.call('streams.ticket', { sessionId, cap: 'drive' })
<DeviceScreen
stream={stream}
getTicket={(cap) => troupe.call('streams.ticket', { sessionId, cap: cap ?? 'drive' })}
/>stream is the first { url, ticket }. getTicket mints another when the stream needs one: after a ticket
is refused, and when the viewer presses Take control.
The component handles the rest:
- It decodes the video onto a canvas and reconnects when the socket drops.
- It sends pointer, wheel and keyboard input while the viewer drives. Shift+Escape leaves the device, since Tab and Escape go to it.
- Its toolbar shows the controls the device offers now: Back, Home and the app switcher, Rotate, Lock or Power, Volume, and a Screenshot button that saves the picture as a PNG.
- Its status line says who drives and why input is unavailable when it is.
- It releases the decoder while the page is hidden.
| Prop | Default | What it does |
|---|---|---|
stream | Required | { url, ticket } from streams.ticket, or undefined while there is none |
getTicket | none | Returns a fresh { url, ticket } for the capability it is given |
chrome | true | true shows the toolbar and the status line, 'compact' one toolbar row, false the picture only |
layout | 'intrinsic' | 'intrinsic' keeps the device's aspect ratio; 'fill' takes the parent's size |
maxHeight | none | A cap on the picture's height |
bezel | false | Draws a dark phone frame around the picture |
screenshot | true | false hides the Screenshot button |
fps, bitrate | none | The frame rate and the bits per second to ask for, where the device takes live settings |
adaptive | false | Steps the frame rate and bitrate down and up with the connection |
highlight | none | A frame to outline on the picture, such as the inspector's hovered element |
toolbar | none | Extra toolbar content |
platform | none | The session's platform id. It picks the buttons' faces |
onStatus | none | Called with the connection's status on every change |
onInput | none | Called once the viewer's input has been quiet for a moment |
onRotate | none | Called on Rotate instead of sending the stream's rotate message |
label, className | The view's accessible name, and a class for its root |
The other components
Each of these takes a client prop, or reads the client from a TroupeProvider above it.
| Component | What it does | Main props |
|---|---|---|
TroupeProvider | Makes a client available to the components below it | client |
SessionList | Lists sessions and keeps the list current from sessions.events. End releases one | selectedId, onSelect, onReleased, filter ('live' or 'all') |
CreateSessionForm | A form for sessions.create: the platform, then its device types and OS versions | onCreated, onCancel, holderLabel |
A11yTree | The accessibility inspector: the tree in aria form, a filter, the selected element's properties and a suggested locator | sessionId, onHighlight, onSelect, refreshKey |
Logs | Follows logs.stream, with a text filter and a lowest level | sessionId, capacity |
ScreenshotButton | Takes a media.screenshot and downloads it, or hands it to onCapture | sessionId, format, onCapture |
ErrorNotice | Shows any error as text, with the fix of an environment error | error, onRetry |
Together they make an inspector:
import { useState } from 'react'
import type { TroupeClient } from '@alivelabs/troupe-client'
import type { StreamSource } from '@alivelabs/troupe-client/stream'
import type { SessionId, StreamCap } from '@alivelabs/troupe-protocol'
import {
A11yTree,
type DeviceHighlight,
DeviceScreen,
Logs,
TroupeProvider,
} from '@alivelabs/troupe-react'
interface InspectorProps {
troupe: TroupeClient
sessionId: SessionId
stream: StreamSource
getTicket: (cap?: StreamCap) => Promise<StreamSource>
}
function Inspector({ troupe, sessionId, stream, getTicket }: InspectorProps) {
const [highlight, setHighlight] = useState<DeviceHighlight | null>(null)
const [driven, setDriven] = useState(0)
return (
<TroupeProvider client={troupe}>
<DeviceScreen
stream={stream}
getTicket={getTicket}
highlight={highlight}
onInput={() => setDriven((count) => count + 1)}
/>
<A11yTree sessionId={sessionId} onHighlight={setHighlight} refreshKey={driven} />
<Logs sessionId={sessionId} />
</TroupeProvider>
)
}Hovering a line in the tree outlines that element on the device. Driving the device changes refreshKey,
so the tree takes a new snapshot.
For a view of your own, the package also exports the logic behind the components: useTroupeClient,
useResource for loading a value with its state, ariaRows and suggestLocator for the tree, appendLogs
and filterLogs for logs, applySessionEvent and buildSpec for sessions, and describeError.
Text that comes from a device or an app, such as an accessibility label or a log line, is always rendered as text, never as HTML.
Theming
styles.css is one plain stylesheet. Its classes start with troupe-, and it has no global reset, so it
does not change the rest of your page.
Every colour, border and focus ring reads one of 24 custom properties. Each has a light and a dark default
through CSS light-dark(), so the components follow your page's color-scheme with no work. To theme them,
set the properties on a container:
.device-panel {
color-scheme: dark;
--troupe-bg: #0b1020;
--troupe-surface: #141a2e;
--troupe-border-focus: #9d7bff;
--troupe-accent: #2a2350;
}| Group | Properties |
|---|---|
| Surfaces | --troupe-bg, --troupe-surface, --troupe-accent |
| Borders | --troupe-border, --troupe-border-strong, --troupe-border-focus |
| Text | --troupe-fg, --troupe-fg-muted, --troupe-fg-dim |
| States | --troupe-ok, --troupe-pill-idle-bg, --troupe-pill-idle-fg |
| Errors | --troupe-danger-surface, --troupe-danger-line, --troupe-danger-fg |
| Notices | --troupe-notice-surface, --troupe-notice-border, --troupe-notice-fg |
| Logs | --troupe-log-stdout, --troupe-log-stdout-tag, --troupe-log-stderr, --troupe-log-stderr-tag, --troupe-log-system, --troupe-log-system-tag |
The package exports the list as THEME_VARIABLES. The bezel is not themed: a phone's frame is dark on any
page.
Next
- The live stream: what
DeviceScreendoes underneath - The client SDK: the client the components call
- Troupe Studio: the app built from these components