Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
d52fc0e
feat(theme): ship a default styling package applied by mxcli new
claude Aug 6, 2026
44396fb
fix(mappings): strip quotes from identifiers in mapping bodies
claude Aug 6, 2026
c052b6d
fix(pages): bind arguments for a parameterized microflow datasource
claude Aug 6, 2026
fd18dea
fix: persist and read back consumed REST operation mappings (#843)
claude Aug 6, 2026
28b9ccc
Merge pull request #97 from ako/claude/fix-842-mapping-quoted-identif…
ako Aug 6, 2026
a7f40b2
Merge branch 'main' into claude/fix-835-datagrid-microflow-params
ako Aug 6, 2026
5266551
Merge pull request #98 from ako/claude/fix-835-datagrid-microflow-params
ako Aug 6, 2026
8fe3538
Merge branch 'main' into claude/fix-843-rest-response-mapping
ako Aug 6, 2026
8ea774d
Merge pull request #99 from ako/claude/fix-843-rest-response-mapping
ako Aug 6, 2026
32cedfe
fix(alter-page): reject ambiguous DataGrid2 column refs, list real na…
claude Aug 5, 2026
e86412b
fix(alter-page): detect DataGrid2 column ambiguity page-wide, not per…
claude Aug 5, 2026
d115c5f
fix(microflows): wire the false branch of a conditional break/continu…
claude Aug 5, 2026
a306810
fix(workflows): refuse standalone `annotation`, which wrote an unload…
claude Aug 6, 2026
18de30b
fix(pages): never write an unresolved association DestinationEntity (…
claude Aug 6, 2026
3391322
docs(proposal): workflow / microflow syntax alignment
claude Aug 6, 2026
4710ec5
fix(workflows): stop DESCRIBE inventing a comment on `jump to` (issue…
claude Aug 6, 2026
93cf734
fix: resolve workflow context references in every authored expression
claude Aug 6, 2026
f0d1aea
fix: resolve cross-module association paths in page bindings
claude Aug 6, 2026
74bc820
fix: cover every entity member in a GRANT (both-owner assocs, audit m…
claude Aug 6, 2026
a987ab2
docs(fix-issue): drop the duplicate CE1571 datasource-argument row
claude Aug 6, 2026
8df6c54
fix(check): reject audit-member rights at check time too (MDL-SEC01)
claude Aug 6, 2026
e4da7dc
test(workflows): assert the annotation refusal instead of the round-trip
claude Aug 6, 2026
a86ccb9
Merge pull request #100 from ako/claude/mxbuild-diagnostics-spike-emta6h
ako Aug 6, 2026
a690d64
fix(javascript-action): write sources to the lowercased module directory
claude Aug 6, 2026
b1ce75b
feat(theme): light/dark variants, runtime switching, and the ledger +…
claude Aug 6, 2026
c4f40dd
docs(theme): close the discoverability gaps and fix two review findings
claude Aug 6, 2026
0ff4226
fix(xpath): accept a negative numeric literal in a constraint
claude Aug 6, 2026
679aa54
Merge pull request #101 from ako/claude/fix-xpath-negative-literal
ako Aug 6, 2026
5714443
Merge branch 'main' into claude/mendix-default-styling-1fgvpm
ako Aug 6, 2026
348b9de
Merge pull request #102 from ako/claude/mendix-default-styling-1fgvpm
ako Aug 6, 2026
fae7735
hub overview: fixed column widths and a first-seen column
claude Aug 6, 2026
0c4675f
fix(theme): remove targeted the wrong theme, switching orphaned a blo…
claude Aug 6, 2026
323e1f9
Merge pull request #103 from ako/claude/mxcli-hub-columns-timestamps-…
ako Aug 6, 2026
d1078d0
Merge pull request #104 from ako/claude/mendix-default-styling-1fgvpm
ako Aug 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .claude/commands/mxcli-dev/review.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ proactively. Add a row after every review that surfaces something new.
| 16 | Bug-fix PR missing `mdl-examples/bug-tests/<issue>-description.mdl` — checklist requires one per fix so Studio Pro can validate the regression case | Test coverage | Add minimal MDL that reproduces the symptom; commit alongside the fix; the PR description often contains the exact reproduction snippet already |
| 17 | Commit message claims a change (e.g. `"PERF001": "Performance"` mapping in `report.go`) that is not present in the diff — git body overstates the actual change, often referencing an example rule as if it were shipped | Docs quality | Diff the file the commit names (`git show <sha> -- <file>`); if the change isn't there, fix the commit body so it doesn't imply shipped behavior |

| 18 | A generated artifact hardcodes a value that an exported constant also declares (e.g. `SwitcherStorageKey = "mxcli-theme"` beside four literal `"mxcli-theme"` in the template) — the constant and the artifact can drift, and a test asserting `Contains(output, TheConst)` keeps passing because it is checking the literal, not the link | Test coverage | Substitute the constant into the template (`{{KEY}}` + `strings.NewReplacer`) so there is one source of truth; assert the placeholder is expanded *and* the expected occurrence count |
| 19 | Docs rewritten in one section while an earlier section still points at the removed content — e.g. "Copy the scaffold below" left in place after the scaffold was replaced by "do not hand-roll a scaffold", producing a direct contradiction two paragraphs apart in a skill `mxcli init` syncs into every user project | Docs quality | After deleting or replacing a doc section, grep the whole file for phrases that referred to it ("below", "scaffold", the old heading) and for the old anchor in the Contents list |
| 20 | Asset-driven feature (themes, templates) whose correctness depends on a toolchain the Go tests never run — SCSS that must compile, a mixin whose name must match its `@include`. A broken asset ships and fails at the user's build, not in CI | Test coverage | Assert the naming/structural contract in Go (`@mixin X {` and `@include X;` both present, every `url()` resolves to a shipped file); compile once by hand against a real project and record it in the proposal |

---

## After Every Review
Expand Down
21 changes: 21 additions & 0 deletions .claude/skills/fix-issue.md

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions .claude/skills/mendix/alter-page.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,10 @@ If an ALTER targeting a DataGrid column completes without error but makes no cha

If the column name you copied from DESCRIBE still doesn't work, check whether the column has an attribute binding — attribute names take priority over captions.

**The authored `column colFoo (...)` name is NOT how you address it.** A column carries no stored name in the Mendix model, so the name you wrote in `create page` is dropped on write — always address a column by its *derived* name (the one `describe page` shows). Using the authored name now fails with an error that lists the available column names, rather than a bare "not found".

**Duplicate captions are ambiguous and rejected.** Two dynamic-text (or custom-content) columns with the same caption derive the same name, so `ON "Amount"` can't tell them apart. mxcli now refuses the operation with an ambiguity error instead of silently mutating the first and leaving the second unreachable. Give such columns distinct captions to address them individually. (Non-attribute column handles are the caption, so `set Caption = ...` also *renames* the handle — plan multi-step caption edits accordingly.)

### ADD Variables - Add a Page Variable

```sql
Expand Down
206 changes: 114 additions & 92 deletions .claude/skills/mendix/atlas-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ built on the Atlas building blocks every Mendix project already ships.
4. [Atlas building blocks — the out-of-the-box inventory](#atlas-building-blocks--the-out-of-the-box-inventory)
5. [Atlas appearance vocabulary — classes & design properties](#atlas-appearance-vocabulary--classes--design-properties)
6. [Brand re-tune (Layer 1) — where most of the win is](#brand-re-tune-layer-1--where-most-of-the-win-is)
7. [Layer-1 brand scaffoldcopy into theme/web/custom-variables.scss](#layer-1-brand-scaffold--copy-into-themewebcustom-variablesscss)
7. [Layer 1 in practicestart from the shipped theme](#layer-1-in-practice--start-from-the-shipped-theme)
8. [Charts — a dataviz-grade theme for the Mendix chart widgets](#charts--a-dataviz-grade-theme-for-the-mendix-chart-widgets)
9. [Dark mode — commit to one theme](#dark-mode--commit-to-one-theme)
9. [Dark mode — Mendix 11 makes this cheap](#dark-mode--mendix-11-makes-this-cheap)
10. [Optional dark-mode Atlas-widget overrides](#optional-dark-mode-atlas-widget-overrides)
11. [Verify at runtime — this is mandatory](#verify-at-runtime--this-is-mandatory)
12. [Gotchas catalog](#gotchas-catalog)
Expand Down Expand Up @@ -61,8 +61,8 @@ Style from the bottom up. Each layer only does what the layer below can't.

```
Layer 3 VERIFY run --local --watch + Playwright screenshot (mx check is NOT enough)
Layer 2 IDENTITY themesource/<mod>/web/main.scss — custom tokens + recipe classes
(mono type, status pills, timeline spine) — ONLY what Atlas can't provide
Layer 2 IDENTITY theme/web/_<name>.scss, imported from theme/web/main.scss — recipe
classes (mono type, status pills, timeline spine) — ONLY what Atlas can't do
Layer 1 BRAND theme/web/custom-variables.scss — retune Atlas tokens (--brand-primary,
backgrounds, semantic colors, radius) so Atlas components inherit the palette
Layer 0 ATLAS Atlas classes / design properties / building blocks — structure & base look
Expand All @@ -72,13 +72,26 @@ Layer 0 ATLAS Atlas classes / design properties / building blocks — str
the building-block inventory below).
- **Layer 1 — Brand.** Retune Atlas tokens in `theme/web/custom-variables.scss` so
the whole framework (buttons, backgrounds, form inputs, pluggable widgets like
Switch/Slider/ProgressBar) picks up your palette. Scaffold below.
- **Layer 2 — Identity.** Only the handful of shapes Atlas genuinely can't express
go in `main.scss` as prefixed recipe classes. See `theme-styling.md` for the SCSS
chain and `migrate-design-prototype.md` for the token→component method.
Switch/Slider/ProgressBar) picks up your palette. Start from the shipped theme
rather than a blank file — see below.
- **Layer 2 — Identity.** Only the handful of shapes Atlas genuinely can't express.
Put them in a partial imported from **`theme/web/main.scss`**, which compiles
*last* — after Atlas Core and after every module theme source — so your rules win
without `!important`. Use `themesource/<mod>/web/main.scss` only when the styling
belongs to that module: a theme source folder whose name does not match a real
module is **silently not compiled**. See `theme-styling.md`.
- **Layer 3 — Verify.** Non-negotiable. `mx check` misses client-side crashes; you
must screenshot a *running* build.

**Start from the shipped default, don't start from nothing.** `mxcli new` applies
the `signal` theme, and `mxcli theme apply -p app.mpr` adds one (`signal`,
`ledger` or `console`) to an existing project. Each carries a full palette in
both light and dark, vendored fonts, the focus ring, the density scale and the
`num` / `pill` / `stat` recipe classes. Re-brand by changing `--mxt-brand` in the
palette block; the block is digest-fenced, so mxcli refuses to overwrite your
edits rather than silently discarding them. `mxcli theme show <name>` lists
exactly which files it writes.

A Layer-1 token retune **cascades down** into Atlas components and pluggable
widgets for free — that is the headline payoff. A full re-brand (new palette, type,
corners) is **theme-only**: retune `custom-variables.scss` + `main.scss`, zero
Expand Down Expand Up @@ -415,9 +428,10 @@ more idiomatic form to mirror from a `describe building block`. Notes:

## Brand re-tune (Layer 1) — where most of the win is

Copy the scaffold below into `theme/web/custom-variables.scss` and set the
placeholder palette. Because Atlas utilities and pluggable widgets read these tokens,
one retune re-skins the whole app:
Retune the palette in `theme/web/custom-variables.scss` — the file
`mxcli theme apply` writes (see the next section; do not hand-roll one). Because
Atlas utilities and pluggable widgets read these tokens, one retune re-skins the
whole app:

- `--brand-primary` → buttons, `background-primary`, links, Switch/Slider/ProgressBar
- background + semantic (`success`/`warning`/`danger`) tokens → alerts, group boxes,
Expand All @@ -430,82 +444,66 @@ only for shapes Atlas can't provide.

---

## Layer-1 brand scaffold — copy into theme/web/custom-variables.scss
## Layer 1 in practice — start from the shipped theme

**Do not hand-roll a brand scaffold.** `mxcli theme apply -p app.mpr` writes a
complete, verified Layer 1 (and Layer 2) into `theme/web/`, and `mxcli new`
applies one by default. Re-brand it instead of competing with it — the generated
blocks are digest-fenced, so a hand-written palette in the same file will either
be refused on the next apply or silently fight the theme in the cascade.

```bash
mxcli theme list # signal (default), ledger, console
mxcli theme show signal # palette, and every file it writes
mxcli theme apply signal -p app.mpr # --variant auto | light | dark
```

### The token architecture it gives you

A theme separates the palette from the wiring, and that split is the whole reason
a light/dark flip or a re-brand is cheap:

| File | Holds | You edit |
|---|---|---|
| `theme/web/custom-variables.scss` | the palette — `--mxt-*` tokens for the default variant | **yes, this one** |
| `theme/web/_mxcli-atlas-map.scss` | ~60 Atlas variables expressed as `var(--mxt-*)` | no |
| `theme/web/_mxcli-<name>.scss` | the other palette, variant blocks, `@font-face`, recipe classes | rarely |

To re-brand, change one line in the palette:

```scss
// =============================================================================
// Layer 1 — BRAND: retune Atlas tokens
// -----------------------------------------------------------------------------
// Copy this into theme/web/custom-variables.scss and swap the placeholder
// palette below for your brand.
//
// WHY THIS FILE MATTERS: Atlas classes and pluggable widgets READ these tokens.
// Retuning them here cascades the palette DOWN into buttons, `background-*`
// utilities, form inputs, cards, popups, and pluggable widgets (Switch, Slider,
// RangeSlider, ProgressBar, ProgressCircle, BadgeButton) — with NO per-widget CSS.
// This is the single highest-leverage styling change you can make.
//
// These vars use Atlas's `!default` chain, so they override
// atlas_core/web/variables.scss. See `theme-styling.md` for the compile order.
// Reach for THIS layer before writing any custom class in main.scss (Layer 2).
// =============================================================================

// 1. BRAND PRIMARY — the one colour that defines the app.
// Flows into: btn-primary, background-primary, links, active nav, and the
// brand-reading pluggable widgets (Switch / Slider / ProgressBar / …).
$brand-primary: #2b5170 !default; // TODO: your brand colour
$brand-secondary: #5c6a78 !default; // TODO: muted / secondary accent

// 2. SEMANTIC COLOURS — success / warning / danger / info.
// Flows into: btn-*, background-*, groupbox-*, alerts, status surfaces.
$brand-success: #4a7a5c !default; // TODO
$brand-warning: #c9a227 !default; // TODO
$brand-danger: #a13a2c !default; // TODO
$brand-info: #2f6f9f !default; // TODO

// 3. BACKGROUNDS & INK — the neutral ground the app sits on. Retune these so
// Atlas surfaces OUTSIDE your scoped classes (form inputs, popups, modals)
// inherit the palette too.
$bg-color: #eef1f4 !default; // TODO: app background
$background-color-page: $bg-color !default;
$font-color-default: #1a2129 !default; // TODO: body ink
$font-color-detail: #5c6a78 !default; // TODO: secondary / muted text
$border-color-default: #dde3ea !default; // TODO: hairline borders

// Form inputs — keeps inputs on-palette everywhere (incl. popups).
$form-input-bg: #ffffff !default; // TODO
$form-input-border-color: $border-color-default !default;
$form-input-color: $font-color-default !default;

// 4. SHAPE — corner radius. 0 = sharp/industrial; higher = soft/friendly.
// Cascades into cards, inputs, buttons, popups.
$border-radius-default: 8px !default; // TODO: 0 … 16px
$card-border-radius: $border-radius-default !default;

// 5. TYPOGRAPHY — set a brand font. If it is a WEB font, `@import` it as the
// FIRST line of main.scss (an @import after any rule is silently dropped), and
// ALWAYS keep a system fallback stack so the layout survives a font-load fail.
$font-family-base: "system-ui", -apple-system, "Segoe UI", sans-serif !default; // TODO

// Bridge Atlas CSS custom properties to the Sass vars above, so runtime CSS
// (`var(--brand-primary)`, `background-primary`, etc.) resolves to your palette.
:root {
--brand-primary: #{$brand-primary};
--brand-secondary: #{$brand-secondary};
--brand-success: #{$brand-success};
--brand-warning: #{$brand-warning};
--brand-danger: #{$brand-danger};
--brand-info: #{$brand-info};

--bg-color: #{$bg-color};
--font-color-default: #{$font-color-default};
--font-color-detail: #{$font-color-detail};
--border-color-default: #{$border-color-default};
--card-border-radius: #{$card-border-radius};
--font-family-base: #{$font-family-base};
--mxt-brand: #0f6e6b; /* the one colour that defines the app */
--mxt-ground: #f4f6f8; /* app background */
--mxt-surface: #ffffff; /* cards, modals, panels */
--mxt-ink: #14181f; /* primary text */
--mxt-line: #dce1e7; /* hairlines */
}
```

Atlas derives `--brand-primary-50` … `-900` from `--brand-primary` with CSS
`color-mix()`, so buttons, links, active navigation, alerts, group boxes and the
brand-aware pluggable widgets (Switch, Slider, RangeSlider, ProgressBar,
ProgressCircle, BadgeButton) all follow — in **both** palettes, with no
per-widget CSS.

### Two rules that decide whether your styling survives

1. **Mendix 11 Atlas is CSS-custom-property-first.** Write `:root { --x: … }`
declarations, not SCSS `$x: … !default;`. The stock `custom-variables.scss` is
a `:root` block plus a few SCSS switches (`$font-family-import`,
`$btn-bordered`, `$use-css-variables`); legacy Sass variables are still mapped
for old modules, but they are not the idiom.
2. **Never pin an Atlas variable to a literal colour.** Map it to a token
(`--bg-color: var(--mxt-ground)`), which is what the Atlas map does. A
hardcoded `--font-color-default` is near-black on a near-black ground the
moment anything flips the palette — the failure is total and silent.

If you genuinely need a token the theme does not expose, add it to the palette
block and reference it from your own Layer-2 rules. See `theme-styling.md` for
the compile order and for why `theme/web/main.scss` is the only correct home for
app-level rules.

---

## Charts — a dataviz-grade theme for the Mendix chart widgets
Expand Down Expand Up @@ -564,20 +562,44 @@ scaffold). The generic `dataviz` skill is the HTML/React analogue of this — sa

---

## Dark mode — commit to one theme
## Dark mode — Mendix 11 makes this cheap

Older guidance here said to commit to a single theme, because a
`prefers-color-scheme` flip repainted your own classes but left Atlas widgets
light. **That was Atlas 3. It does not hold on Mendix 11.**

Measured by adding `theme-dark` to `<html>` on a running 11.13 app and changing
nothing else: the page ground, cards, form controls, sidebar, buttons and
DataGrid2 all followed. Atlas is CSS-custom-property-first now, so the token
cascade genuinely propagates. And because the class lands on `<html>`, popups and
modals — which Mendix renders at `<body>`, outside any page container — follow it
too, which was the other half of the old objection.

The practical route is `mxcli theme apply <name>` with the default
`--variant auto`: it ships both palettes, follows the OS before first paint, and
honours a `theme-light` / `theme-dark` class when a switcher sets one. Add
`mxcli theme switcher install` for a user-facing toggle.

Three things to know if you build this by hand:

1. **Mendix ships the slot, not the switcher.** `theme/web/_theme-dark.scss`
declares `:root.theme-dark`; nothing in Atlas ever applies the class.
2. **Your dark block must come after Mendix's** — same specificity, later wins.
Otherwise its stock Mendix blue overrides your brand the moment the class
appears.
3. **Anything you pinned to a literal colour breaks.** This is the whole reason
Layer 1 maps Atlas variables to tokens instead of to hex values.

A `prefers-color-scheme: dark` flip repaints **your** custom chrome, but Atlas's own
widgets and Plotly ship **light-only** surfaces — on a dark page they render as white
boxes with (often) near-invisible text. **Decide theme-count up front:**
The rail is the one place Atlas still assumes: several topbar widgets paint text
with `--color-base`, expecting white because they expect a dark navigation rail.
Keep the rail dark in both palettes, or force `color: inherit` on those widgets.

- A **dark-only** app is simpler and more robust — drop the `@media` gate and make
the widget overrides **unconditional + global** (this also covers portal-rendered
popups/modals that live outside your scoped class).
- If you can't fund the override recipe, ship **light-only**. A half-dark result
(your chrome dark, Atlas widgets light) is **worse** than a consistent light app.
Charts remain the exception — series colour lives in the model
(`customSeriesOptions`), not CSS, so it does not follow a runtime flip. Use the
transparent `paper_bgcolor` trick above, which is correct in both palettes.

Charts are the exception — don't CSS them; use the transparent `customLayout` trick
above, which adapts to light **and** dark automatically.
The override sheet below is still useful for a hand-rolled dark theme, or for
Atlas corners a token flip misses.

---

Expand Down
29 changes: 29 additions & 0 deletions .claude/skills/mendix/create-page.md
Original file line number Diff line number Diff line change
Expand Up @@ -1134,6 +1134,35 @@ The following features are NOT implemented in mxcli and require manual configura
> DYNAMICTEXT spacer (Content: ' ')
> ```

### Binding across modules and to audit members

An attribute path may cross module boundaries, including into the platform's
`System` module — the association does not need to live in the same module as
the entity it targets:

```sql
create association IT.Issue_Assignee from IT.Issue to System.User;

DATAVIEW dv (DataSource: $Issue) {
DYNAMICTEXT txtAssignee (Attribute: Issue_Assignee/Name) -- into System
DYNAMICTEXT txtApprover (Attribute: Issue_Approver/Name) -- into another module
}
```

A bare association name is qualified with the module of the entity the widget
sits on. On a ComboBox that matters: its `DataSource:` is the *option list*, but
`Association:` names a reference on the containing entity, so
`Association: Issue_Assignee` resolves against the dataview's entity, not the
option list's module.

Audit members declared with the `Auto*` pseudo-types bind under the name you
declared:

```sql
create or modify persistent entity IT.Issue ( CreatedDate: AutoCreatedDate );
DYNAMICTEXT txtCreated (Attribute: CreatedDate) -- also accepts createdDate
```

**Script Execution Note:** Script execution stops on the first error. If a page fails to create (e.g., invalid widget syntax), earlier statements in the script will have already been committed. Plan scripts with uncertain syntax in phases.

## Tips
Expand Down
Loading
Loading