Skip to content
Back to skills

Migrate Design Prototype

ASecurity

Reproduce a Claude Design prototype or design handoff (HTML/CSS, .dc.html export, tokens, screenshots) inside a Mendix app: build the palette with `mxcli theme create --from`, then apply classes in pages with MDL. Use when given a design artefact and asked to make the app look like it.

  • 128 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added September 26, 2026
ai-agentsrustgoshellbashsqlexpressdockerawsapidatabase

Works with

  • cli
  • api

Security analysis

A100/100

Scanned October 4, 2026

npx -y skills add mendixlabs/mxcli --skill migrate-design-prototype --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Migrate Design Prototype?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Migrate Design Prototype
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/mendixlabs-migrate-design-prototype/badge)](https://www.skillsdirectory.com/skills/mendixlabs-migrate-design-prototype)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: migrate-design-prototype
description: "Reproduce a Claude Design prototype or design handoff (HTML/CSS, .dc.html export, tokens, screenshots) inside a Mendix app: build the palette with `mxcli theme create --from`, then apply classes in pages with MDL. Use when given a design artefact and asked to make the app look like it."
---

# Migrate a Claude Design Prototype into a Mendix App (Theme + Pages)

## When to Use This Skill

Use this skill when you are given a **Claude Design prototype / design handoff** (an HTML/CSS
prototype, a `*.dc.html` design-console export, a tokens file, a PRD, and/or screenshots) and need to reproduce that look in a Mendix app using **mxcli + MDL**.

It covers the two halves of the job:

1. **Build the SCSS theme** — turn the prototype's design language (colours, fonts, spacing, component styles) into a Mendix theme in `theme/web/main.scss`.
2. **Apply it in pages** — attach the theme's classes to widgets with MDL
   (`Class:` / `DynamicClasses:` on `create page` / `alter page`).

Related skills: **`atlas-design` (read first — the Atlas-first taste + workflow layer)**,
`theme-styling` (SCSS compilation chain, hot-reload, styling caveats),
`create-page` (widget syntax), `alter-page` (in-place widget edits),
`bulk-widget-updates` (apply a class across many widgets).

---

## The Pipeline at a Glance

```
Claude Design handoff                         Mendix app
─────────────────────                         ──────────────────────────────
*.dc.html / prototype  ──①  theme    ──►  mxcli theme create --from <file>
tokens / CSS / PRD          create        → a theme the project owns
                                                    │
component styles       ──②  rebuild  ──►  Atlas block / utility class, and only
(cards, chips, …)           as classes      then .ss-* classes in main.scss
                                                    │
screenshots            ──③  reference ──►  widgets get Class: / DynamicClasses:
(per screen)                per screen          via create page / alter page
                                                    │
                            ──④  build   ──►  docker build → docker reload --css
                                                    │
                            ──⑤  verify  ──►  compare running screen to screenshot
```

**Golden rule:** the prototype is the source of truth. Before building or polishing any
screen, open the matching screenshot/handoff for that screen and match it — colours,
spacing, font, component shapes. Do not invent styling the prototype doesn't show.

**Atlas-first (read `atlas-design`).** Reproduce the prototype with what Atlas already
gives you *before* hand-writing custom SCSS. In order of preference:

1. **An Atlas building block** — `use building block Atlas_Web_Content.Card` / `Pageheader`
   / `List_Cards` etc. gives you the whole component's markup + styling for free. Discover
   with `list building blocks`, inspect with `describe building block`.
2. **Atlas utility classes and typed design properties** — `class:'card'`, `class:'btn btn-primary'`,
   `spacing-inner-*`/`spacing-outer-*` for padding/margin, `flex-row`/`flex-column` +
   `align-x-*`/`align-y-*` for layout (no `layoutgrid` needed); or the typed equivalents
   `designproperties: ('Card style': on)`, `['Background color': 'Brand Primary']`,
   `['Spacing': ['margin-bottom': 'L']]`. `mxcli check -p` validates design-property keys
   and values (MDL-WIDGET11/12) and lists the allowed values.
3. **Brand-token retune** — build the palette with `mxcli theme create --from`
   (step ①). The theme maps ~60 Atlas variables onto it, so the whole app inherits
   the look; hand-mapping a handful of `--brand-*` leaves most of Atlas on stock blue.
4. **Custom `.ss-*` SCSS — for brand identity only.** Reach for a hand-rolled component
   class (below) only when Atlas genuinely can't express the shape (bespoke chrome,
   fractional-track grids, pixel-exact rows). Hand-rolling `.panel`/`.stat`/`.card` SCSS
   that just re-implements what `class:'card'` already does is the single most common
   mistake — see `atlas-design`.

The rest of this skill (custom SCSS components, `.ss-*` classes, ListView row reshaping) is
**layer 4** — the identity layer you drop to when the first three don't reach the design.

---

## Where the Theme Lives (read this first — it avoids the main friction)

