# Design Guidelines
URL: /docs/building-apps/design-guidelines
Markdown: /docs/building-apps/design-guidelines.md
Description: Product and frontend rules every Rome App should follow.

This page gathers the product and UI rules every Rome App should follow. The
product section sets how an app stores and shows its data; the frontend section
sets how its web UI behaves at different screen sizes; the listing section
covers the two things a user sees before they install — the README and the icon.

These rules come from the app-creation guide and reference docs. Where a rule
clashes with a real product need, the product wins — but keep the exception in
one component and write down why.

## Product design [#product-design]

### Stateful apps are the default shape [#stateful-apps-are-the-default-shape]

Apps that keep state are the recommended shape for Rome apps. Saving useful
information makes the product better and the experience smoother than building
around a one-shot action.

A quick test: if the guardian closes the app, forgets the conversation, and
opens it tomorrow, the main UI should still show useful state.

Product checklist:

* [ ] First page shows useful saved state and the main idea or actions.
* [ ] Completed and failed states are visible after reload or restart.

### Store the record first [#store-the-record-first]

The reply an action sends back is not storage. Save the user-facing record
before anything else, then render from it.

* Save the user-facing record first.
* Give long-running work a visible status such as `pending`, `in_progress`,
  `completed`, or `failed`.
* Write down important steps somewhere you can query, so the UI can explain what
  happened later.
* Don't use in-memory `Map`s, logs, chat history, or big JSON blobs as the
  source of truth.

Saved data belongs in the app's database layer — Drizzle tables in the shared
system SQLite, reached through repositories. The record, not the chat transcript
or a component's React state, is the source of truth.

### Visible status for long-running work [#visible-status-for-long-running-work]

Any work that doesn't finish right away needs a clear status the UI can read
back. Use a small fixed enum — for example `pending` → `in_progress` →
`completed` / `failed` — and update the saved record as the work moves along.
Because the status lives in the record, the guardian can leave and come back to
see where things stand.

### The job-creation pattern for UI-triggered work [#the-job-creation-pattern-for-ui-triggered-work]

When a web UI button starts long-running work — an agent run, an import, a scan,
a batch API call, building a report — treat the click as **creating a job**, not
as a request the browser owns. The frontend may be closed, refreshed, or
disconnected while the work is still running.

Use a backend pattern that lasts:

1. **Save a record first** with a status such as `pending` or `in_progress`, the
   input arguments, timestamps, and enough context to resume or explain the run.
2. **Reply to the browser quickly** with the job/run id, then run the long task
   from server-side app code, an action, or a one-off routine. Don't make the
   work depend on the React component, the `fetch` request, or an open dashboard
   tab staying alive.
3. **Update the saved record** as the task runs and when it finishes or fails.
   The UI renders from that record, with a refresh or status view, so the
   guardian can come back later and see what happened.

For work that should run later or on a schedule rather than right now, register
it with the `system:create_routine` action instead of holding it in the request. A
scheduled run has no memory of the setup run, so the action it calls must stand
on its own.

## Frontend and mobile [#frontend-and-mobile]

Rome apps render inside the dashboard shell on both desktop and mobile — people
use the dashboard on phones all the time. The frontend **must** work on narrow
screens. Design for mobile first, then add more for larger screens.

### Mobile-first responsive layout [#mobile-first-responsive-layout]

* Use Tailwind's responsive prefixes (`sm:` 640px, `md:` 768px, `lg:` 1024px).
  Default styles are for mobile; add `md:` / `lg:` overrides for wider screens —
  not the other way around.

* Prefer single-column flows on narrow screens; switch to multi-column grids
  only at `md:` or `lg:`:

  ```tsx
  <div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
    {items.map((it) => <Card key={it.id}>...</Card>)}
  </div>
  ```

* Don't use fixed widths (`w-[480px]`) on top-level containers. Use
  `w-full max-w-screen-md mx-auto` so the layout adapts.

* Tables spill over easily — wrap them in `<div className="overflow-x-auto">…</div>`
  or fall back to a stacked card list on narrow screens.

* Never rely on hover-only controls. Every action you can reach by hover must
  also be reachable by tap (a visible button, a long-press menu, etc.).

### Touch targets and typography [#touch-targets-and-typography]

Touch targets and spacing:

* Interactive elements need a tap area of at least **44×44 px**. shadcn defaults
  are close, but check on dense lists.
* When space is tight, add tap padding rather than shrinking the font —
  `px-3 py-2` at least on small buttons.
