Front-end hat (pragmatic LiveView)
Ship the smallest thing that works and reads clearly. LiveView-first for the
operator console: the server holds the state, HEEx renders it, JS only when
LiveView genuinely can't. Marketing pages are different: they are public,
server-rendered HEEx and may have a distinctive visual language directed by
design-creative-director, as long as they remain fast, crawlable, accessible, and
honest.
Pick the surface first
- Operator console / LiveView: follow the CoreComponents, LiveTable, and IL-18
rules below. Consistency and calm matter more than novelty.
- Marketing pages (
controllers/marketing_html/**): use design-creative-director
for art direction and content-seo for crawlable positioning. Custom section
layouts are allowed; generic SaaS templates are not. Keep real content in the
initial HTML, avoid unnecessary dependencies, and verify with rendered desktop
and mobile screenshots.
Reuse before you build
EmisarWeb.CoreComponents first. It's large and already covers buttons,
inputs, tables, modals, flash, etc. Grep it before writing markup; extend it only
if the primitive is genuinely missing — and then it's shared, not one-off.
EmisarWeb.LiveTable for any list/table: it's stateless and URL-driven. Feed
it LiveTable.params_to_opts(params, Query.filters()) → Repo.list/3. Don't
hand-roll pagination, sorting, or filtering.
- Match the existing screens' Tailwind utility patterns. Don't invent spacing/color
scales; reuse what layouts and CoreComponents already use.
LiveView Iron Law (IL-18 — Credo won't catch these; you must)
- No unconditional DB/context read in
mount — mount runs twice. Use
assign_async, or connected?(socket) with a cheap disconnected branch.
stream/3 for any list that can grow (runs, audit events, runners). Never
assign(socket, :events, big_list) — it bloats socket memory per connection.
connected?(socket) guard before any PubSub subscribe (live runner/run
status updates), or you double-subscribe.
- Never
assign_new for per-mount values (current_user, locale) — use assign/3.
- Authorize in every
handle_event by routing through a context call with the
subject (IL-15). The button being hidden is not authorization.
Pragmatic rules
- One component, one job. Pass data in via
attr/slot; don't reach into parent
assigns. Function components for stateless UI; a LiveComponent only when it owns
state.
- Loading / empty / error states are part of the component, not a later pass (the
/design-ux hat will ask for them).
- Keep markup readable: no deeply nested conditionals in HEEx — compute in the LV,
render flat. Extract a function component when a block repeats.
- Forms use
to_form/2 + CoreComponents inputs; show changeset errors (IL-18's
sibling: if a save "silently fails", check {:error, changeset} first).
Fixing a reported UI defect
When the user points at something broken on a rendered surface, the fix ships
with before/after screenshot proof — and the BEFORE shot comes before any edit
(the broken state is unrecoverable once the fix deploys). Shoot the element from
the active workspace with ./run shot (--label before), using the current
task's screenshots/ directory (create and claim a basic task first when none
exists), fix,
rebuild the stack, re-shoot (--label after, full page + element crop), verify
the pixels yourself, then hand the user both paths for review. Full workflow:
portal/.agent/kb/rules/design-ui-fix-screenshot-proof.md.
Finish
Run ./run gate portal --changed from the repository root, click-test the happy
path + one error path, and confirm lists stream. Before pushing, run the complete
./run gate portal. Before a rendered surface is done, run the
design-interface-polish micro-craft pass against design-system §3 — calm on the
console, expressive on marketing. Hand UX judgment calls to /design-ux; keep this
hat on the implementation.
1---2name: design-frontend3description: Put on the pragmatic front-end hat for the emisar Phoenix UI and marketing HEEx — build correct LiveView/operator UI with CoreComponents and LiveTable, or execute public marketing pages from creative direction with server-rendered HEEx + Tailwind. Use when implementing or changing a LiveView, HEEx template, component, operator UI, or controllers/marketing_html page in apps/emisar_web.4---56# Front-end hat (pragmatic LiveView)78Ship the smallest thing that works and reads clearly. LiveView-first for the9operator console: the server holds the state, HEEx renders it, JS only when10LiveView genuinely can't. Marketing pages are different: they are public,11server-rendered HEEx and may have a distinctive visual language directed by12`design-creative-director`, as long as they remain fast, crawlable, accessible, and13honest.1415## Pick the surface first1617- **Operator console / LiveView:** follow the CoreComponents, LiveTable, and IL-1818 rules below. Consistency and calm matter more than novelty.19- **Marketing pages (`controllers/marketing_html/**`):** use `design-creative-director`20 for art direction and `content-seo` for crawlable positioning. Custom section21 layouts are allowed; generic SaaS templates are not. Keep real content in the22 initial HTML, avoid unnecessary dependencies, and verify with rendered desktop23 and mobile screenshots.2425## Reuse before you build2627- **`EmisarWeb.CoreComponents` first.** It's large and already covers buttons,28 inputs, tables, modals, flash, etc. Grep it before writing markup; extend it only29 if the primitive is genuinely missing — and then it's shared, not one-off.30- **`EmisarWeb.LiveTable`** for any list/table: it's stateless and URL-driven. Feed31 it `LiveTable.params_to_opts(params, Query.filters())` → `Repo.list/3`. Don't32 hand-roll pagination, sorting, or filtering.33- Match the existing screens' Tailwind utility patterns. Don't invent spacing/color34 scales; reuse what layouts and CoreComponents already use.3536## LiveView Iron Law (IL-18 — Credo won't catch these; you must)3738- **No unconditional DB/context read in `mount`** — `mount` runs twice. Use39 `assign_async`, or `connected?(socket)` with a cheap disconnected branch.40- **`stream/3` for any list that can grow** (runs, audit events, runners). Never41 `assign(socket, :events, big_list)` — it bloats socket memory per connection.42- **`connected?(socket)` guard before any PubSub `subscribe`** (live runner/run43 status updates), or you double-subscribe.44- **Never `assign_new` for per-mount values** (`current_user`, locale) — use `assign/3`.45- Authorize in **every** `handle_event` by routing through a context call with the46 subject (IL-15). The button being hidden is not authorization.4748## Pragmatic rules4950- One component, one job. Pass data in via `attr`/`slot`; don't reach into parent51 assigns. Function components for stateless UI; a LiveComponent only when it owns52 state.53- Loading / empty / error states are part of the component, not a later pass (the54 `/design-ux` hat will ask for them).55- Keep markup readable: no deeply nested conditionals in HEEx — compute in the LV,56 render flat. Extract a function component when a block repeats.57- Forms use `to_form/2` + CoreComponents inputs; show changeset errors (IL-18's58 sibling: if a save "silently fails", check `{:error, changeset}` first).5960## Fixing a reported UI defect6162When the user points at something broken on a rendered surface, the fix ships63with before/after screenshot proof — and the BEFORE shot comes before any edit64(the broken state is unrecoverable once the fix deploys). Shoot the element from65the active workspace with `./run shot` (`--label before`), using the current66task's `screenshots/` directory (create and claim a basic task first when none67exists), fix,68rebuild the stack, re-shoot (`--label after`, full page + element crop), verify69the pixels yourself, then hand the user both paths for review. Full workflow:70`portal/.agent/kb/rules/design-ui-fix-screenshot-proof.md`.7172## Finish7374Run `./run gate portal --changed` from the repository root, click-test the happy75path + one error path, and confirm lists stream. Before pushing, run the complete76`./run gate portal`. Before a rendered surface is done, run the77`design-interface-polish` micro-craft pass against design-system §3 — calm on the78console, expressive on marketing. Hand UX judgment calls to `/design-ux`; keep this79hat on the implementation.