This guide records common misunderstandings coding agents make when working on Dialup apps. Add a new section here whenever a review or production issue reveals a pattern agents get wrong.
1. layout.css classes already apply on every page
The mistake
An agent sees page-specific CSS such as agent_demo/page.css and assumes that each page only has access to its own page.css. It then reimplements shared UI (buttons, code blocks, shadows) with new selectors like .mcp-handoff-btn or .mcp-add button.
The reality
Dialup colocation CSS works like this:
layout.css(next tolayout.ex) defines styles shared by all pages under that layout.page.css(next topage.ex) adds page-specific rules only.- At render time, the framework injects both into one
<style data-dialup-css>block.
DOM structure:
<div class="d-layout"> <!-- layout.css scope -->
<nav>...</nav>
<main>
<div class="d-page"> <!-- page.css scope -->
... page content ...
</div>
</main>
</div>Layout wraps the page. Classes from layout.css — for example .btn, .btn-primary, .btn-ghost — match elements inside any page as long as you put the class on the element.
There is no separate import step and no UI component module required. Reuse is class-based:
<.dialup_action navigate="/docs" class="btn btn-ghost">Get Started</.dialup_action>
<button type="submit" class="btn btn-ghost">Submit</button>
<button type="button" class="btn btn-ghost btn-block" data-mcp="handoff">Hand off</button>What belongs where
| File | Use for |
|---|---|
layout.css | Site-wide tokens, buttons (.btn*), nav, docs chrome, shared utilities (.btn-block, .btn-sm) |
page.css | Layout and content unique to that page (grids, cards, domain-specific regions) — not another copy of button styles |
How to avoid it
Before adding button CSS to page.css, check layout.css for existing .btn variants. Prefer:
.btn.btn-ghost— purple CTA.btn.btn-primary— light background.btn.btn-accent— accent2 highlight.btn.btn-sm— compact control.btn.btn-block— full width
Add a new variant in layout.css only when the design system genuinely needs one, not per page.
Reference implementation
See site/lib/app/layout.css (definitions) and site/lib/app/agent_demo/page.ex (usage on MCP demo buttons).
2. ws-change inputs should not return {:update, assigns}
The mistake
A text field uses ws-change + ws-debounce, but handle_event/3 returns {:update, assigns}. Every debounced keystroke triggers a full #dialup-root morph. The input has value={@field} from the server, so the cursor jumps and characters disappear — especially on deployed sites with higher WebSocket latency.
The reality
For live typing, follow Events — ws-change:
- Human input over
ws-change→{:noreply, assigns}(state only, no DOM replace) - Optional feedback elsewhere →
{:patch, id, html, assigns}on a different element (see/demodraft_change) - Agent-only updates that must refresh the field →
{:patch, input_id, render_input(assigns), assigns}
Reference implementation
site/lib/app/agent_demo/page.ex — set_project/3site/lib/app/demo/page.ex — draft_change/3