Skip to content
Build with Fungi

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.

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.

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

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.

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.

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.

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

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.

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.

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