- **Custom styles go in `theme/web/main.scss` AFTER the `@import`s, or in your own
  partial.** Styles placed after the imports win the cascade over Atlas defaults. Once
  `main.scss` grows, prefer splitting a partial out for readability: create
  `theme/web/_<name>.scss` and add `@import "<name>";` after the Atlas imports (the same
  cascade-order rule then applies within the partial). New partials **are** creatable —
  keep the import order (custom after Atlas) and everything works.
- Use a **project prefix** for every custom class so it never collides with Atlas or
  widget CSS — `.ss-panel`, `.ss-chip`. Pick one and use it everywhere. Do **not**
  invent a parallel set of `--ss-*` colour variables: the theme's `--mxt-*` palette is
  already the app's vocabulary, and a second one silently stops following a theme or
  variant swap.
- `theme/web/custom-variables.scss` holds the **palette**, and `mxcli theme apply`
  writes it — it is a generated, digest-fenced block. Retune tokens there for a
  one-value tweak; for a real brand, own the theme (`mxcli theme create`, step ①)
  rather than editing inside the fence, which the next `apply` refuses.
- `theme/mxcli-themes/<name>/` is where a theme the project owns lives. Committed,
  and **not compiled** — mxbuild's entry point is `theme/web/main.scss` and it does
  not glob `theme/`, so the sources sit inert until `theme apply` copies them into
  `theme/web/`.
- Do **not** hand-edit `theme-cache/web/` — that is the compiled build artifact.

---

## ① Build the Theme — `mxcli theme create --from`

**Do not hand-write a token block.** A theme's palette is nothing but `--mxt-*`
custom properties, and mxcli builds one from a design artifact directly:

```bash
mxcli theme create acme -p app.mpr --from design/canvas.dc.html
mxcli theme apply acme -p app.mpr
```

`--from` reads `--mxt-*` declarations out of any CSS-shaped text — a stylesheet,
an SCSS partial, or the `<style>` blocks of an HTML export — wherever they appear:

```css
:root { --mxt-brand: #2b5170; --mxt-ground: #eef1f4; --mxt-ink: #1a2129; }
@media (prefers-color-scheme: dark) { :root { --mxt-ground: #16161a; } }
```

Declarations inside a dark block (`prefers-color-scheme: dark`, `.theme-dark`,
`[data-theme="dark"]`) seed the dark palette; everything else seeds the light
one. Tokens the design does not name keep the base theme's value, so a
five-colour handoff still yields a complete, working palette.

Three reasons this beats writing the tokens yourself, and each is a mistake this
skill used to teach:

1. **The Atlas mapping is already done, and it is ~60 variables, not eight.**
   Hand-mapping `--brand-primary`, `--topbar-bg` and a handful of others leaves
   most of Atlas — form controls, tables, modals, the pluggable widgets — on
   stock Mendix blue.
2. **Fonts are vendored, not `@import`ed from a CDN.** A
   `@import url("https://fonts.googleapis.com/…")` in `main.scss` is an
   `@import`-ordering trap, a third-party request on every page load, and it
   fails outright in an air-gapped deployment. The theme ships the `.woff2`
   files under `theme/web/mxcli-fonts/`.
3. **Light and dark come for free.** A hand-written `:root` block is one palette;
   every mxcli theme carries both and follows the OS before first paint.

### What to extract from the handoff

Ask the design step to emit a `--mxt-*` block if you can — then this is a parse,
not a judgement call. Otherwise read the values off the prototype and write them
into a small `tokens.css` to pass to `--from`. Run `mxcli theme show signal` for
the full vocabulary; the ones that carry the look:

| From the handoff | Token |
|---|---|
| brand / primary, its hover, text on a brand fill | `--mxt-brand`, `--mxt-brand-hover`, `--mxt-brand-ink` |
| app background, cards/panels, striped rows, hover, selected | `--mxt-ground`, `--mxt-surface`, `--mxt-surface-alt`, `--mxt-surface-hover`, `--mxt-surface-selected` |
| body text, muted text, faint text, hairlines | `--mxt-ink`, `--mxt-ink-muted`, `--mxt-ink-faint`, `--mxt-line` |
| sidebar / topbar chrome and its text | `--mxt-rail`, `--mxt-rail-line`, `--mxt-rail-ink`, `--mxt-rail-ink-active` |
| ok / warning / danger / info | `--mxt-success`, `--mxt-warning`, `--mxt-danger`, `--mxt-info` |
| status chip fills and their text | `--mxt-tint-ok` / `--mxt-tone-ok` (and `-warn`, `-risk`, `-info`, `-neutral`) |
| body font, headings, mono, base size, line height | `--mxt-font`, `--mxt-font-heading`, `--mxt-font-mono`, `--mxt-font-size`, `--mxt-line-height` |
| corner radius, row/control height, elevation, focus ring | `--mxt-radius`, `--mxt-radius-lg`, `--mxt-row-height`, `--mxt-control-height`, `--mxt-shadow`, `--mxt-focus-halo` |

**A `--mxt-*` name the base theme does not declare is refused, not written.**
Nothing reads it, so the theme would apply cleanly and render unchanged — which
is indistinguishable from the design never having been applied. If a handoff
value has no token, it belongs in the theme's own skin (below), not in the
palette.

