Use when the design reaches a web application: write its prototype (prototype.json + prototype.tsx), a clickable, checked app on the data model and the roles that the reviewer uses before anything is built. A coding run that builds a web-application reads its references/implementing.md.
Installs into .claude/skills of the current project.
Are you the author of Prototype?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/wso2-prototype)
---
name: prototype
description: "Use when the design reaches a web application: write its prototype (prototype.json + prototype.tsx), a clickable, checked app on the data model and the roles that the reviewer uses before anything is built. A coding run that builds a web-application reads its references/implementing.md."
metadata:
aep:
kind: platform
audience: [design, coding]
---
# Prototype
A coding run that builds a web-application reads `references/implementing.md`.
The rest of this file is how the design agent writes a prototype.
A prototype is the reviewable picture of a `web-application` before it is
built: its screens, navigation, roles, flows and display states, filled with
mock data, running in a sandbox inside the review page. It is the web
application's artifact in the design. The `design` flow writes it after the
data model and the roles, and before the API contracts, which then serve its
screens. The business reviewer uses it and comments on it, and the build
makes what it shows.
The screens are a React module written against a fixed kit of components
(`@wso2/prototype-kit`). You compose screens from the kit; you do not build
components, style anything, or reach outside the frame.
## Inputs
Read these before writing anything:
- `specs/design/design.cell`: every `component … web-application` line is one
prototype to write. No web-application means there is nothing to do: say so
and stop. When the turn names components, write only those.
- `specs/design/domain-model.md`: the records. Each entity that a screen shows
or changes is a collection in the mock data, and its fields are the
entity's attributes (see **Mock data**).
- `specs/design/security.json`: the roles. When the file exists, each
prototype role's `id` is a role's `name` from it, **copied verbatim** (same
case, same spelling), and its `name` is a short human label for that role.
Include exactly the roles that use this application. Each role's
`description` says what the role does and what it never sees: show each role
only the screens, controls and records that its description allows. Without
the file, take the roles from the PRD's actors and give them short
kebab-case ids.
- The web application's `design.json`: `stories` lists the stories it serves.
- The requirements of those stories: each feature's file,
`specs/requirements/features/F<n>-<slug>.md`, with its stories (`F<n>.<m>`)
and its Decisions, the `*assumed*` lines included; `product-wide.md` for the
items that apply to it; and `prd.md`'s Actors, for the people your records
are about. Every story the application serves gets at least one screen. Set
a story aside only when the product gives it no view (sign-in through the
platform's SSO, a machine-facing endpoint), and name it in your closing.
- A sketch or mockup the user attached: the layout brief. Refine it; do not
reinvent it. Where it and the stories disagree, the stories' scope wins and
the sketch's layout intent survives inside it.
- `specs/design/components/<component>/prototype.json` and `prototype.tsx` if
they already exist: you are revising them, not starting over (see **Stable
ids**).
## Output
Two files per web-application, in `specs/design/components/<component>/`,
where `<component>` is the web-application's name in the cell:
1. **`prototype.json`, the manifest**: the roles, display states, screens and
flows the reviewer picks from. Write it **first**.
2. **`prototype.tsx`, the screens**: one React component per manifest screen,
exported with `defineApp`. It is checked against the manifest already
written.
Write a new file with ONE `addFile` of the whole file. Revise an existing one
with `editFile` edits: the file is already in front of you, and an edit costs
what it changes. Never write a partial file to finish later.
Each write of `prototype.tsx` is type-checked alone. A change to a record type
and its uses (a field that you add, rename or remove) cannot go in as one edit
per line: the first edit leaves the other uses wrong, and the check refuses
it (`TYPE_ERROR`, one finding for each wrong line). Write such a change, or
any other change to many lines, in one write: `removeFile`, then ONE `addFile`
of the whole corrected file, with each id as it was.
This step writes only these two files. A screen can need a record, a field or
a role that the data model or `security.json` does not have. Then change that
file first (it comes earlier in the design), and write the screen after it.
Do not change a requirement here: the design's gap check writes the
requirements.
Every write is checked as it lands: the manifest's shape and references, the
source's static rules, then a render of every screen for every role that reaches
it in every display state. A refusal lists every finding with its code and
where (a JSON path in the manifest, a line in the source, or the screen, role
and state that failed to draw). Fix all of them and retry once. The codes are
listed at the end of **The kit**.
## The manifest
Plain JSON, schema version 3. Every object is closed: a key not shown here is
refused.
```json
{
"schemaVersion": 3,
"name": "Expense approvals",
"entryScreen": "screen.queue",
"roles": [{ "id": "Approver", "name": "Approver" }],
"states": [
{ "id": "state.default", "name": "Default" },
{ "id": "state.empty", "name": "Nothing to show" },
{ "id": "state.validation-error", "name": "Validation errors" },
{ "id": "state.failed", "name": "Expense feed failed" }
],
"screens": [
{ "id": "screen.queue", "name": "Approval queue", "roleIds": ["Approver"] },
{ "id": "screen.detail", "name": "Expense detail", "roleIds": ["Approver"] },
{ "id": "screen.account", "name": "Account", "roleIds": ["Approver"] },
{ "id": "screen.signed-out", "name": "Signed out", "roleIds": ["Approver"] }
],
"flows": [
{ "id": "flow.approve", "name": "Approve an expense", "roleId": "Approver",
"screenIds": ["screen.queue", "screen.detail"] }
]
}
```
- `roles`, `states` and `screens` have at least one entry; their ids and the
flows' ids share one namespace and are unique together. `entryScreen` is the
screen the prototype opens on. `name` is the application's name.
- A screen's `roleIds` are the roles that reach it. A flow is one role's walk
through screens that role reaches, in order, for one story. Give each story
that has a view a flow, named for what the actor does ("Submit a claim").
- The app shell's screens, an account screen and a signed-out screen, go to
**every** role: every user has them.
- A settings screen exists only for the settings that the requirements name
(a notification choice, a default that a user picks). It goes to the roles
that set them, and it holds those settings and nothing else. When the
requirements name no setting, write no settings screen. Never invent a
preference (a digest email, a Slack alert, a compact view) to fill one.
- `states` always starts with a default state (`state.default`); add one per
presentation a reviewer must see:
- `state.empty` wherever a list can be empty.
- Wherever the product can refuse or fail what a screen does, the states
that the refusal or the failure produces. A form that a requirement limits
(a required receipt, a cap) shows its validation errors in
`state.validation-error`: its Fields' `error` and a ValidationSummary.
A screen whose data or action can fail (an outside service it needs, a
save) gets `state.failed`: an error Alert saying what failed and what the
user can do. A step that can be slow (an outside service, a long-running
job) gets `state.delayed`: an info or warning Alert saying the result is on
its way.
A prototype with only default and empty states hides the presentations a
reviewer most needs to judge.
## The screens
```tsx
import { useState, type ReactNode } from "react";
import {
Alert, AppShell, Button, Detail, Dialog, EmptyState, Field, Form, Heading, Screen, Section, Stat,
StatGroup, Table, defineApp, useCollection, useDisplayState, useNav, useParams,
} from "@wso2/prototype-kit";
interface Expense { id: string; employee: string; amount: string; status: string }
const expenses: Expense[] = [
{ id: "1042", employee: "Maya Fernando", amount: "$148.20", status: "Awaiting approval" },
{ id: "1039", employee: "Ravi Perera", amount: "$62.00", status: "Awaiting approval" },
{ id: "1031", employee: "Ana Silva", amount: "$310.00", status: "Approved" },
];
const user = { name: "Ravi Perera", email: "ravi@acme.example" };
function Shell({ children }: { children: ReactNode }) {
return (
<AppShell
id="shell"
user={user}
nav={[{ id: "nav.queue", label: "Approval queue", to: "screen.queue" }]}
account="screen.account"
signOut="screen.signed-out"
>
{children}
</AppShell>
);
}
function Queue() {
const state = useDisplayState();
const all = useCollection<Expense>("expenses");
const waiting = state === "state.empty" ? [] : all.items.filter((e) => e.status === "Awaiting approval");
const approved = all.items.filter((e) => e.status === "Approved").length;
return (
<Shell>
<Heading id="heading.queue" text="Approval queue" />
{state === "state.failed" && (
<Alert id="alert.feed-failed" tone="error" title="Expense feed unavailable" text="Amounts may be stale. Refresh in a few minutes." />
)}
<StatGroup>
<Stat id="stat.waiting" label="Awaiting you" value={String(waiting.length)} hint="claims to decide" icon="Inbox" tone="warning" />
<Stat id="stat.approved" label="Approved" value={String(approved)} hint="this month" icon="CircleCheck" tone="success" />
</StatGroup>
<Section id="section.expenses" title="Expenses" count={waiting.length} subtitle="Oldest first. Open a claim to reject it with a reason.">
<Table
id="table.expenses"
columns={["Employee", { label: "Amount", kind: "number" }, { label: "Status", kind: "status" }]}
rows={waiting.map((e) => ({
id: `expense.${e.id}`,
cells: [e.employee, e.amount],
status: { text: e.status, tone: "warning" },
to: "screen.detail",
params: { expense: e.id },
actions: [{ id: `expense.${e.id}.approve`, label: "Approve", onPress: () => all.update(e.id, { status: "Approved" }) }],
}))}
empty={<EmptyState id="empty.queue" title="Nothing to approve" text="New claims appear here." />}
/>
</Section>
</Shell>
);
}
function ExpenseDetail() {
const { expense: id } = useParams();
const state = useDisplayState();
const navigate = useNav();
const all = useCollection<Expense>("expenses");
const [rejecting, setRejecting] = useState(false);
const expense = (id ? all.get(id) : undefined) ?? all.items[0]!;
const decide = (status: string) => {
all.update(expense.id, { status });
navigate.go("screen.queue");
};
return (
<Shell>
<Heading id="heading.detail" text={`Expense ${expense.id}`} />
<Detail
id="detail.expense"
fields={[
{ label: "Employee", value: expense.employee },
{ label: "Amount", value: expense.amount },
]}
/>
<Button id="btn.approve" label="Approve" emphasis="primary" onPress={() => decide("Approved")} />
<Button id="btn.reject" label="Reject" emphasis="danger" onPress={() => setRejecting(true)} />
<Dialog id="dialog.reject" title={`Reject expense ${expense.id}`} open={rejecting} onClose={() => setRejecting(false)}>
<Form
id="form.reject"
onSubmit={() => decide("Rejected")}
actions={<Button id="btn.reject.confirm" label="Reject" emphasis="danger" submit />}
>
<Field
id="field.reason"
label="Reason"
type="textarea"
required
error={state === "state.validation-error" ? "Give the employee a reason" : undefined}
/>
</Form>
</Dialog>
</Shell>
);
}
function Account() {
return (
<Shell>
<Heading id="heading.account" text="Account" />
<Detail id="detail.account" fields={[{ label: "Name", value: user.name }, { label: "Email", value: user.email }]} />
</Shell>
);
}
function SignedOut() {
return (
<Screen>
<Heading id="heading.signed-out" text="You are signed out" />
<Button id="btn.sign-in" label="Sign in" emphasis="primary" to="screen.queue" />
</Screen>
);
}
export default defineApp({
screens: {
"screen.queue": Queue,
"screen.detail": ExpenseDetail,
"screen.account": Account,
"screen.signed-out": SignedOut,
},
data: { expenses },
});
```
- **One component per manifest screen**, keyed by the screen's id in
`defineApp({ screens })`: exactly the manifest's screens, no more, no less.
- **Every screen sits in the app shell.** Write one `<AppShell>` in a small
local component (`Shell` above) and make it every screen's root: it draws
the product's header, the signed-in user and the user menu (Account,
Settings, Sign out) and the side navigation. Its `account` and `signOut`
are screens you write: an account screen with the user's details, and a
signed-out screen. Only that signed-out screen, outside the product, has a
bare `<Screen>` as its root, with a way to sign back in. Give `settings`
only when the prototype has a settings screen (see the manifest rules):
without it, the user menu has no Settings entry.
The shell's `menu` adds the application's own user-menu entries (a profile,
billing) before Sign out. A reviewer points at an entry by
opening the menu in Preview, then switching to Annotate.
- **One shell serves every role.** A navigation entry is drawn only for the
roles that reach its screen. Give `user` the person each role signs in as
(pick by `useRole()` when roles differ); the header shows the viewing role's
name beside them.
- **Read the review through the hooks.** `useDisplayState()` is the display
state the reviewer chose and `useRole()` the role they view as: branch on
them to show an error, an empty list, a role's own controls. Show a control
only to the roles that reach where it leads: a `to` a role cannot follow is
refused.
- **Navigate with `to`** on a Button, Link, table row, row action or breadcrumb (with
`params` the target reads through `useParams()`); `useNav().go(...)` inside
an `onPress` for a navigation that follows an action. Every screen must
also draw **without** params (the reviewer can open any screen from the
pickers), so fall back to the first record.
- **Overlays are the screen's own state**: `const [open, setOpen] =
useState(false)` and `<Dialog open={open} onClose={() => setOpen(false)}>`.
Tabs and Steppers keep their own position, or take `active` and `onChange`
when a button moves them (a wizard's Next).
- **Forms submit through the kit.** A `<Form onSubmit>` with a `<Button submit>`
validates its Fields (`required`, `pattern`) and then calls `onSubmit` with
their values keyed by `name` (default: the Field's id). A Field with no
`value` holds what is typed into it; give it `defaultValue` to start filled.
- **The mock data is `defineApp({ data })`.** An array of records that each have
a string `id` is a collection: read it with `useCollection(name)` and change
it with `create`, `update` and `remove`, so the next screen shows the result.
Anything else is a single value for `useValue(key)`. The change lives in
memory until the reviewer presses Reset data; nothing is sent anywhere.
What a prototype may not use is refused as it lands: imports other than `react`
and `@wso2/prototype-kit`; raw HTML elements (`<div>`, `<table>`, `<img>`, …),
because every element is a kit component; the page, storage, network and
code-generation APIs (`window`, `document`, `fetch`, `eval`, …); and anything
nondeterministic (`Math.random()`, `Date.now()`, `new Date()` with no argument;
use `useToday()` for today's date). Styles do not exist: the theme draws every
component, and a component takes no `className` or `style`.
### What the reviewer sees
The review renderer draws only what the screen component renders: kit
components, inside the chrome its `<AppShell>` draws (the header with the
product's name and the signed-in user, the user menu with Account, Settings
when the shell names a settings screen, and Sign out, the side navigation).
A bare `<Screen>` is drawn with no chrome at all.
Nothing else is drawn automatically: no notification bell, search, theme
toggle, help menu or footer, whatever the built application's shell will
carry. If a reviewer asks for such chrome, model it in the kit: a navigation
entry, or a Button or Link in the screen's content (a Heading's `actions` for
the top right) whose `to` is the screen it leads to or whose `onPress` opens a
Drawer (a notification list) or a Dialog (a confirmation). Only if what they
ask for is outside the kit, say so in your reply and name the limitation.
Never tell a reviewer the platform draws something the screen does not render.
### The Oxygen look
The reviewer sees the prototype in the Oxygen theme, the look of WSO2 products,
so a prototype should read as an Oxygen screen. The host applies the theme: the
source never imports one, never styles anything, and never names Oxygen. Your
part is composing screens the way an Oxygen application composes them:
- **Page anatomy.** A screen is the `<AppShell>` holding a `<Heading>` (the
page title, with page-wide action Buttons in `actions`), then the content in
the order a user works through it: headline numbers as one `<StatGroup>` of
`<Stat>`s, then a `<Section>` per part of the page (a list, a form, a
record), with `<Filters>` and the `<Table>` inside it, and `<Detail>`,
`<Form>` and `<Timeline>` for a single record. A sub-page opens with
`<Breadcrumbs>` back to its list.
- **Stats in a group.** Put a screen's `<Stat>`s in one `<StatGroup>`, never a
`<Stack direction="row">` or `<Grid>`: the group gives them one width. Give
each a `hint` that gives the number its scale ("of 20 left this year",
"claims to decide") and an `icon` from the kit's list that names what it
counts; `tone` colours the icon when the number needs attention.
- **Sections own their actions.** A `<Section>` titles a part of the page,
with `count` for the records it lists and `subtitle` for one line on what it
holds. An action that adds to or acts on that part sits in the Section's
`actions` (New request above the requests), not under the table and not in
the page Heading.
- **One primary action per screen.** `emphasis="primary"` marks the screen's
main action and nothing else; destructive actions are `"danger"`.
- **Status is a chip, not prose.** A record's status is the table's status
column (`{ label: "Status", kind: "status" }` in `columns`, and each row's
`status: { text, tone }`) or a `<Badge>`: `success`, `warning`, `error`,
`info`, `default`. Put the column where it reads best; the row's `cells` fill
the other columns in order. Callouts are `<Alert>`s with a tone.
- **Row actions are buttons, not cells.** What a user does to one record
without opening it (Cancel, Approve) is the row's `actions`, each with an id
built from the row's (`` `${rowId}.cancel` ``), a `label`, and `to` or
`onPress` like a Button (`emphasis: "danger"` for a destructive one). They
draw in a trailing column (behind one menu past two) and pressing one never
presses the row, so the row can still open the record. Never write an
action's label into a cell. Figures are `{ label, kind: "number" }` columns.
- **Navigation is the shell's side rail**, one entry per top-level screen.
Account and Settings live in the user menu, not the rail.
- **Records, not cards.** Show a list of records as a `<Table>` whose rows
open the record; use `<Split>` for a record next to its activity.
- **Forms are short and labelled.** One `<Form>` per task, a `<Field>` per
input with a plain label, `required` where the API requires it, errors in
the Field's `error` and a `<ValidationSummary>` for the whole form.
When the organization names a design system, its skill is loaded beside this
one and is the full account of how an enterprise screen is composed (a listing
page's header, filters and table, a form's layout and actions). Read it for the
arrangement and build each screen that way from the kit; with none loaded, use
the rules above. Its setup, packages and code are the build's concern,
not this flow's: the kit's components are the only vocabulary here.
## The kit
Nothing else exists: no markup, styles, scripts, charts, file upload or custom
component. Express a screen in these components or leave the part out.
<!-- kit:start -->
A prototype is `prototype.json` (the manifest) and `prototype.tsx`, which imports only `react` and `@wso2/prototype-kit`. Every component with an `id` is an element a reviewer can point at: give each a stable id, unique on its screen.
### Module
#### `defineApp(definition: PrototypeAppDefinition): PrototypeApp`
Declare the prototype: `export default defineApp({ screens, data })`.
#### `PrototypeAppDefinition`
- `screens: Record<string, ComponentType>` — One component per manifest screen, keyed by the screen's id.
- `data?: Record<string, unknown>` — The mock data the screens share, as JSON literals. An array of records that each have a string `id` is a collection (`useCollection(name)`); anything else is a single value (`useValue(key)`). Changes live until Reset data (or a reload without `--persist`).
### View hooks
#### `useNav(): KitNav`
#### `KitNav`
- `screen: string` — The screen shown.
- `go: (screenId: string, params?: Record<string, string>) => void` — Go to a screen, optionally with params the target reads with `useParams`.
#### `useParams(): Readonly<Record<string, string>>`
The params the navigation to this screen carried. Empty when the reviewer opened the screen from the preview's pickers, so every screen must render without them (fall back to the first record).
#### `useRole(): string`
The id of the role the reviewer is viewing as.
#### `useDisplayState(): string`
The id of the display state the reviewer chose (e.g. empty, loading, error).
### Data hooks
#### `useCollection<T extends { id: string; }>(name: string): Collection<T>`
The collection `name` from `defineApp({ data })`: an array of records that each have a string `id`.
#### `Collection`
A collection of mock records, and the ways to change it for every screen.
- `items: readonly T[]` — The records, in insertion order.
- `get: (id: string) => T | undefined`
- `create: (record: Omit<T, "id">) => string` — Adds a record and returns its new id, `${name}-${n}` with `n` past the highest number in use.
- `update: (id: string, patch: Partial<Omit<T, "id">>) => void`
- `remove: (id: string) => void`
#### `useValue<T>(key: string): [T, (next: T | ((previous: T) => T)) => void]`
The single value `key` from `defineApp({ data })`, and a setter shared by every screen.
#### `useToday(): string`
Today's date, fixed (`PROTOTYPE_TODAY`): a prototype never reads the clock.
#### `PROTOTYPE_TODAY` = `"2026-01-15"`
The fixed date every prototype treats as today (ISO `YYYY-MM-DD`).
### Layout
#### `<AppShell>`
The root of a screen inside the product's chrome: header, user menu and side navigation. Use `<Screen>` for a screen outside it (signed out).
- `id: string` — The shell's id; its user menu's elements are `<id>.user`, `<id>.account`, `<id>.settings` (with `settings`) and `<id>.sign-out`.
- `product?: string` — The product's name in the header; prototype.json's `name` by default.
- `user: AppShellUser` — The signed-in user the header shows.
- `nav: NavigationItem[]` — The side navigation's entries.
- `account: string` — The screen the user menu's Account entry opens.
- `settings?: string` — The screen the user menu's Settings entry opens. Leave it out when the application has nothing to set: the menu then has no Settings entry.
- `signOut: string` — The screen Sign out leads to: a signed-out screen, drawn on a bare `<Screen>`.
- `menu?: NavigationItem[]` — More user-menu entries (a profile, billing), drawn after Account (and Settings) and before Sign out.
- `children?: ReactNode` — The screen's content.
#### `AppShellUser`
- `name: string`
- `email?: string` — Shown under the name in the user menu.
- `role?: string` — The role beside the name; the viewing role's name in prototype.json by default, so a role switch shows in the header.
#### `<Screen>`
The root of a screen outside the app shell (signed out, a landing page): its content, and navigation if any.
- `nav?: ReactNode` — Navigation for an app drawn without `<AppShell>`: one `<Navigation>`, usually shared by every screen.
- `children?: ReactNode`
#### `<Section>`
A titled part of a screen — a table, a form, a group of stats — with the actions that belong to it.
- `id: string`
- `title: string`
- `subtitle?: string` — One line under the title: what the section holds or why.
- `count?: number` — How many records the section lists, shown beside the title.
- `actions?: ReactNode` — The section's own Buttons ("New request" above the requests it adds to).
- `children?: ReactNode`
#### `<Stack>`
Children in a column (default) or a wrapping row.
- `direction?: "row" | "column"`
- `children?: ReactNode`
#### `<Grid>`
Children in 1–6 equal columns.
- `columns: number` — 1–6 equal columns (one column on a narrow window).
- `children?: ReactNode`
#### `<Split>`
Two panes side by side (stacked on a narrow window).
- `left: ReactNode`
- `right: ReactNode`
- `ratio?: number` — The left pane's share of 12 columns (default 6).
#### `<Detail>`
A read-only record: label/value pairs.
- `id: string`
- `title?: string`
- `fields: DetailField[]`
#### `DetailField`
- `label: string`
- `value: string`
### Navigation
#### `<Navigation>`
Navigation for an app drawn on `<Screen nav>` (`<AppShell nav>` draws its own). Its items are what a reviewer points at.
- `id: string`
- `layout: "side" | "top"`
- `items: NavigationItem[]`
#### `NavigationItem`
- `id: string`
- `label: string`
- `to: string` — The screen the entry opens; it shows as active there.
#### `<Breadcrumbs>`
A trail back to where the reviewer came from.
- `id: string`
- `items: BreadcrumbItem[]` — The trail, root first.
#### `BreadcrumbItem`
- `id: string`
- `label: string`
- `to?: string` — Where the crumb leads; the last crumb is the current page and takes none.
- `params?: Record<string, string>`
#### `<Tabs>`
Tabbed panels. Each tab header is an element a reviewer can point at (its `id`); the Tabs itself is layout.
- `id: string`
- `tabs: Panel[]`
- `active?: string` — The open tab, when the screen controls it; the first tab otherwise.
- `onChange?: ((tabId: string) => void)`
#### `<Stepper>`
A multi-step wizard. Each step header is an element a reviewer can point at (its `id`).
- `id: string`
- `steps: Panel[]`
- `active?: string` — The current step, when the screen controls it (a Next button); the first step otherwise.
- `onChange?: ((stepId: string) => void)`
#### `Panel`
- `id: string`
- `label: string`
- `content: ReactNode`
### Content
#### `<Heading>`
A page or section title, with the actions beside it.
- `id: string`
- `text: string`
- `level?: "page" | "section"` — `page` (default) for a screen's title; `section` titles a part of it. Prefer `<Section>` for a part with its own actions or records.
- `actions?: ReactNode` — Buttons and Links beside the heading.
#### `<Text>`
A paragraph of body text.
- `id: string`
- `text: string`
- `tone?: "primary" | "secondary"` — `secondary` (default) for body copy, `primary` for emphasis.
#### `<Badge>`
A status chip.
- `id: string`
- `label: string`
- `tone?: Tone`
#### `<Stat>`
A headline number with its label. Put a screen's stats side by side in a `<StatGroup>`.
- `id: string`
- `label: string`
- `value: string`
- `hint?: string` — A short line under the value that gives it scale or context: "of 20 days", "3 due this week".
- `icon?: StatIcon` — An icon beside the label.
- `tone?: Tone` — Colours the icon (`default` is the theme's primary).
#### `StatIcon` = `"Activity" | "Bell" | "Briefcase" | "Bug" | "Building2" | "Calendar" | "CalendarCheck" | "CalendarClock" | "CalendarDays" | "ChartColumn" | "CircleCheck" | "CircleX" | "ClipboardList" | "Clock" | "Cloud" | "Cpu" | "CreditCard" | "Database" | "DollarSign" | "FileText" | "Gauge" | "Globe" | "HeartPulse" | "Hourglass" | "Inbox" | "Layers" | "ListChecks" | "Lock" | "Mail" | "MessageSquare" | "Package" | "Plane" | "Receipt" | "Rocket" | "Server" | "Shield" | "ShoppingCart" | "Star" | "Tag" | "Ticket" | "Timer" | "TrendingDown" | "TrendingUp" | "TriangleAlert" | "Truck" | "User" | "UserCheck" | "Users" | "Wallet" | "Zap"`
The icons a `<Stat>` may show, named as in WSO2's Oxygen icon set (Lucide's names). A theme draws each or none; an unknown name fails the check.
#### `<StatGroup>`
A row of `<Stat>`s at equal widths, wrapping on a narrow window.
- `children?: ReactNode` — The `<Stat>`s, in reading order.
#### `<Alert>`
A callout: info, success, warning or error.
- `id: string`
- `tone: Tone`
- `title?: string`
- `text: string`
#### `<EmptyState>`
What a list shows when there is nothing in it.
- `id: string`
- `title: string`
- `text: string`
- `actions?: ReactNode` — The Buttons that start something.
#### `<Button>`
A button. Give it `to` to navigate, `onPress` to do anything else, `submit` to submit its form.
- `id: string`
- `label: string`
- `emphasis?: "primary" | "danger"` — `primary` for the screen's main action, `danger` for a destructive one; outlined otherwise.
- `disabled?: boolean`
- `submit?: boolean` — Submits the enclosing `<Form>` (which validates its fields, then calls its `onSubmit`).
- `to?: string` — Navigate to this screen when pressed. Prefer it to `onPress` for plain navigation: the checks verify it.
- `params?: Record<string, string>` — The params `to` carries, read on the target with `useParams`.
- `onPress?: (() => void)` — Run when pressed: change mock data, open a dialog, navigate conditionally.
#### `<Link>`
An inline link; presses like a Button.
- `id: string`
- `label: string`
- `to?: string` — Navigate to this screen when pressed. Prefer it to `onPress` for plain navigation: the checks verify it.
- `params?: Record<string, string>` — The params `to` carries, read on the target with `useParams`.
- `onPress?: (() => void)` — Run when pressed: change mock data, open a dialog, navigate conditionally.
#### `Tone` = `"default" | "info" | "success" | "warning" | "error"`
#### `Pressable`
- `to?: string` — Navigate to this screen when pressed. Prefer it to `onPress` for plain navigation: the checks verify it.
- `params?: Record<string, string>` — The params `to` carries, read on the target with `useParams`.
- `onPress?: (() => void)` — Run when pressed: change mock data, open a dialog, navigate conditionally.
### Forms
#### `<Form>`
A form card: its Fields, then its action Buttons. Validates on submit.
- `id: string`
- `title?: string`
- `children?: ReactNode` — Its Fields.
- `actions?: ReactNode` — Its Buttons, under the fields; a `<Button submit>` submits.
- `onSubmit?: ((values: Record<string, string>) => void)` — Called with every field's value, keyed by name, when a submit passes validation.
#### `<Field>`
One input. It holds what is typed into it; inside a Form, the Form validates and submits it.
- `id: string`
- `label: string`
- `name?: string` — The key of its value in the enclosing Form's `onSubmit` values (default: its id).
- `type?: FieldType`
- `value?: string` — Controls the field: it shows this; with `onChange` it is editable, without it read-only.
- `defaultValue?: string` — The starting value of a field that holds its own (no `value`). A switch is `on` or `off`.
- `options?: string[]` — A select's choices.
- `error?: string` — A problem to show regardless of validation (e.g. in a validation-error display state).
- `required?: boolean`
- `pattern?: string` — A regular expression the whole value must match when it is not empty.
- `patternMessage?: string` — What a value that does not match `pattern` shows.
- `placeholder?: string`
- `onChange?: ((value: string) => void)`
#### `FieldType` = `"number" | "text" | "select" | "date" | "textarea" | "switch"`
#### `<Filters>`
A filter bar above a list.
- `id: string`
- `children?: ReactNode` — Its Fields, laid out compactly in a row.
#### `<ValidationSummary>`
The problems a failed submit shows, for a display state that shows them without a submit.
- `id: string`
- `issues: string[]`
### Data
#### `<Table>`
A table of records.
- `id: string`
- `title?: string`
- `columns: (string | TableColumn)[]`
- `rows: TableRow[]`
- `onRowPress?: ((rowId: string) => void)` — Run when a row is pressed, with its id; a row with neither this nor `to` is only highlighted.
- `empty?: ReactNode` — What an empty table shows instead (an `<EmptyState>`).
#### `TableColumn`
- `label: string`
- `kind?: "number" | "text" | "status"` — `text` (default); `number` aligns figures to the right; `status` shows each row's `status` as a badge (one per table). A plain string is a `text` column.
#### `TableRow`
- `id: string` — The row's element id: unique on the screen (prefix it with the table's).
- `cells: string[]` — One per `text` or `number` column, in column order; the status column takes `status`.
- `status?: TableStatus` — The badge in the table's status column.
- `actions?: TableAction[]` — What can be done to this record, drawn as buttons in a trailing column; pressing one does not press the row.
- `tone?: Tone` — Colours the last cell as a status, in a table without a status column. Prefer `status`.
- `to?: string` — Open this screen when the row is pressed.
- `params?: Record<string, string>`
#### `TableStatus`
- `text: string`
- `tone: Tone`
#### `TableAction`
- `id: string` — The action's element id: unique on the screen (prefix it with the row's).
- `label: string`
- `emphasis?: "danger"` — `danger` for a destructive action.
- `to?: string` — Navigate to this screen when pressed. Prefer it to `onPress` for plain navigation: the checks verify it.
- `params?: Record<string, string>` — The params `to` carries, read on the target with `useParams`.
- `onPress?: (() => void)` — Run when pressed: change mock data, open a dialog, navigate conditionally.
#### `<Timeline>`
An activity history.
- `id: string`
- `title?: string`
- `entries: TimelineEntry[]` — Newest first.
#### `TimelineEntry`
- `when: string`
- `who: string`
- `text: string`
### Overlays
#### `<Dialog>`
A modal.
- `id: string`
- `title: string`
- `open: boolean`
- `onClose: () => void`
- `children?: ReactNode`
- `actions?: ReactNode` — Its Buttons, at the bottom.
#### `<Drawer>`
A side panel.
- `id: string`
- `title: string`
- `open: boolean`
- `onClose: () => void`
- `children?: ReactNode`
### Finding codes
`prototype check --json` prints `{ ok, findings: [{ code, file, location, message }] }` and exits 1 when there are findings.
| Code | Meaning |
|---|---|
| `MISSING_FILE` | prototype.json or prototype.tsx is not in the folder. |
| `SCHEMA_VIOLATION` | prototype.json is not JSON, or does not have the manifest's shape. |
| `UNSUPPORTED_VERSION` | prototype.json's schemaVersion is not 3. |
| `DUPLICATE_ID` | A role, state, screen or flow id repeats another; the four share one namespace. |
| `UNKNOWN_REFERENCE` | A manifest reference names nothing, or a flow step its role cannot reach. |
| `SYNTAX_ERROR` | prototype.tsx does not parse. |
| `FORBIDDEN_IMPORT` | An import other than react and @wso2/prototype-kit, a dynamic import(), or require. |
| `FORBIDDEN_API` | A browser, network, storage, code-generation or nondeterministic API. |
| `FORBIDDEN_ELEMENT` | A raw HTML element; screens are drawn with kit components only. |
| `SOURCE_TOO_LARGE` | prototype.tsx is over the size cap. |
| `NO_APP` | prototype.tsx does not `export default defineApp({ screens, data })`. |
| `SCREEN_MISMATCH` | defineApp's screens are not exactly the manifest's screens. |
| `UNKNOWN_NAV_TARGET` | A `to` or `go()` names a screen that does not exist, or one the viewing role cannot reach. |
| `TYPE_ERROR` | prototype.tsx does not type-check: it reads a property, prop or name its types do not have, or passes a value of the wrong type. |
| `RENDER_FAILED` | The module, or a screen for some role and display state, throws or runs too long. |
| `DUPLICATE_ELEMENT_ID` | Two elements on one screen share an id. |
<!-- kit:end -->
## Mock data
Records are part of the source and never generated at render time, so write
them out in full:
- **Shaped by the data model.** Each collection in `defineApp({ data })` is
an entity of `domain-model.md`, and each record has that entity's
attributes. Columns, detail fields and form fields are those attributes, in
words a user reads (`submittedAt` becomes "Submitted"). A value has the type
the model gives it: an enum value is one of its values, an amount reads as
an amount. A relation shows as the related record's name, not its id. Type
the records after the entity (`interface Expense { … }`).
- **Realistic.** Plausible names, amounts, dates and statuses for the domain;
never `foo`, `test`, `Lorem ipsum` or `Item 1`.
- **Deterministic.** The same design gives the same records: fixed values, no
randomness, no "today" other than `useToday()`. A row's id is derived from the
record (`expense.1042`).
- Enough rows to read as real (three to six), and a state in which a list is
empty when the story has one.
## Stable ids
Manifest ids and element ids (the `id` of every kit component) are identities,
labels are not: a reviewer's annotations point at element ids, and shared
links name screens, flows and states, so:
- Name them `<kind>.<slug>` from what the thing is (`screen.queue`,
`btn.approve`, `field.amount`), except role ids, which are `security.json`'s
names. An element id is unique on its screen; a row's id is prefixed with
its table's kind (`expense.1042`).
- **Revising an existing prototype keeps every key and id whose thing still
exists.** Rename labels freely; change an id only when its thing is removed
or genuinely becomes something else.
- **Regenerating a prototype that has review comments** (`/prototype` on a
reviewed design) is a revision like any other: edits, ids kept. Leave
`specs/design/review.json` as it is. Its comments stay open for Address
comments, and the file's Checked mark clears by itself when the prototype
changes.
## Closing
When a prototype passes its checks, report it in one chat line: its screen
count and flow count, then each story you set aside and anything from the
design you could not show, with the reason.