Focus
Practice-session provider, panel, and study-subject picker from @flow-industries/ui/focus.
@flow-industries/ui/focus is an opt-in entry point for Flow's practice sessions — starting and running Meditate/Study actions, and picking a study subject. It ships FocusProvider, FocusPanel, StudySubjectPicker, and their hooks and types. It is a separate subpath so apps that use only the base components never pull in the Flow ID SDK.
Install the SDK peer
The provider talks to Flow ID through a FocusApi, so pair the UI package with @flow-industries/id 0.22.0 or newer. The peer is optional for apps that import only other UI components.
bun add @flow-industries/id@^0.22.0Provider
Build one FocusApi with createFocusApi(authHost, getToken, surface) and keep it stable — credentials and the surface belong to the host, not the provider. Wrap the practice UI in FocusProvider, passing a stable authenticated account id as viewerId (or null for an inert provider).
import { createFocusApi } from "@flow-industries/id/focus";
import { FocusProvider } from "@flow-industries/ui/focus";
const api = createFocusApi(authHost, getToken, "web");
<FocusProvider api={api} viewerId={viewerId}>
<Practice />
</FocusProvider>;The provider restores the active session on mount and refreshes when the document becomes visible; an account change clears session and subject state and discards in-flight responses. The host keeps its guest, disabled-account, and embedded-surface policy — omit the provider (or pass viewerId={null}) wherever practice must be excluded.
Panel
FocusPanel renders the whole flow — subject picker, running timer, and the finished/conflict states — from a focus value read through useFocus(). It also takes an optional managementLink, a surface ("game" | "talk" | "web"), and a text translator.
import { FocusPanel, useFocus } from "@flow-industries/ui/focus";
function Practice() {
const focus = useFocus();
return <FocusPanel focus={focus} surface="web" />;
}Hooks
Read state without polling: useFocus() returns the full context (state, catalog, study, and the start/pause/resume/stop/createSubject actions), useActiveAction() returns the running action or null, and useFocusElapsed() returns whole seconds elapsed on the local clock, frozen while paused. Outside a provider every hook returns an inert value, so a signed-out caller reads as idle rather than erroring.
import { useActiveAction, useFocusElapsed } from "@flow-industries/ui/focus";
const active = useActiveAction();
const seconds = useFocusElapsed();Starting a session
start() resolves to "started", "conflict", or "failed". A "conflict" means a session is already running; the provider keeps that attempt as pendingStart, so a "Switch" control can repeat it with replace: true to finish the old session and open the new one.
const outcome = await focus.start({ source: "action.study", subjectId });
if (outcome === "conflict") {
await focus.start({ source: "action.study", subjectId, replace: true });
}Subject picker
StudySubjectPicker is fully controlled: pass catalog, subjects, busy/failed flags, and onPick/onCreate callbacks — the host owns the requests when using it alone. Arrow keys navigate, Enter selects, and Escape clears the search or steps back out of a category. Private subject names are marked data-user-content and bidi-isolated so a right-to-left label can't reflow the surrounding chrome.
import { StudySubjectPicker } from "@flow-industries/ui/focus";
<StudySubjectPicker
catalog={catalog}
subjects={subjects}
busy={busy}
onPick={(subject) => focus.start({ source: "action.study", subjectId: subject.id })}
onCreate={(request) => focus.createSubject(request)}
/>;Localization
FocusPanel and StudySubjectPicker take a text translator. Build one with createFocusText(locale, messages) — pass a locale for number and plural formatting and an optional partial message map to override any string.
import { createFocusText } from "@flow-industries/ui/focus";
const text = createFocusText("es", { /* FocusTextKey → string overrides */ });
<FocusPanel focus={focus} surface="web" text={text} />;Source
src/focus/provider.tsx—FocusProvider,useFocus,useActiveAction,useFocusElapsed.src/focus/panel.tsx—FocusPanel.src/focus/study-subject-picker.tsx—StudySubjectPicker.src/focus/text.tsx—createFocusText,FocusTextProvider,useFocusText.