Build with Caps
Compose App controls, forms and conversations with the shared styles and theme.
Caps supplies the React controls and presentation blocks used by Fungi’s Apps. Stipe supplies their themes, colors, spacing and fonts. Your App supplies the data, actions and permission decisions.
Start with one button
Section titled “Start with one button”Use a configured Fungi source workspace with Caps and its dependencies built. The component needs React and React DOM. Caps accepts React 18 or 19. Import a specific component subpath. Caps has no root component export.
Import the compiled stylesheet and fonts once at your browser entry. This example uses the same style entries as the Caps workbench. Your bundler must load CSS imports.
Illustrative: this component belongs in an existing React host with the
workspace dependencies built. The host supplies pending and onSave; the
example does not perform a file write.
import "@fungi.computer/caps/styles.css";import "@fungi.computer/caps/fonts.css";import { Button } from "@fungi.computer/caps/components/button";
export function SaveButton({ pending, onSave,}: { pending: boolean; onSave: () => void;}) { return ( <Button type="button" variant="primary" loading={pending} onClick={onSave}> Save changes </Button> );}The ready button says Save changes. When pending is true, it shows its
loading indicator, marks itself busy and disables the native button. The host
starts the save, handles any failure and updates pending.
For an App inside Fungi, use its admitted Agar connection for operations. See Build an App for connecting and requesting access. A styled button does not grant file or model permissions.
Choose a style entry
Section titled “Choose a style entry”| Entry | Use |
|---|---|
@fungi.computer/caps/styles.css |
Compiled Caps styles for a browser host |
@fungi.computer/caps/tailwind.css |
Source styles for a host compiling with Tailwind |
@fungi.computer/caps/fonts.css |
The shared fonts |
@fungi.computer/stipe/theme |
Theme observation and application in a browser host |
The source style entry imports Stipe and scans Caps’ source. Use the host’s Tailwind setup when your own classes also need compilation. Avoid loading both style entries merely to get a component working.
For a conversation App, use @fungi.computer/parakeet/styles.css instead of
Caps’ compiled sheet. Parakeet’s sheet includes the Caps/Stipe foundation. Its
/tailwind.css entry is the alternative for a host compiling its own utilities.
Import the shared fonts separately.
Follow the host theme
Section titled “Follow the host theme”Use Agar’s mountApp from @fungi.computer/agar/react for an App frontend. It
applies the host theme, follows updates, and restores the prior theme when its
returned disposer runs. Do not add a second theme subscription around that
mount. The App tutorial shows the complete lifetime.
Caps uses Stipe’s inherited CSS tokens. Latte and Mocha are the defaults; nested
data-theme scopes select materials such as phosphor, codec or paper.
Compose Surface for a material frame, Screen for its display face, or
Display for their framed combination. Keep spacing and placement in your App;
use component props and theme tokens for appearance. Avoid descendant selectors
that repaint Caps controls or copy their borders, bevels and shadows.
A nested theme follows the DOM, including portals. Put a dialog or menu in the appropriate themed portal container if it must inherit that local material. An overlay portalled elsewhere inherits the theme at its actual destination.
Build a form
Section titled “Build a form”Caps’ Form and FormFooter supply native form markup and presentation.
@fungi.computer/caps/form connects that presentation to TanStack Form. In the
workspace, the adapter’s peers are @tanstack/react-form 1.33.2 and Effect
4.0.0.
| API | Responsibility |
|---|---|
FormField |
Binds a TanStack field to its label, description and readable error; shows errors after blur or a submission attempt. |
effectValidator |
Adapts an Effect Schema to Standard Schema for TanStack validation, preserving nested field paths. |
fieldError |
Reads the first nonempty string or schema issue message from field metadata. |
SubmitFooter |
Renders a native submit button using the form’s canSubmit and isSubmitting, plus any explicit unavailable condition. |
Give schema checks human messages. The adapter surfaces those messages; it does not turn raw schema diagnostics into product copy. TanStack keeps input values; decode separately if your schema transforms a value before saving it.
Illustrative: this component runs in a configured React App. Its caller supplies
saveName, which resolves only when the save has finished and rejects on
failure.
import { useRef, useState } from "react";import { useForm } from "@tanstack/react-form";import { Schema } from "effect";import { Form } from "@fungi.computer/caps/components/field";import { Input } from "@fungi.computer/caps/components/input";import { FormField, SubmitFooter, effectValidator,} from "@fungi.computer/caps/form";
const nameSchema = Schema.String.check( Schema.isMinLength(1, { message: "Enter a name." }),);const validate = effectValidator(Schema.Struct({ name: nameSchema }));
export function NameForm({ saveName,}: { saveName: (name: string) => Promise<void>;}) { const element = useRef<HTMLFormElement>(null); const [error, setError] = useState<string | undefined>(); const form = useForm({ defaultValues: { name: "" }, canSubmitWhenInvalid: true, validators: { onChange: validate, onSubmit: validate }, onSubmitInvalid: () => element.current?.querySelector("input")?.focus(), onSubmit: async ({ value }) => { setError(undefined); try { await saveName(value.name); } catch (cause) { setError("Couldn't save the name. Try again."); console.error("Name save failed", cause); } finally { const input = element.current?.querySelector("input"); if ( input && input.ownerDocument.activeElement === input.ownerDocument.body ) input.focus(); } }, }); return ( <Form ref={element} aria-label="Change a name" noValidate onSubmit={(event) => { event.preventDefault(); if (!form.state.isSubmitting) void form.handleSubmit(); }} > <FormField form={form} name="name" label="Name" required> {(field) => ( <Input name={field.name} value={field.state.value} onBlur={field.handleBlur} onChange={(event) => field.handleChange(event.currentTarget.value)} /> )} </FormField> <SubmitFooter form={form} label="Save name" error={error} /> </Form> );}Keep the primary submit’s variant stable. canSubmitWhenInvalid: true lets a
person submit invalid input and see the validator’s error; onSubmit validates
again before saving. It does not authorize an unavailable operation. Use the
footer’s disabled prop for missing access or another real prerequisite. Await
the mutation so the footer stays busy until it settles. Preserve entered values
after failure. Use Form busy={pending} when the whole form must be locked; its
native fieldset disables every control.
Keep controls usable
Section titled “Keep controls usable”Give controls visible labels. Give an icon-only control an accessible name. Use
native button types deliberately, especially inside forms. Keep error text next
to the action that failed and make status changes available to assistive
technology. Disabled filled Button variants keep the same face and shape at
0.45 opacity; an outline variant remains outline. Do not switch a primary button
to outline because a form is invalid or pending. Native disabled controls cannot
receive focus or activate. aria-disabled communicates a state but does not by
itself block an action.
Keep Caps’ focus indicator. The App owns focus after its mutation changes or removes controls; Caps does not automatically restore form focus after a request. On validation failure, focus the relevant field. When restoring focus after asynchronous work, do not steal it if the person has moved elsewhere. Check Tab, Shift+Tab, Enter and Escape in the mounted App, including pending and failed requests, long input, and narrow windows.
Caps dialogs and menus use Base UI’s focus and keyboard behavior. In an App window, supply the appropriate mounted portal container and let the host own window interaction where required. A null container is not a mounted window. Test keyboard focus and Escape with the actual host.
Button can render a child such as a link with asChild. In that case its busy
and disabled state is expressed with ARIA attributes. The host still owns
preventing the link’s navigation. Native button disabling does not transfer to a
link.
Compose larger views
Section titled “Compose larger views”Use Caps blocks for recurring presentation such as an empty state, composer, upload queue or permission grid. Keep network work and authorization in the host rather than putting them into presentation props.
Parakeet presents an authorized Agent Session and owns conversation interaction.
An App that passes its Query client to Agar’s mountApp can render the browser
view inside that existing provider.
Illustrative: the App has a mounted Query provider and supplies an authorized
ParakeetSession. This component does not create an Agent or obtain access.
import { Parakeet, type ParakeetSession } from "@fungi.computer/parakeet";
export function Conversation({ session }: { session: ParakeetSession }) { return <Parakeet session={session} />;}Parakeet includes its provider and view. Use ParakeetProvider with
ParakeetView when composing additional controls in the same Session scope. The
App owns the Session handle and releases it according to its capability
lifetime. Unmounting the view releases observation; it does not cancel work
already accepted by the Session.
The composer supports Enter to send, Shift+Enter for a newline, and its slash
menu’s arrow, Tab and Escape controls. The host can supply its Whistle runtime
through commands; it does not need a second slash parser or key listener. The
default action is primary. Grove selects the supported
composerActionVariant="outline" for its quieter composer; that is an explicit
host choice, not a disabled-state swap.
Sprite renders the Agent character. Caps alone does not create a Session, select a model or authorize an Agent.
The workspace package contains generated API reference in docs/api/README.md.
Its workbench demonstrates the components, overlay composition and themes. The
editable-source registry is a separate local workflow. Do not substitute a
guessed public registry endpoint for the package’s exported entrypoints.