Compose actions and labelled fields
Use the public controls for every ordinary button, badge, label, and text control in a complete
application. They carry the SDK's theme, focus, hover, touch-size, and accessibility rules, so an
application does not restate them and the browser verifier's touch-target and input-zoom rules
are satisfied by construction.
Import the components from the public entrypoint:
import {
Badge,
Button,
Field,
Input,
Label,
Textarea,
useFieldControlProps,
} from "@dev-mainsequence/command-center-sdk/controls";
Load the package theme and component styles once near the application root:
import "@dev-mainsequence/command-center-sdk/theme/styles.css";
import "@dev-mainsequence/command-center-sdk/styles.css";
Give each rule one owner
Every interactive control renders with the cc-control class. That class reads
--application-control-min-size, which the theme raises from 36px to 44px on a coarse pointer, so
a consumer's buttons and inputs grow with the SDK's own. Text controls keep a 16px minimum font
size on a coarse pointer so iOS Safari does not zoom on focus. Do not restate these in application
CSS and do not override the control minimum size and still claim conformance.
The controls own presentation and accessible wiring only. They have no form state, validation, submission, router, transport, or persistence dependency. Values, validation rules, when a message is an error, submission, and mutation policy stay in the application.
Compose page and card actions
Place actions in the layout header or a card header and choose one primary action per surface:
import { Button } from "@dev-mainsequence/command-center-sdk/controls";
import {
ApplicationPage,
ApplicationPageHeader,
} from "@dev-mainsequence/command-center-sdk/layout";
export function PortfolioPage({ rebalance, rebalancing }: PortfolioPageProps) {
return (
<ApplicationPage maxWidth="wide">
<ApplicationPageHeader
actions={
<>
<Button onClick={exportPositions}>Export</Button>
<Button onClick={rebalance} pending={rebalancing} variant="primary">
Rebalance
</Button>
</>
}
title="Portfolio overview"
/>
</ApplicationPage>
);
}
variant is outline (default), primary, secondary, ghost, or danger. size is
small, medium (default), or large. pending reports the action as busy, shows the activity
indicator, ignores clicks, and keeps the button focusable; the application decides when the
action is pending. The rendered element is a <button> with type="button" unless type is
supplied. Use type="submit" inside a form the application handles.
An icon-only button is a square control and needs an accessible name:
<Button aria-label="Remove position" iconOnly variant="ghost">
<Trash2 aria-hidden="true" />
</Button>
Links keep native anchor semantics. Do not wrap a Button in an anchor or style an anchor as a
button with these classes; use the navigation primitives or a plain link.
Compose a labelled field
Field owns the relationship between a label, its control, a description, and an error:
import { Field, Input, Textarea } from "@dev-mainsequence/command-center-sdk/controls";
export function ServiceForm({ errors, onChange, values }: ServiceFormProps) {
return (
<form onSubmit={onSubmit}>
<Field
description="Shown in the catalog and in exports."
error={errors.name}
label="Display name"
required
>
<Input
autoComplete="off"
name="name"
onChange={(event) => onChange("name", event.target.value)}
value={values.name}
/>
</Field>
<Field label="Notes">
<Textarea
name="notes"
onChange={(event) => onChange("notes", event.target.value)}
rows={4}
value={values.notes}
/>
</Field>
</form>
);
}
The field generates the control id, targets it from the label, joins the error and description
into aria-describedby, sets aria-invalid while error is present, sets aria-required when
required, and propagates disabled. Pass controlId when a stable id is needed for tests or
deep links. Explicit id, aria-describedby, disabled, and invalid values on the control
win over the field's values.
required on the field is presentational and assistive. Add the native required attribute to the
control when the browser should block submission; leave it off when the application validates on
its own terms.
Show error only when the application has decided the value is invalid, for example after a blur,
a submit attempt, or a server response. Do not render an error for an untouched empty field on
first paint.
Wire a custom control into a field
A picker, an editor, or another composite joins the field through the public hook:
import { useFieldControlProps } from "@dev-mainsequence/command-center-sdk/controls";
import { ResourcePicker } from "@dev-mainsequence/command-center-sdk/views";
function CurrencyPicker(props: CurrencyPickerProps) {
const control = useFieldControlProps({ invalid: props.invalid });
return <ResourcePicker {...props} triggerProps={control} />;
}
<Field error={errors.currency} label="Settlement currency" required>
<CurrencyPicker onChange={setCurrency} options={currencies} value={currency} />
</Field>;
The hook returns id, aria-describedby, aria-invalid, aria-required, and disabled as the
field resolves them and omits every value that does not apply. Outside a Field it returns only
the explicit values, so the control can call it unconditionally.
Use Label on its own only when no Field owns the control, for example a search box in a
toolbar:
<Label htmlFor="positions-search">Search positions</Label>
<Input id="positions-search" type="search" />
Mark status with a badge
import { Badge } from "@dev-mainsequence/command-center-sdk/controls";
<Badge variant={deployment.healthy ? "success" : "danger"}>
{deployment.healthy ? "Healthy" : "Failing"}
</Badge>;
variant is neutral (default), primary, secondary, success, warning, or danger. Map
domain status to a tone in the application. A badge is static; do not attach a click handler to
it. Use ResourceStatusCell from /views inside a resource table.
Split a page into sections with tabs
Tabs are not a /controls component. A page about one object whose sections sit under a summary
is a ResourceDetailShell, and the shell's tabs carry the keyboard model, the tab panel, disabled
tabs, leading icons, and the More menu for tabs that do not fit (SDK ADR 015). Pass tabs,
activeTabId, and onTabChange, and resolve hidden or disabled tabs with
resolveResourceDetailTabs; see Build a resource detail.
Do not build a tab strip from Buttons that swap variant, and do not put one in an
ApplicationCard header.
Migrate from an application kit
Replace private button, input, textarea, and badge components with the SDK controls and delete
the private files. A private tab component maps to the tabs of ResourceDetailShell. A kit whose default button was the filled primary needs variant="primary" at
the call sites that meant it; the SDK default is outline. size="sm", "lg", and "icon"
become size="small", size="large", and iconOnly. A private card or page header maps to
ApplicationCard and ApplicationPageHeader from /layout. Remove application CSS that
reproduced the control geometry, focus ring, or touch sizing from SDK variables.
Verify in a real browser
Run the layout verifier from /layout/testing on a route that renders the controls. Coarse
entries report touch-target; entries narrower than 768px report input-zoom. Configure
hasTouch and isMobile on the Playwright context for coarse entries.
import { expect, test } from "@playwright/test";
import {
assertCommandCenterPageLayout,
} from "@dev-mainsequence/command-center-sdk/layout/testing";
test.use({ hasTouch: true, isMobile: true, viewport: { width: 375, height: 812 } });
test("service form controls are touch-sized", async ({ page }) => {
await page.goto("/services/new");
await expect(page.getByRole("button", { name: "Save" })).toBeVisible();
await assertCommandCenterPageLayout(page);
});
Run the closed theme audit separately:
npx command-center-sdk theme audit --path src
Exercise each variant and size, the pending state, a field with and without a description and error, a disabled field, at least one dark and one light SDK theme, 375×812 with a coarse pointer, and 1280×800.
Backend and storage boundary
The control props are React presentation input, not a backend contract. There is no controls contract ID, JSON Schema, fixture bundle, persisted state, or migration. Form payloads, validation responses, and error shapes are application and backend concerns.