### Two constraints worth knowing before you start

- **The navigation rail stays dark in both palettes.** Several Atlas topbar
  widgets paint their own text assuming a dark rail, at a specificity a simple
  override cannot beat. If the prototype has a light sidebar, expect to fight it
  — see `theme-styling`.
- **Never pin an Atlas variable to a literal colour.** Map it to a token so the
  dark variant restates ~30 values instead of ~60. A hardcoded
  `--font-color-default` is invisible the moment the ground goes dark.

### Fonts the theme does not ship

The built-in themes vendor IBM Plex, Source Sans/Serif, JetBrains Mono and Space
Grotesk. For a different family, drop the `.woff2` files into
`theme/mxcli-themes/<name>/files/theme/web/mxcli-fonts/`, add an `@font-face`
loop to that theme's partial beside the existing ones, and point `--mxt-font` at
it. Vendored, for the reasons above — not a CDN `@import`.

### If the design needs more than the palette

Genuinely bespoke chrome goes in the theme's own skin mixin
(`@mixin mxcli-<name>-skin` in `theme/mxcli-themes/<name>/files/theme/web/_mxcli-<name>.scss`),
where it is scoped with the theme and survives `theme apply`. Reach for `.ss-*`
classes in `main.scss` only for per-screen identity that is not part of the
design language — and read `atlas-design` first, because most of what looks
bespoke is an Atlas building block or utility class.

---

## ② Rebuild Components — Atlas block/class first, custom class only for identity

For each repeated element in the prototype (panel, stat tile, chip, card, table row,
progress bar…), **first check whether Atlas already provides it** (Atlas-first, above):
is there a building block (`list building blocks`) or an Atlas class / design property
(`card`, `btn-*`, `spacing-*`, `flex-*`+`align-*`, `['Card style': on]`) that gets you
most of the way? If so, use it and add a thin `.ss-*` class only for the brand delta
(colour, radius, font). Re-implementing `card`/`panel`/`btn` from scratch is the mistake
`atlas-design` exists to prevent.

When Atlas can't express the shape, write **one reusable class driven by the theme's
tokens**. Never a literal colour: a literal survives a light/dark flip and a theme
swap, and is wrong under every palette but the one you wrote it against. Keep classes
small and composable so a widget can stack several (`Class: 'ss-panel ss-grid-lv'`).

Check the theme first — `.pill` and `.stat` below already ship as recipe classes
(`mxcli theme show <name>`), so a chip and a KPI tile usually need no CSS at all.

```scss
// Surface panel — every value resolves through the palette
.ss-panel {
  background: var(--mxt-surface);
  border: 1px solid var(--mxt-line);
  border-radius: var(--mxt-radius);
  box-shadow: var(--mxt-shadow);
}

// Status chip — one base + colour modifiers. The theme's own `pill` /
// `pill-ok` / `pill-warn` / `pill-risk` do this already; write your own only
// if the design's shape genuinely differs.
.ss-chip {
  display: inline-block; border-radius: 11px; padding: 2px 10px;
  font-family: var(--mxt-font-mono); font-size: 11px; font-weight: 600;
  border: 1px solid transparent;
  white-space: nowrap;              // status chips must never wrap to 2 lines
}
.ss-chip--ok     { background: var(--mxt-tint-ok);   color: var(--mxt-tone-ok); }
.ss-chip--danger { background: var(--mxt-tint-risk); color: var(--mxt-tone-risk); }
```

**Base + modifier convention.** Give each component a base class and add `--variant`
modifiers for state/colour (`.ss-chip` + `.ss-chip--danger`, `.ss-heat--ok/--warn/--over`).
Widgets then combine base + modifier: `Class: 'ss-chip ss-chip--danger'`.

**Reshaping Mendix chrome.** To make Atlas widgets read like the prototype you often need
to override Mendix's own DOM classes. Common targets:

- Sidebar / topbar shell: `.region-topbar`, `.mx-header`, `.region-sidebar`,
  `.mx-scrollcontainer-left`, and nav items under `.mx-navigationtree`.
- ListView rows are the workhorse for grids/tables — neutralise Atlas's default row
  chrome (padding/border/background) so rows read as your design's grid lines:

  ```scss
  .ss-grid-lv > ul > li,
  .ss-grid-lv .mx-listview-item {
    padding: 12px 16px !important;
    margin: 0 !important;
    border-bottom: 1px solid var(--mxt-line);
  }
  ```

- `::before` / `::after` on `.mx-navigationtree` can inject brand blocks / section labels
  the design shows but the Mendix nav model doesn't produce.

Use `!important` sparingly but expect to need it when overriding Atlas widget CSS.

---

## Component → Mendix widget map

The lookup that removes the guesswork: for each component in the prototype, pick the widget
here **first**, then style it with your `--<prefix>` classes. Validated across the BAE Resource
Scheduling and Expense Approval designs.