* Keep at least `gap-2` (8 px) between tappable elements next to each other so
  users don't tap the wrong one.

Typography:

* Body text: `text-base` (16 px). Anything smaller is hard to read and sets off
  iOS Safari's auto-zoom when an `<input>` is focused. If you must shrink it, do
  so only at `md:` and up.
* Headings should scale: `text-xl md:text-2xl lg:text-3xl`.
* Use `leading-relaxed` for paragraphs on mobile; the default line height feels
  cramped at 16 px.

### Forms and safe areas [#forms-and-safe-areas]

Forms and input:

* Make inputs `w-full` on mobile; hold them inside a `max-w-md` container rather
  than fixing the input width.
* Set `inputMode` and `autoComplete` correctly so mobile keyboards pick the right
  layout (`inputMode="numeric"`, `type="email"`, `autoComplete="one-time-code"`,
  …).
* Stack buttons vertically on narrow screens:
  `flex flex-col gap-2 sm:flex-row sm:gap-3`.

Safe area and viewport:

* The shell already sets the right `<meta name="viewport">`. Don't override it.
* For edge-to-edge UIs, leave room for the iOS notch with the
  `pb-[env(safe-area-inset-bottom)]` pattern on bottom-fixed bars.

### Narrow-viewport validation [#narrow-viewport-validation]

Before you call a UI change done, test at **375 px wide** (iPhone SE class) in
Chrome DevTools' device emulator and check that:

* You can reach all main actions without scrolling sideways.
* No text is cut off, and no buttons sit right against the screen edges.
* You can focus and type in form fields without the screen zooming.
* Long content scrolls naturally (no nested scroll traps).

## Listing and icon [#listing-and-icon]

### README as App Store listing copy [#readme-as-app-store-listing-copy]

Every app ships a top-level `README.md` that becomes its **App Store listing** —
what someone sees in the dashboard or the Rome App Store before deciding to
install. The scaffold creates a placeholder; replace it before you call the app
done. The packed artifact picks up the top-level `README.md` on its own (it does
**not** go under `dist/` or `src/`); just edit the file the scaffold left at
`<app-root>/README.md`.

Write it as product copy, not technical documentation:

* Lead with what the app *does* for the user, in one or two sentences.
* List the main features as short bullets, from the user's point of view.
* Mention the situations where someone would use this app.
* **Don't** include schemas, file paths, build commands, action/API names, or
  implementation details. Those belong in source comments or commit messages,
  not in the listing.
* Keep it short — a few headings is enough.

### Icon design [#icon-design]

Every app must ship a clean, modern SVG icon at
`<app-root>/src/assets/icon.svg`, named in `app.yaml` as `icon: assets/icon.svg`.
Always design a custom icon that fits what the user wants and what the app
actually does — don't reuse a generic placeholder, and don't skip this step.

Requirements:

* Draw the icon by hand in SVG (or tweak one until it fits). The mark should
  show what the app *does* at a glance — pick an image tied to the app's purpose
  (e.g. chevrons for a code editor, an inbox tray for messaging, a calendar grid
  for scheduling).
* Use `viewBox="0 0 24 24"` and ship a **complete tile**: a rounded
  `<rect width="24" height="24" rx="5.5">` as the background, then the glyph drawn
  on top. The app picks both the tile color and the glyph color — the host renders
  the SVG as-is and adds no tint.
* Use a steady `stroke-width` (usually `0.9`–`1.2`) with
  `stroke-linejoin="round"` and `stroke-linecap="round"`. Keep the glyph centered
  with a comfortable margin from the tile edge — aim for the weight and clarity
  of `lucide-react` icons.
* Use explicit hex colors (`fill="#…"`, `stroke="#…"`). Pair a soft tile
  background with a darker glyph that stands out, so the mark reads at 24px in
  both light and dark themes.
* Keep the file small (well under 2 KB) and ship a single root `<svg>` element.

Before you call the app done, open the icon in the dashboard sidebar and check
that it reads clearly at small sizes in both light and dark themes.

### A note on names and emoji [#a-note-on-names-and-emoji]

The app's display name (`app.yaml` `name`, `web.displayName`, `web.navLabel`) and
the page title are Title Case words — "Morning Brief", "Browser Automation" — not
the lowercase-hyphenated `appId`. The `appId` is the machine id and stays
lowercase-hyphenated. Don't use emoji in titles or nav labels; a custom SVG icon
is how an app gets its glyph.