# Screen craft playbook (AI agents)

Read this before building any **new screen, flow, or component** — especially
Kasy Flows catalog items. It exists so screens come out professional
(responsive, elegant, on-brand) on the first pass, and so a bug found once
never gets re-discovered from scratch in a different chat.

This is a **living document**. When you hit a real bug or a real quality
lesson while building a screen, add it to the sections below — don't just fix
it and move on. Keep entries short: symptom → root cause → fix. No entry
without all three.

## Quality bar for a new screen

- **Design system only.** Tokens from `DESIGN_SYSTEM.md`
  (`context.colors.*`, `context.kasyTextTheme.*`, `KasySpacing.*`,
  `KasyRadius.*`, `KasyIconSize.*`) — never hardcoded colors/sizes in a
  screen meant to ship. (Hardcoded `TextStyle`/`Colors.*` is fine only as a
  throwaway diagnostic, never in the final widget.)
- **Icons:** check `KasyIcons` first; fall back to `LucideIcons.*` directly
  for a one-off glyph not yet catalogued (`package:lucide_icons_flutter`).
- **Real imagery, not placeholders.** Unsplash direct CDN works for
  prototypes/Flows previews without an API key:
  `https://images.unsplash.com/photo-{id}?auto=format&fit=crop&w=1200&q=80`.
  Pick an image that actually matches the screen's subject and mood — don't
  grab the first result.
- **Both themes, always.** Verify light AND dark before calling a screen
  done. Colors from `context.colors.*` already adapt; don't fight that with
  hardcoded values.
- **Responsive by construction.** Use `Expanded`/`Flexible`/`LayoutBuilder`
  over fixed pixel heights where the screen has to work across phone sizes.
- **Kit components over raw Material.** `KasyButton`, `KasyCard`, etc. — see
  `AGENTS.md` golden rules for the full list.

## Structural rule: every screen needs a `Material` ancestor

Even a full-bleed screen that skips `Scaffold` on purpose (hero image behind
the status bar, no app bar) still needs a `Material` ancestor somewhere above
its `Text` widgets:

```dart
return Material(
  type: MaterialType.transparency,
  child: /* your Stack/Positioned full-bleed layout */,
);
```

Skipping this doesn't just risk missing ink/ripple effects — it can trigger
a real Flutter Web rendering bug (see Known Issues #1). Default to wrapping
in `Material(type: MaterialType.transparency)` any time a screen's root isn't
already a `Scaffold`.

## Verification checklist before calling a screen done

1. `flutter analyze` on the changed files — must be clean, not "mostly clean".
2. Hot reload via `r`/`R` (or MCP `reload_app`), never a raw browser
   refresh/URL reload — see `docs/agent-browser-qa.md`. A plain browser
   reload does not recompile Dart and will make you debug stale code.
3. Screenshot in **both** light and dark.
4. If something looks wrong and you don't know why: don't guess out loud
   repeatedly — investigate empirically (isolate a minimal reproduction,
   compare against a known-clean screen) before proposing a fix. See the
   diagnostic method in Known Issues #1 as the template for how to do this.

## Known issues & fixes

### 1. Solid yellow line under text (Flutter Web debug, semantics-only)

**Symptom:** a solid yellow underline appears under every line of `Text` on
a screen — title, description, button labels — visible in screenshots and in
a real Chrome tab hitting the dev server. Looks like a `TextDecoration`
config, but changing/removing every `TextStyle` on the screen (even swapping
to a bare hardcoded style with zero `decoration` property) does not remove
it.

**Root cause:** Flutter Web's accessibility layer mirrors visible text into
invisible `<flt-semantics>` / `<span>` DOM nodes (for screen readers). This
layer is **off by default** for a normal visitor — it only activates when
something explicitly requests it (a real screen reader, or a browser
automation tool building an accessibility tree to click things by name,
which is what a coding-agent's browser tool typically does). Confirmed via
DevTools: the semantics mirror `<span>` gets a CSS
`transform: scale(x, y)` that the Flutter Web engine computes wrong for a
screen that lacks a `Material` ancestor — verified `scale(_, 4)` on a
screen built as a raw `ColoredBox > Stack > Positioned` (no `Scaffold`), and
`scale(_, 1)` (normal) on the identical text inside a screen with a
`Scaffold`. The distorted invisible span leaks a visible sliver.

**Fix:** wrap the screen's root in
`Material(type: MaterialType.transparency, child: ...)` — see the
structural rule above. Confirmed clean afterward with the exact same layout.

**Why it's safe to ignore if you can't fix it immediately:** it never
reaches a real user. Semantics is off by default in production and for any
visitor without a screen reader; this is a debug/automation-only artifact.
Still — fix it, because the underlying "no Material ancestor" issue is a
real code smell independent of this specific bug.

**How this was diagnosed (reusable method for the next weird rendering
bug):** don't trust a first guess.
1. Searched the full DOM (`document.querySelectorAll`) for
   `text-decoration`/`border-bottom`/`background-image` — found nothing, so
   it wasn't CSS.
2. Grepped the whole codebase for `TextDecoration` — found nothing touching
   the screen.
3. Swapped the real style for a bare hardcoded `TextStyle` with zero
   `decoration` — bug persisted, so it wasn't the app's `TextStyle` at all.
4. Built a second, minimal isolated screen (`Scaffold` + title + description,
   nothing else) — clean. Then removed just the `Scaffold` from that same
   minimal screen, keeping everything else identical — bug reappeared.
   That isolated the real variable in one change.
5. Only then inspected the DOM node itself (not just computed CSS) and found
   the `transform: scale()` smoking gun.

The lesson: when a rendering bug doesn't match any code you can find, build
the smallest possible isolated repro and change **one variable at a time**
instead of re-theorizing about the same unverified guess.

## Dev workflow reminders

- Never iterate on a Flutter web screen by re-navigating the browser URL —
  it doesn't recompile Dart and can also fight the dev-route-resume memory
  in debug mode. Use `r`/`R` or MCP `reload_app`.
- `kasy run --web` uses `usePathUrlStrategy()` — routes are plain paths
  (`/dev/flow-preview/x`), **not** hash routes (`/#/dev/...`). A hash-only
  navigation is silently a no-op path-wise.