| Design component | Mendix widget | Notes |
|---|---|---|
| Page / screen canvas | `container` | one per page, e.g. `Class: 'ea-page'` |
| Card / panel / section | Atlas `Card` building block, or `container class:'card'` / `['Card style': on]` | drop to a custom panel class only for brand delta |
| KPI / stat tile | `container` | label + value + delta as child `dynamictext` |
| Row/column layout (even columns, gaps) | `container` with `flex-row`/`flex-column` + `align-*` (or `['Flex container': …]`) | no `layoutgrid` needed for simple flex layouts |
| Multi-column / dashboard layout | `layoutgrid` + `row` + `column` | for exact fractional tracks (`2.4fr 1.2fr …`) use a `container` styled `display:grid` instead — see Layout techniques |
| Heading / title | `dynamictext` (RenderMode H1/H2) | |
| Body / label / caption / table cell | `dynamictext` | the workhorse — text is inline, see techniques |
| Metric / big number | `dynamictext` | mono class |
| Chip / badge / tag / status pill | `dynamictext` | base class + colour modifier; leading dot via CSS `::before` |
| Data table / grid / row list | `listview` (database source; row = `layoutgrid` or grid `container`) | preferred for bespoke row layouts — full control of the row markup; the `datagrid` pluggable widget exists but is heavier to style to a custom design |
| Table header row | static header band (`container`/`layoutgrid`) above the listview | |
| Tabs / segmented / filter-chip row | `tabcontainer` styled as pills (one `tabpage` per XPath-filtered view) | for static/decorative chips use `dynamictext` |
| Master list + detail pane | `listview` (Selection) + `dataview` (DataSource: SELECTION) | |
| Detail / read view | `dataview` | |
| Create / edit form | `dataview` + inputs + `footer` | Save/Cancel in the footer |
| Text input / multiline | `textbox` / `textarea` | |
| Dropdown / enum select | `combobox` | bound to an enum or association |
| Date field | `datepicker` | |
| Boolean / toggle | `checkbox` | |
| Button (primary/secondary) | `actionbutton` | `ButtonStyle` or a class |
| Link / text button | `linkbutton` | |
| Search box | listview built-in search bar | hoist/restyle via CSS |
| Avatar / initials | `dynamictext` | styled as a circle |
| Image / logo / thumbnail | `image` / `dynamicimage` / `staticimage` | |
| Icon / colour dot | CSS `::before` on a class | |
| Chart (line/bar/column/pie/area/bubble) | chart **pluggable widget** (Mendix Charts / ChartJS) | via `PLUGGABLEWIDGET '<id>'` — see Pluggable widgets below; needs a datasource + series config |
| Donut / gauge | ProgressCircle **pluggable widget** | via `PLUGGABLEWIDGET '<id>'`; static or attribute-driven — worked example below |
| Progress bar / meter | `progressbar` widget, or a `container` (track + fill) | a styled track+fill container needs no widget package |
| Sparkline / bespoke SVG | HTMLElement **pluggable widget**, or a `container` with a CSS SVG background | embed the design's inline SVG directly |

### Layout techniques

- **Exact fractional columns.** Atlas's `layoutgrid` is a 12-column system and can't express
  ratios like `2.4fr 1.2fr 1fr 1.4fr 0.6fr`. For pixel-faithful tables/dashboards, style a plain
  `container` as `display:grid; grid-template-columns: …` in its class and put the cell widgets
  as its **direct children** — each widget becomes a grid item.
- **`dynamictext` is inline by default.** For stacked text (a title over a subtitle) set
  `display:block` in the class, or the lines run together.

### Pluggable widgets (charts, donut, HTML/SVG)

