Composes a component-based mockup (mockup.json) from the standard themed component library. Uses the data model when available to structure the views. The interactive HTML is generated by the backend on demand.
Scanned 9/6/2026
Install to Claude Code
npx -y skills add Federico-Anastasi/DYLA-ai --skill mockup --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Mockup?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/federico-anastasi-mockup-dyla-ai)More formats (shields.io, HTML) on the badges page.
---
name: mockup
description: "Composes a component-based mockup (mockup.json) from the standard themed component library. Uses the data model when available to structure the views. The interactive HTML is generated by the backend on demand."
user-invocable: true
pack: delivery
---
# /mockup — Component-based mockup
Composes the mockup of the application as `mockup.json`: a set of pages built ONLY from
components of the standard library (see PHASE 3). **It does not generate HTML.** The
preview in the viewer (`web/src/components/Viewer/MockupView.tsx`) and the interactive
export (`mockup.html` — working navigation, tabs and search, generated on demand by
`server/mockup_export.py`) use the SAME themed component library (`web/src/mockup-lib/`,
`mk-*` classes, themes in `web/src/mockup-lib/themes/{standard,compact,plain}.css`). They
are pixel-identical for layout and theme, exactly as for the estimate and the data model —
with one gap: the icon sets are not perfectly mirrored (see PHASE 3 below), so a page using
an icon the export does not know renders fine in the viewer and loses that one icon in the
HTML export.
The consequence matters: **you never invent markup or colours.** The theme owns the
structure; you choose which components to use and what goes in them.
**UX: chat stays conversational.** The mockup is never presented as a list or a table in
chat — the user sees it in the viewer next to the chat. In chat you discuss it in words,
citing pages and components by id (e.g. the `detail` page, the `grid1` component). When
you cite a chapter of the brief, write `[[brief:Chapter title]]`: it renders as a
clickable reference that opens the document at that point.
---
## PROTOCOL
1. **Read context.md + brief + data_model.json** (if present)
2. **Pick the theme** — from context.md, or ask
3. **If `mockup.json` exists:** ask in a `questions` block (same pattern as the theme
question below) whether to regenerate it from scratch or edit it
4. **Compose the pages** from the library (PHASE 3)
5. **Write mockup.json straight away** with `meta.status: "draft"`, and validate it
6. **Present a conversational summary** (pages and their main components, not the data row by row), then wait
7. **On confirmation:** set `meta.status: "confirmed"`
8. **Update context.md**
9. **Check consistency**
---
## PHASE 1 — THE THEME
The theme goes in `meta.theme` and is one of `"standard" | "compact" | "plain"`. It picks
both the CSS and, for several components, the STRUCTURE — the themes are not the same
layout in different colours.
Read the brief and context.md first. If the theme is not evident, ask — and ask it as a
`questions` block, like every other question to the user. The example below is written out
in full because this is the first question of the skill, and one asked as plain prose
loses the user the clickable options:
````
```questions
[
{
"id": 1,
"q": "Which look should the mockup use? They are different layouts, not the same screens in another colour.",
"options": [
"standard — deep teal, dark top bar with horizontal tabs. The default business application",
"compact — violet, left sidebar, denser type. For information-heavy back-office screens",
"plain — indigo, no strong identity, the quietest of the three"
]
}
]
```
````
The colours above are the ones in `web/src/mockup-lib/themes/*.css` (`--mk-primary`:
`#2F6F7E`, `#5B4B8A`, `#3B5BDB`). Read them from there if you are unsure rather than
trusting this line — describing a theme as a colour it no longer is sends the user to
choose on a false description.
### What each theme imposes (binding — do not improvise variants)
**standard** — top bar plus tabs:
- A full-width dark top bar (app title on the left, user on the right). The nav is not
attached to it: it renders inside the page content, just under the title.
- The page title (derived from `pages[].name`, not a component) is large, coloured and
centred. The "Back" link above it is derived automatically from the page's `breadcrumb`
component (second-to-last item) — a secondary page that needs "Back" just needs a
`breadcrumb` with at least two items, nothing else.
- `filters` is a collapsible "Filters" block with a chevron and a magnifier, closed by
default.
- `legend` (coloured stars) goes above the `grid` when the list has a prioritisation
logic.
- `actions` (a free button bar, e.g. "+ New request" above a grid) is aligned LEFT, in
neutral grey outline style rather than the primary colour. The exception is the button
bar INSIDE a `form` (submit/cancel), which is always right-aligned — that is a
different pattern.
- `grid.actions` (row icons: eye, pencil) go in the FIRST column, before the status
column. Automatic, not configurable.
- **Binding rule: every entry in `grid.actions[]` must target a page with
`kind: "modal"`, never a `kind: "page"`.** A row icon opens a dialog, it does not
navigate away — see "Detail dialogs with `sidebar-nav`" below.
**compact** — a codified section order, do not invert it:
- No top bar: a left sidebar with the logo (fixed theme chrome), the vertical nav, and the
user block at the bottom (fed by the `topbar` component, which here only supplies that
footer).
- The page title is left-aligned. If the FIRST component of the page is `actions`, the
export automatically shows it next to the title (buttons to the right, the record-view
header pattern) — no flag needed, just the order.
- Inside a `section` (or directly on the page) the order is: banner → `actions` (if not
already hoisted to the title) → `filters` → `kpi-row`/`grid`.
- `wizard-steps`: horizontal up to 7 steps, vertical beyond that. No need to set
`orientation` by hand in the common case.
- **Two-column detail view, automatic:** if the first component of the page body (after
any `actions` hoisted to the title) is `state-progress`, it — and the `section`
immediately following it, if there is one — moves into a narrow left column; everything
else (typically `tabs` holding sections and grids) goes into the wide right column. No
flag: just put `state-progress` first.
- **`grid` has no actions column in this theme.** To open a row's detail use a column with
`kind: "id-link"` (typically the id) and still pass `actions[0].target` — it only
supplies the click destination, it is never rendered as a button. In this theme an
`id-link` targets a normal `kind: "page"` (a full record view, unlike standard).
- `actions` is aligned RIGHT — the opposite of standard.
- Buttons are squared (~7px radius), never full pills.
- Action forms here are simple (`form`, with `wizard-steps` if multi-step): never use
`sidebar-nav`, which is a standard-theme pattern only.
**plain**: neutral, none of the structural rules above are binding. Use the standard
structure (top bar plus tabs) as the simplest default.
---
## PHASE 2 — SOURCES AND DATA
Read all of these to understand the domain, NOT to reinvent markup (the library already
matches the theme — see PHASE 1):
- the brief: `projects/{project}/brief.json`, or the input document (`brief.md`, or its
extracted text under `.extracted/`)
- `projects/{project}/docs/*` — client documents (binaries extracted under `.extracted/`)
- `projects/{project}/context.md`
- `projects/{project}/data_model.json` — use `tables[].fields` and `relations[]` to decide
which columns belong in grids, details and forms, and which pages link to which
- `projects/{project}/estimate.json` — if it exists, for the agreed scope
- `knowledge/` — if the folder exists: naming, branding, recurring patterns
- `web/src/mockup-lib/` — the catalogue of available components
If there is no data model, work from the brief directly.
What to mock, in priority order (usually 2 to 4 pages):
1. The main list (`kpi-row`? + `filters`? + `grid` with an action opening the detail)
2. The record detail: in the standard theme a `kind: "modal"` dialog with `sidebar-nav`
(see below), opened from a row icon; in the compact theme a normal `kind: "page"`
(`state-progress` + `tabs` + `actions`), opened from the grid's `id-link`
3. The create/edit form: always `kind: "modal"` (with `wizard-steps` if it is multi-step
in the compact theme)
---
## MODAL PAGES (`kind: "modal"`)
Actions open a dialog on top of the current screen, they do not navigate to a new page.
The mockup mirrors that: every page is either `"kind": "page"` (the default, and the
value assumed when absent) or `"kind": "modal"`.
**Modelling rule:** create and action forms ("New request", confirmations, a rejection
reason) are modelled as `kind: "modal"` pages. In the standard theme the detail or edit
view of an existing record opened from a row icon (`grid.actions`) is a modal too — the
only full-page navigation left there is the compact theme's `id-link`. So normal pages
are: in the standard theme only lists, dashboards and reports; in the compact theme those
plus the record detail view.
- A `kind: "modal"` page never appears in `nav` and is never navigable directly: it opens
as a centred dialog (dark overlay, card, X at the top right) when an
`actions.buttons[].target`, a `grid.actions[].target` or a `form.submit_target`
references it by id.
- A button INSIDE the dialog targeting a `kind: "page"` closes the dialog and navigates
there (e.g. the form's `submit_target` → the list page). Esc and a click on the overlay
close it without navigating.
- Modal pages carry no `topbar`/`nav` (global chrome, irrelevant inside a dialog).
- The dialog title is `pages[].name` — there is no title component, same as normal pages.
### Detail dialogs with `sidebar-nav` (standard theme)
When a row icon opens the detail or edit view of an existing record, the target dialog
follows the record-form pattern: a status bar at the top, then `sidebar-nav` (a vertical
menu on the left with a fixed title, info panel and alerts, and the active section's body
on the right), then `actions` at the bottom. A typical composition, in order:
1. `statusbar` (optional, e.g. "Completed" — only when the record has a headline state
worth showing full width)
2. `banner` (optional, e.g. "fill in X before continuing")
3. `sidebar-nav`: `title` is a heading like "REQUEST: {id}"; `sections[]` are the menu
entries, each with a `label` ("Summary", "Documents", "History", "Approvals") and its
`components[]`. For "Summary" use two to four `section`s with `collapsible: true` (one
per group of fields, e.g. "Requester" / "Amounts" / "General"), each holding a `detail`
with a SINGLE section and NO `title` (the collapsible `section` already shows it — do
not repeat it). For the other entries use a `grid`. `info` (optional) is the fixed
panel under the menu; `alerts` (optional) the fixed badges under it
4. `actions`: the first button must be the exit or cancel one (e.g. "Exit") — inside a
dialog in the standard theme it is automatically isolated on the left while the rest
goes right. No prop to set: it is automatic
You choose only the record title, the menu entries, the section titles and sample data,
the info and alert content, and the button labels. The structure is fixed and handled by
the theme.
---
## PHASE 3 — THE COMPONENT LIBRARY (BINDING)
`mockup.json` is validated against `schemas/mockup.schema.json`. **Writing HTML, CSS or JS
is forbidden**: every page is `{id, name, kind?: "page"|"modal", components: [Component]}`
and every component is `{id, type, props}` with `type` drawn from this closed list of 19.
No other `type` is allowed. The **page title is not a type**: it is automatic chrome
derived from `pages[].name`.
| type | Use | Main props |
|---|---|---|
| `topbar` | Global chrome (once per document, first occurrence wins). App title and user. standard/plain: dark bar on top. compact: feeds the sidebar footer only. | `title`, `user?` |
| `nav` | Global chrome (once per document). Navigation between pages. standard/plain: horizontal tabs. compact: vertical sidebar. | `items:[{label, page}]` |
| `breadcrumb` | standard: the source of the automatic "Back" link, renders nothing else. compact/plain: a clickable "A / B / C current" trail. | `items:[{label, page?}]` (last one without `page` = current) |
| `kpi-row` | KPI cards. compact: coloured title, divider, large number. standard/plain: flat card with an optional trend. | `cards:[{label, value, trend?: up\|down\|flat}]` |
| `grid` | Data table / record list. Row action placement is decided by the theme: standard first column, compact NO column (only `id-link`), plain last column. **Every `actions[].target` must be a `kind:"modal"` page**, except the compact `id-link` case, which targets a normal page. | `title?`, `columns:[{key,label,kind?: text\|id-link\|chip\|progress\|sla\|status}]`, `rows:[{...}]`, `actions?:[{label,target,icon?,variant?:button\|link}]`, `searchable?`, `paginationLabel?` |
| `form` | Create/edit form, a single card. | `title?`, `fields:[{label,type:text\|number\|date\|select\|textarea\|checkbox,required?,options?}]`, `submit_label?`, `cancel_label?`, `submit_target?` |
| `detail` | Read-only detail view: label/value sections. `sections[].title` is optional — omit it when the `detail` sits inside a collapsible `section` that already shows the title. | `title?`, `sections:[{title?, fields:[{label,value}]}]` |
| `actions` | A row of buttons. First component of the page in the compact theme → hoisted next to the title. In the standard theme inside a `kind:"modal"` page, the FIRST button is isolated on the left (e.g. "Exit") and the rest go right — so put the exit/cancel button first. | `buttons:[{label, style?: primary\|secondary\|danger\|action\|link, icon?, target}]` |
| `tabs` | A set of tabs with nested content. compact: filled accent pill plus links. standard/plain: outlined pills. | `tabs:[{label, components:[nestedComponent]}]` |
| `banner` | A prominent message, four tones. | `style: info\|success\|warning\|error`, `title?`, `text` |
| `section` | White card with a title and a divider; groups other components. `collapsible: true` adds a chevron (starts expanded). | `title?`, `icon?`, `collapsible?`, `components:[nestedComponent]` |
| `filters` | Filter bar. standard: collapsible "Filters" block. compact: dense chips (small-caps label, italic value) plus optional search. | `fields:[{label,value}]`, `collapsible?` (standard only, default true), `search_label?` (compact only) |
| `legend` | Coloured-star legend (standard-theme prioritisation pattern). | `items:[{label, color: grey\|red\|green\|blue}]`, `title?` |
| `statusbar` | Full-width centred status band (e.g. "Completed"). | `label`, `tone?: info\|success\|warning\|error` (default success), `icon?` |
| `wizard-steps` | Multi-step wizard. Horizontal up to 7 steps, vertical beyond (automatic). | `steps:[string]`, `current` (1-based), `orientation?` |
| `state-progress` | Vertical status progression (left rail of a compact-theme record view). | `states:[{label, date?}]`, `current` (1-based), `title?` (default "Status") |
| `segmented` | Pill selector / view switcher (e.g. "By activity" / "By team"). | `options:[{label, target?}]`, `active?` (default 0) |
| `tiles` | A row of navigation tiles (top-level landing page). | `items:[{label, icon?, link_label?, target}]` |
| `sidebar-nav` | STANDARD THEME ONLY, inside a `kind:"modal"` page: vertical navigation BETWEEN SECTIONS (the record-form pattern). Real navigation like `tabs`, but vertical and with fixed info/alert panels. See "Detail dialogs" above. | `title`, `sections:[{label, components:[nestedComponent]}]`, `info?:{title?, fields:[{label,value}]}`, `alerts?:{title?, items:[string]}` |
**Rules:**
- Every `target`/`page` in a navigation component must match an existing `pages[].id`.
- Page `id`: lowercase slug (`^[a-z][a-z0-9-]*$`). Component `id`: unique within the page.
- Data in `grid.rows` and `detail.sections[].fields`: **fictional but plausible**, and
consistent with `tables[].fields` in the data model (realistic names, types and formats
— never "Lorem ipsum"). This is what makes a mockup discussable: people react to
realistic data and skim past filler.
- `grid.actions` navigates to the same target page for every row (it is a prototype, there
is no per-row detail).
- Do not repeat `topbar`/`nav` on every page: they are global chrome (taken from the first
occurrence) and stay put while the page content changes.
- Icons (`grid.actions[].icon`, `actions.buttons[].icon`): use names from the shared
Lucide-style set — never emoji. A frequent sample: `pencil`, `eye`, `trash-2`,
`download`, `play`, `check-circle`, `flag`, `plus`. The full set is larger (~30 names
shared by preview and export — check `web/src/components/icons.tsx` for what exists).
**Avoid `arrow-up`, `refresh-cw`, `settings`, `upload`, `paperclip`, `link-2`,
`history`, `folder`, `table`**: the React preview knows them but
`server/mockup_export.py` does not, so the icon silently disappears from the HTML
export.
### A minimal example (standard theme: one list page plus two dialogs)
```json
{
"meta": {"project": "demo", "title": "Expense claims", "theme": "standard", "date": "2026-07-18", "status": "draft"},
"pages": [
{"id": "list", "name": "Claims", "components": [
{"id": "top", "type": "topbar", "props": {"title": "Expense claims", "user": "M. Rossi"}},
{"id": "nav1", "type": "nav", "props": {"items": [{"label": "Claims", "page": "list"}]}},
{"id": "kpi1", "type": "kpi-row", "props": {"cards": [{"label": "Awaiting approval", "value": "4", "trend": "up"}]}},
{"id": "act1", "type": "actions", "props": {"buttons": [{"label": "New claim", "style": "action", "icon": "plus", "target": "new"}]}},
{"id": "grid1", "type": "grid", "props": {
"columns": [{"key": "id", "label": "ID"}, {"key": "requester", "label": "Requester"}, {"key": "status", "label": "Status"}],
"rows": [{"id": "1024", "requester": "M. Bianchi", "status": "Awaiting approval"}],
"actions": [{"label": "Open", "target": "detail", "icon": "eye"}]
}}
]},
{"id": "detail", "name": "Claim detail", "kind": "modal", "components": [
{"id": "det1", "type": "detail", "props": {"sections": [{"title": "Claim", "fields": [{"label": "Amount", "value": "245.00 EUR"}]}]}},
{"id": "act1", "type": "actions", "props": {"buttons": [{"label": "Exit", "style": "secondary", "target": "list"}]}}
]},
{"id": "new", "name": "New claim", "kind": "modal", "components": [
{"id": "frm1", "type": "form", "props": {
"fields": [{"label": "Amount (EUR)", "type": "number", "required": true}],
"submit_label": "Save", "cancel_label": "Cancel", "submit_target": "list"
}}
]}
]
}
```
Note that `detail` and `new` are both `kind: "modal"` (in the standard theme a
`grid.actions` entry always opens a dialog, as does a create form), do not appear in
`nav1.items`, and open when something references them as a `target`. `list` is the only
normal page (`kind` absent = "page").
---
## PHASE 4 — COMPOSITION
Compose the pages following the priority in PHASE 2. For each page pick components from
the library and populate them with realistic data derived from the data model and the
brief.
---
## PHASE 5 — WRITE THE DRAFT mockup.json (before you talk in chat)
**Do not generate HTML.** The export (`mockup.html`) is produced on demand by the backend
(`server/mockup_export.py`, `GET /api/projects/{project}/export/mockup.html`).
**Write it one page at a time.** A composed mockup runs to tens of thousands of
characters, and emitting all of it in a single tool call is where this breaks: observed on
a local model, the `Write` call arrived with an EMPTY payload, twice, after which it tried
to smuggle the file through a shell heredoc and failed at that too — thirty-three minutes
for nothing written. A page at a time each call is small, and what is already written
stays written.
1. **Write the skeleton first**: `meta` — including `theme` INSIDE it, as one of its
fields, not a sibling key at the document root (the root only allows `meta` and
`pages`; a top-level `theme` fails `additionalProperties: false`) — and `pages`
holding the FIRST page already composed. `pages` has `minItems: 1` in the schema, so
`pages: []` is not a valid skeleton: `save_doc` rejects it with 422 and the viewer
never gets a document to open. A few lines, but a complete, valid one.
2. **Then add one page per `Edit`**, in the order you described them in chat. Anchor
`old_string` on the closing of the LAST page already written — its unique `id` makes
the match unambiguous — and insert the new page object right before the array's
closing `]`. After each edit the file is still valid JSON — so if anything goes wrong,
the user is left with three good pages instead of a broken document.
3. VALIDATE it:
```
python -c "import json; from jsonschema import validate; validate(json.load(open(r'projects/{project}/mockup.json', encoding='utf-8')), json.load(open(r'schemas/mockup.schema.json', encoding='utf-8'))); print('valid')"
```
If it fails, fix the JSON (almost always: a `type` not in the library, a `target`/`page`
matching no `pages[].id`, or a missing field in some `props`) and re-run.
---
## PHASE 6 — CONVERSATIONAL SUMMARY (BLOCKING)
The file already exists (`meta.status: "draft"`) and the user can see it in the viewer. In
chat give a **conversational summary**: how many pages, which ones (by name and id), and
the main components of each ("the list has a kpi-row and a grid whose Open action goes to
the detail dialog"). No row-by-row data, no markup. Close by asking for confirmation or
changes.
**STOP. Wait for confirmation or change requests.**
If the user asks for changes: edit `mockup.json` directly, re-validate, present a short
summary of what changed (naming the page and component) and ask again.
---
## PHASE 7 — CONFIRMATION
On confirmation: set `meta.status: "confirmed"` in `mockup.json` (`Edit` just that line — do not rewrite the file: it is tens of thousands of
characters, and regenerating all of them to change one word is both slow and a
chance to alter something by accident), re-validate. Confirm briefly in chat.
---
## PHASE 8 — UPDATE context.md
Update `## Deliverables` with `mockup.json`. If the mockup is the anchor document,
summarise the pages and views composed under a `## Mockup` section.
---
## PHASE 9 — CONSISTENCY CHECK
If `estimate.json` exists: "The mockup shows views and fields that differ from what was
estimated. Want me to revisit the estimate?"
If `test_plan.json` exists: flag that the steps of any case citing the mockup pages you
touched may no longer line up.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!