FLOWDocs

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.0

Provider

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

On this page