Design Guidelines

Product and frontend rules every Rome App should follow.

Agent-readable version: Markdown · llms.txt

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

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

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 Maps, 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

Any work that doesn't finish right away needs a clear status the UI can read back. Use a small fixed enum — for example pendingin_progresscompleted / 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

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

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

  • 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::

    <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 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 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

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

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

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.91.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

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.

On this page