Pluggable widgets **do round-trip through MDL** — but not by bare name. A bare
`progresscircle` / `CUSTOMWIDGET` is rejected by the builder (*"unsupported widget
type"*). The working form uses the widget's **full package id** as a quoted string:

```
PLUGGABLEWIDGET '<widget.package.id>' widgetName ( prop: value, … ) { childslots }
```

**One-time registration.** The widget package must be present in the project's `widgets/`
before you can reference its id:

```bash
mxcli widget init    -p baedemo.mpr                 # scaffold pluggable-widget support (run once)
mxcli widget extract -p baedemo.mpr --mpk widgets/ProgressCircle.mpk   # register a package
mxcli widget list    -p baedemo.mpr                 # list available widget ids + their props
```

`mxcli widget list` prints each widget's id and property names — copy the id **verbatim** into
the `PLUGGABLEWIDGET '…'` string, and use the property names it reports as the widget's props.

**Worked example — the status donut (Expense Dashboard).** A ProgressCircle in static mode,
with a text label overlaid via a sibling `container` (the widget draws only the ring):

```sql
container donutWrap (Class: 'ea-donut') {
  PLUGGABLEWIDGET 'com.mendix.widget.custom.progresscircle.ProgressCircle' donut (
    type: 'static', staticCurrentValue: 67, staticMinValue: 0, staticMaxValue: 100, showLabel: false
  ) { }
  container donutLabel (Class: 'ea-donut-label') {
    dynamictext donutPct (Content: '67%', Class: 'ea-donut-pct')
  }
}
```

This passed `mx check` with 0 errors, survived `docker build`, and renders its SVG arc at
runtime (verified on the dashboard).

**Real Mendix Charts** (BarChart/LineChart/PieChart/HeatMap/…) are fully authorable
too — each `series`/`line` binds its own OQL-view datasource + X/Y attributes;
Pie/HeatMap bind at the widget level (`ValueAttribute:`, Pie needs `SeriesName:`).
See **[Custom & Pluggable Widgets → Charts](../custom-widgets/SKILL.md)** for the chart-type
→ id table, per-chart required-property gotchas (TimeSeries needs a datetime X,
Bubble needs a size attribute), and the **CE0463 → `mxcli fix widgets`** step
(it normalizes the stored widgets *and* preserves MPRv2 storage — never run bare
`mx update-widgets` on a `mxcli new` project; it deletes `mprcontents/`).
`mdl-examples/doctype-tests/34-chart-widget-examples.mdl` is the full showcase.

**Pluggable-widget gotchas:**
- **Reach for built-ins first.** `listview`, `dynamictext`, `container`, `gallery`, `combobox`
  need no registration. Drop to a pluggable widget only when the design genuinely needs one
  (charts, gauges, embedded SVG, maps, sliders). Many "charts" in a handoff are just static
  SVG — a `container` with a CSS `background` SVG (KPI sparklines, area trends here) is lighter
  than a real chart widget and needs no datasource.
- **Reserved keywords can't be widget names** — `activity`, `legend`, etc. are rejected by the
  parser; rename (`actCard`, `legendCol`).
- **Empty slot is `{ }`.** Always close the child-slot braces, even when empty.

---

## The App Shell: Navigation & Layout (built once, not per page)

Most Claude Design prototypes render a **persistent sidebar + topbar** on every screen — a
brand block, a menu, sometimes a footer tag. It is tempting to rebuild that chrome inside each
page. **Don't.** In Mendix the shell is not a page — it comes from two shared places:

- **The layout** (`Atlas_Core.Atlas_Default` in this project) provides the topbar + left
  sidebar regions. Every page sets `Layout: Atlas_Core.Atlas_Default`, so they all inherit the
  same shell; the page's own widgets render only in the content region.
- **The navigation profile** supplies the menu items. One `Responsive` profile drives the whole
  app — home page, login page, and the flat/nested menu. Menu items point at pages, not at
  widgets you place.

So the prototype's sidebar maps to **navigation config + layout styling**, configured once, and
its menu grows by adding navigation items — never by editing pages.

### Add a screen to the menu

```bash
mxcli -p baedemo.mpr -c "LIST NAVIGATION"              # profiles, home page, item count
mxcli -p baedemo.mpr -c "LIST NAVIGATION MENU Responsive"   # the menu tree
```

Add or reorder items with `CREATE OR MODIFY NAVIGATION <Profile> …` (full-replacement — dump
the current profile first with `DESCRIBE NAVIGATION <Profile>`, edit, re-apply). See
`manage-navigation` for the item syntax, home/login pages, and role-based homes.

### Style the shell to match the design

The menu items and regions are standard Atlas DOM, so the prototype's look is reproduced with
CSS in `main.scss` (step ②) — you do **not** model the sidebar's chrome as widgets:

- Recolour the regions via the mapped Atlas vars (`--sidebar-bg`, `--topbar-bg`, `--navigation-bg`)
  or by overriding `.region-sidebar` / `.region-topbar` / `.mx-header` directly.
- Restyle menu entries under `.mx-navigationtree` (idle / hover / active states, spacing, the
  active-item accent bar).
- **Inject chrome the nav model can't express** — a brand logo block, a `WORKSPACE` section
  label, an `ITERATION 1 · DEMO` footer tag — with `::before` / `::after` on `.mx-navigationtree`
  (or the sidebar region). The Mendix navigation model has no field for these, so CSS
  pseudo-elements are the right tool; keep their text in the SCSS with the rest of the theme.

> Rule of thumb: if a design element is **the same on every screen**, it belongs to the shell
> (navigation + layout + CSS), not to a page. Only the content region is built per-page in ③.

### Restructuring the shell to match the prototype (full-height sidebar, fixed topbar)

Recolouring is rarely enough — most prototypes put a **full-height sidebar** (brand block at the
very top) with the topbar **only over the content**, whereas `Atlas_Default` renders the topbar
full-width *above* a sidebar+content row. Reproduce the prototype layout with CSS, no custom
layout document needed:

- **Full-height sidebar:** pin it out of the flex flow and offset the rest.
  ```scss
  .mx-scrollcontainer-left.region-sidebar { position: fixed; top: 0; left: 0; height: 100vh; z-index: 100; }
  .region-topbar, .mx-scrollcontainer-top,
  .region-content, .mx-scrollcontainer-center { margin-left: 256px !important; }  /* = sidebar width */
  ```
- **Force region sizes with `flex-basis`, not `width`/`height`.** Atlas sizes the topbar and
  sidebar as **flex items**, so plain `width`/`height` is ignored — use
  `flex: 0 0 256px !important` (sidebar) and `flex: 0 0 62px !important` (topbar).
- **Hide the collapse toggle** when the design has no collapse: `.toggle-btn { display: none !important; }`.
- The sidebar's inner scroll area (`.mx-scrollcontainer-wrapper`) should be `flex: 1 1 auto` so a
  `::after` footer tag pins to the bottom.

### Pixel-exact multi-part chrome via inline-SVG backgrounds

A `::before`/`::after` pseudo-element is a **single box with one text style** — it cannot render a
brand block (logo square + title + differently-styled subtitle) or a user chip (avatar circle +
two text lines) faithfully. For those, render the whole element as an **inline-SVG `background`
image**, which gives you exact sub-shapes, fonts, and colours:

```scss
.region-sidebar::before {
  content: ''; height: 80px; border-bottom: 1px solid rgba(255,255,255,.07);
  background: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='256' height='80'>\
<defs><linearGradient id='g' x1='0' y1='0' x2='1' y2='1'><stop offset='0' stop-color='%233a6fd6'/>\
<stop offset='1' stop-color='%231d3f86'/></linearGradient></defs>\
<rect x='22' y='21' width='38' height='38' rx='10' fill='url(%23g)'/>\
<text x='41' y='45' fill='%23fff' font-family='Public Sans' font-size='15' font-weight='800' text-anchor='middle'>EA</text>\
<text x='72' y='37' fill='%23fff' font-family='Public Sans' font-size='15' font-weight='700'>Expense Approval</text>\
<text x='72' y='55' fill='%237d8ba4' font-family='IBM Plex Mono' font-size='11'>Finance workflow</text></svg>") no-repeat;
}
```

Encode `#` as `%23` in the data URI. Use the same trick for the topbar's decorative search field
(a `::before` box with a magnifier-icon SVG background) and the user chip (`::after`). Simpler
single-style chrome — the `WORKSPACE` label, the `PROTOTYPE · DEMO` footer, per-item nav **dots**
(`.mx-navigationtree a::before { content:''; width:9px; height:9px; border-radius:3px; background: … }`,
coloured per item by its `.mx-name-*` anchor class) — stay as plain pseudo-elements.

> Decorative-only: an injected search box / avatar / "New" button is **not interactive** (it's
> CSS). The prototype's are usually mockups too; if the design needs a *working* search or profile
> menu, that's a real widget in a custom layout (Studio Pro), not CSS on the Atlas shell.

---

## ③ Apply Classes in Pages (MDL)

Every Mendix widget takes a `Class:` (static) and `DynamicClasses:` (expression) property.
This is how the theme reaches the page. Prefer **native Mendix widgets** styled with your
classes — `container`, `listview`, `dataview`, `dynamictext`, `tabcontainer` — over custom
widgets, which are far harder to drive from MDL.

### Static classes — the primary mechanism

Space-join base + modifiers in a single `Class:` string:

```sql
mdl 1;
create or modify page ResourceScheduling.ResourceHeatmap (
  Title: 'Resource Heatmap', Layout: Atlas_Core.Atlas_Default
) {
  container heatmapPage (Class: 'ss-page') {
    dynamictext heatmapTitle (Content: 'Resource Heatmap', RenderMode: H1, Class: 'ss-page-title')

    listview loadLV (
      DataSource: database from ResourceScheduling.Resource where LoadSeries != empty,
      Class: 'ss-panel ss-heat-lv'
    ) {
      container heatRow (Class: 'ss-heat-row') {
        dynamictext hc01 (Content: '{1}', ContentParams: ({1} = M01), Class: 'ss-heat-cell')
      }
    }
  }
};
```

### State-driven styling — `DynamicClasses:` expression

For colour/state that depends on data (over-capacity cell, conflict card, load bucket),
use a `DynamicClasses:` expression returning a space-separated class string. It **stacks on
top of** `Class:`.

```sql
container heatCell (
  Class: 'ss-heat-cell',
  DynamicClasses: if $currentObject/M01 >= 100 then 'ss-heat--over'
                   else if $currentObject/M01 >= 80 then 'ss-heat--warn'
                   else 'ss-heat--ok'
)
```

(Written as-is: plain single quotes inside, no outer quotes around the expression.)

### Computed dimensions — the bucket-class idiom

A widget has **no computed inline style**: `Style:` is a static string and
`DynamicClasses:` returns *class names*, not CSS values. So anything with a
data-driven dimension — a progress bar width, a bar-chart height, a meter fill —
can't be `Style: 'width: {expr}%'`. The idiom is to **quantise the value into a
bucket and generate one class per bucket**:

1. In a microflow, publish an integer bucket attribute (e.g. `0..20`):
   `$obj/PctBucket = round($obj/Done / $obj/Total * 20)`.
2. Generate the classes once with an SCSS `@for` loop:

```scss
@for $i from 0 through 20 { .ss-pb-#{$i} { width: $i * 5%; } }
```

3. Select the class from the bucket:
   `DynamicClasses: 'ss-pb-' + toString($currentObject/PctBucket)`.

Trade-off worth noting: this adds one bucket attribute per animated dimension to the
domain model. Pick a bucket count that matches the visual precision you need (20 → 5%
steps is usually plenty).

### Adding classes to an existing page

Use `alter page` to attach a class without rewriting the page (see `alter-page`):

```sql
mdl 1;
alter page ResourceScheduling.Approvals {
  set (Class: 'ss-appr-card ss-appr-card--conflict') on queueCard;
};
```

To apply the same class across many widgets/pages at once, see `bulk-widget-updates`
(`update widgets ... dry run` first).

---

## ④ Build & Preview

SCSS is **not** live — you must compile before the theme shows. See `theme-styling`.

```bash
mxcli docker build -p baedemo.mpr      # compiles SCSS into the deployment package (~55s)
mxcli docker reload -p baedemo.mpr --css   # pushes compiled CSS to browsers (instant)
```

- `--css` only pushes already-compiled CSS — always `docker build` first after editing SCSS.
- For widget-property changes (a new `Class:` on a page), use a normal `docker reload`.

---

## ⑤ Verify Against the Prototype

Put the running screen next to the handoff screenshot and reconcile the diff — spacing,
colours, font, component shapes. The project already has Playwright wired in (`test-app`)
for screenshotting the running app. Iterate ②–④ per screen until it matches.

---

## Never put a grid on a Mendix widget's own class

A layout that should be two columns comes out as one, and the CSS is right — it
is on the wrong element. Mendix wraps a repeating widget's children in an
intermediate element, so `display: grid` on the widget's own class has exactly
**one** grid item and every card stacks:

```
div.mx-listview.my-cards   [689x2054] display=grid    <- the class you wrote
  ul.                      [334x2038] display=block   <- ONE child
    li.mx-name-index-0     [334x526]                  <- the things you meant to lay out
```

A data view does the same with `.mx-dataview-content`. One project hit this
twice in two different widgets before naming the rule (mxcli-owid, findings #15
and #41).

Put the grid on the element that actually holds the repeated children:

```scss
/* list view */
.my-cards > ul                { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); }
.my-cards > ul > li           { min-width: 0; }

/* data view */
.my-page > .mx-dataview-content { display: grid; grid-template-columns: 240px 1fr; }
```

`min-width: 0` on the child matters: a grid item defaults to `min-width: auto`,
so a wide table or a long unbroken string inside a card pushes the column past
its track instead of scrolling within it.

**How to find the right element** rather than guess: run the app, inspect the
widget, and walk down from the class you wrote until you reach the element with
one child per row. `mxcli run --local --screenshot` plus the browser inspector
settles it in one pass — see `.claude/skills/verify-in-runtime.md`.

## Gotchas (learned building this app)

- **Never put `Style:` (inline style) on a `DYNAMICTEXT`** — it crashes MxBuild with a
  NullReferenceException. `Class:` on a DYNAMICTEXT is fine; for inline style, wrap it in a
  styled `container` instead. (Same applies to `alter styling`/`alter page set style`.)
- **Prefer `Class:` + a real CSS class over inline `Style:`.** It keeps the design system in
  one place and dodges the DYNAMICTEXT crash.
- **`DynamicClasses:` for state, not duplicate widgets.** One widget + an expression beats
  cloning a widget per state.
- **Status chips: `white-space: nowrap`** so labels like "Fully allocated" never wrap.
- **ListView rows carry Atlas padding/border/background** — neutralise them in your
  `.ss-*-lv` class or rows won't read as the design's grid.
- **For bespoke tables, prefer a styled `listview`** over the `datagrid` pluggable widget —
  you control the full row markup, which a pixel-faithful design usually needs.
- **Prefer typed `designproperties:` / Atlas classes over custom SCSS for anything Atlas
  covers** (spacing, alignment, card/background, flex layout) — see `atlas-design`. Keys
  and values are case-sensitive; `mxcli check -p` validates them (MDL-WIDGET11/12) and lists
  the allowed values. `theme-styling` has the compilation/reload mechanics.
- **`alter styling` can't find widgets in MDL-builder-created pages** — apply classes via
  `Class:`/`DynamicClasses:` in `create page` / `alter page` instead.

## MDL gotchas that block a faithful build

Distilled from a full prototype build. Some are covered in depth by the linked skills — this is
the fast index so a design migration doesn't rediscover them.

- **A real `docker build` is the only trustworthy check.** `mxcli check --references` passes MDL
  that `mxbuild` rejects. Build after every slice before you screenshot.
- **Quote to escape keywords in bindings; the unquoted form is cleaner in expressions.** Quote
  identifiers in `create entity`, `attribute:` bindings, and `create`/`change` targets when they
  collide with an MDL keyword. Inside expressions — microflow `if`/decisions, `contentparams`,
  `visible`, `dynamicclasses`, and inline-bracket XPath `where` — mxcli now **strips** stray
  identifier quotes, so a quoted attribute no longer breaks the build or makes an XPath `where`
  silently return 0 rows; still prefer the unquoted form there for readability. See
  `write-microflows`.
- **Show a related (to-one) object's attributes** — two ways, both persist now:
  - A nested **"data from context" DataView** for a full read view of the referenced object; its
    children bind to the related entity:
    ```
    dataview dvEmp (datasource: $currentObject/Module.Entity_Related) {
      dynamictext n (content: 'By {1}', contentparams: ({1} = Name))   -- own attr of the related entity
    }
    ```
  - An **association path** for a single inline value: `contentparams: ({1} = Entity_Related/Name)`
    in a `dynamictext`, or `attribute: Entity_Related/Name` on a DataGrid2 column — both persist as
    an AttributeRef over the association (use a **bare** association name; a module-qualified one is
    rejected on a column).
- **Status chips = one `dynamictext` + a `DynamicClasses:` expression** mapping the enum/value to
  a colour modifier (`ea-chip--ok/--warn/--danger`). Base `.ea-chip` + modifiers; `white-space: nowrap`.
- **Charts:** `ProgressCircle` / `ProgressBar` build and work (donut, meters). **Mendix Charts
  (`Charts.mpk`: column/bar/line/area/pie)** now author via MDL — each `series` (an object-list
  item inside the chart) binds a datasource plus X/Y attributes:
  `series s1 (staticDataSource: database from Module.View, staticXAttribute: "X", staticYAttribute: "Y")`
  (a per-series OQL view works too). `mxcli fix widgets` clears the
  widget-version-drift CE0463 (it normalizes the stored widgets and preserves MPRv2 storage —
  do not run bare `mx update-widgets`, which deletes `mprcontents/`). Still lighter when the design allows: a **CSS-background
  SVG** container (or `HTMLElement`) for sparklines/trends — no datasource — and `ProgressCircle`
  (`type: expression`, `expressionCurrentValue: '$currentObject/Rate'`, min `'0'` / max `'100'`,
  `labelType: percentage`) for a single-value gauge.
- **Dashboard aggregates → OQL view entities** (`write-oql-queries`). A grouped enum column in
  the view must be typed `enumeration(Module.Enum)`, not `string`, or you get CE6770. Test with
  `mxcli oql` first.
- **Seed demo data via an after-startup microflow** guarded to run once (skip if data exists); it
  must `return true`. Direct-SQL seeding doesn't apply to the default **HSQLDB**. See `demo-data`.
- **New entities / view entities need a runtime restart** (`docker up --detach --wait`) to
  register — a hot `reload` won't see them. Hot `reload` also occasionally crashes the runtime; if
  the app stops responding, `docker up --detach --wait` recovers it with data intact.
- **mxcli behaviour shifts between versions** — the chart-build and a few persistence quirks are
  version-dependent. If a documented property silently vanishes on write, check `mxcli version`
  and confirm with `describe page`.

---

## Checklist

- [ ] Read the handoff/screenshot for the screen **before** building or polishing it
- [ ] Palette built with `mxcli theme create --from <design-file>`, not hand-written — the Atlas mapping is ~60 variables and the theme already does it
- [ ] Every handoff value that has a `--mxt-*` token uses it; anything left over went into the theme's skin mixin, not the palette
- [ ] `mxcli theme apply <name> -p app.mpr` run, and the theme verified in a browser (light **and** dark)
- [ ] Fonts vendored under `theme/web/mxcli-fonts/` — no `@import url("https://fonts.googleapis.com/…")` anywhere
- [ ] Components are base classes + `--variant` modifiers, all using the project prefix
- [ ] Persistent sidebar/topbar built as **navigation profile + layout + CSS**, not per-page widgets; new screens added via `CREATE OR REPLACE NAVIGATION`
- [ ] Shell **restructured** to the design (full-height sidebar via `position:fixed` + `margin-left` offset; region sizes forced with `flex-basis`; collapse toggle hidden if unused)
- [ ] Multi-part chrome (brand block, user chip) rendered as **inline-SVG backgrounds**; single-style chrome (labels, dots, footer) as plain `::before`/`::after`
- [ ] Related (to-one) object attributes shown via a **nested "data from context" DataView** (full read view) or an **association path** (`contentparams: ({1} = Assoc/Attr)` / column `attribute: Assoc/Attr`) for a single inline value — both persist
- [ ] A real **`docker build`** run after each slice (not just `mxcli check`) before screenshotting
- [ ] Widgets styled with `Class:`; data-driven state via `DynamicClasses:`
- [ ] No inline `Style:` on any DYNAMICTEXT
- [ ] Grids/tables built as styled `listview`s, not custom Datagrid widgets
- [ ] Charts/gauges use `PLUGGABLEWIDGET '<id>' …` with the package registered via `mxcli widget extract`; static SVG (sparklines) done as CSS-background containers
- [ ] `docker build` then `docker reload --css` after SCSS edits
- [ ] Running screen verified against the prototype screenshot

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…