Skip to content

DESIGN.md — FireRiskApp Design System

Design reference for the CHICHORRO fire risk assessment platform. Stack: React 18 + TypeScript + Vite + Tailwind CSS (with custom config). Last updated: 2026-05-12


1. Design Philosophy

FireRiskApp is an engineering analysis tool, not a consumer product. Every UI decision should serve the engineer running a fire-risk assessment.

Guiding principles

Clarity over decoration. Data is the product. Chrome, gradients, and animations exist only to aid comprehension — never as ornamentation. When in doubt, remove.

Hierarchy through typography, not color. Use size, weight, and spacing to establish importance. Reserve color for semantic communication (risk level, status, state).

Density with breathing room. Engineering workflows are data-dense. Layouts should pack meaningful information without feeling cramped. Consistent whitespace creates structure.

Predictability. Every button, card, and input behaves the same across all modules. Engineers build mental models fast — break the pattern only with intention.

Accessibility is not optional. Focus states, ARIA roles, and color-contrast ratios are requirements. Screen-reader users and keyboard navigators are first-class.

What this tool is NOT

  • Not a dashboard product (avoid KPI-card overload, chart-as-decoration)
  • Not a mobile-first consumer app (desktop-first, data-dense layouts are correct)
  • Not a startup landing page (no hero sections, gradients as decorative, big taglines)
  • Not a gaming or gamification product (no progress bars for engagement, no confetti)

2. Design Tokens

2.1 Color Palette

All colors are defined in tailwind.config.js under theme.extend.colors.

Brand (Fire Red)

Used for primary actions, active navigation, calculated result values, and brand identity.

TokenHexUsage
brand-50#fef2f2Lightest tint — result card backgrounds
brand-100#fee2e2Light backgrounds for brand-tinted panels
brand-500#ef4444Mid-range (rarely used directly)
brand-600#dc2626Focus ring accent
brand-700#b91c1cButton hover state
brand-800#991b1bPrimary brand color — active nav, primary buttons, result values
brand-900#7f1d1dAuth page background, darkest brand

brand-800 is the canonical brand color. Use it for CTAs and primary data values.

Ink (Neutral Slate)

All text, borders, backgrounds, and structural UI elements use the ink scale.

TokenHexUsage
ink-50#f8fafcApp shell background (bg-ink-50)
ink-100#f1f5f9Hover backgrounds, subtle fills
ink-200#e2e8f0Borders, dividers, card edges
ink-400#94a3b8Placeholder text, icons at rest
ink-500#64748bSecondary/muted text, overline labels
ink-600#475569Body text
ink-700#334155Medium-weight UI text, nav labels
ink-800#1e293bStrong body text
ink-900#0f172aPrimary text color — headings, labels
ink-950#020617Modal overlay (bg-ink-950/40)

Never use raw Tailwind gray-* or slate-* tokens in new code. Use ink-* exclusively.

Semantic Colors (Tailwind defaults — use as-is)

These communicate state and risk. Do not invent new colors for new states — map to this set.

SemanticToken familyUse case
Successemerald-*Acceptable risk, confirmed state, compliant data
Warningamber-*Stale data, mid-risk classification (B scale)
Cautionorange-*Elevated risk classification (C scale)
Errorred-*Calculation errors, not-acceptable risk, field validation
Criticalrose-*Highest risk classifications (E, F scale)
Extremefuchsia-*D classification on the RI scale
Infosky-*Secondary data panels, cost displays, VHE scenario tinting
Accent greenemerald-50/30 with emerald-300 borderVVE scenario group

Risk Scale Color Map

Used in RiPage.tsx and InterventionsPage.tsx. Always use this mapping — never ad-hoc colors.

RI ClassColor tokenRationale
A++, A+, Atext-emerald-700Low risk — safe green
B+, B, B-text-amber-600Medium risk — cautionary amber
C+, C, C-text-orange-600Elevated risk — warning orange
Dtext-fuchsia-700High risk — visual break from warm palette
Etext-rose-700Very high risk — strong red
Ftext-rose-800Critical — darkest red

2.2 Typography

Font: DM Sans (loaded via @fontsource/dm-sans or equivalent). Fallback: system-ui, sans-serif.

The typographic scale is fixed. Do not introduce new sizes outside this set.

Scale

RoleClassesSize / Weight
Page titletext-2xl font-bold text-ink-90024px / 700
Card / section headertext-lg font-semibold text-ink-90018px / 600
Form labeltext-sm font-medium text-ink-70014px / 500
Body texttext-sm text-ink-60014px / 400
Section overlinetext-xs font-semibold uppercase tracking-wide text-ink-50012px / 600 / uppercase
Nav group labeltext-[10px] font-semibold uppercase tracking-widest text-ink-40010px / 600 / uppercase
Buttontext-sm font-semibold14px / 600
Primary metric (large)text-4xl font-bold36px / 700
Primary metric (medium)text-3xl font-bold30px / 700
Secondary metrictext-2xl font-semibold24px / 600
Tertiary metrictext-lg font-semibold18px / 600
Code / monospacefont-mono text-sm text-ink-500 rounded bg-ink-100 px-114px / mono

Rules

  • Never set text-base (16px) in the UI — the baseline is text-sm (14px).
  • Never use raw font-bold on body paragraphs — only on metrics and headings.
  • Overlines (uppercase tracking-wide text-xs) are for section labels and result card labels only.
  • Use tabular-nums on any displayed numeric value that may change dynamically.
  • Line height: default Tailwind leading-5 (20px) for body, leading-6 (24px) for descriptive paragraphs.

2.3 Spacing

All spacing follows the Tailwind 4px base grid. The following values are canonical in this project.

Padding

ContextClasses
Card bodypx-6 py-5
Card headerpx-6 py-4
Form / result boxpx-4 py-3
Large result boxpx-5 py-4
Input fieldpx-3 py-2.5
Buttonpx-4 py-2.5
Nav linkpx-3 py-2
Sidebar brand areapx-4 py-5
Modalp-6

Gaps and stacking

ContextClasses
Page section spacingspace-y-6
Card inner spacingspace-y-4 or space-y-5
Form field grid gapgap-5
Result card grid gapgap-4
Intervention list gapgap-6
Button row gapgap-3
Inline icon + label gapgap-2

2.4 Border Radius

Two canonical radii. Do not introduce new values.

RadiusTokenUsage
12pxrounded-xlInputs, buttons, result boxes, small panels
16pxrounded-2xlCards, modals, the Card component

Sidebar nav links use rounded-lg (8px) — an intentional exception for tighter nav items.


2.5 Shadows

TokenDefinitionUsage
shadow-soft0 1px 2px rgba(15,23,42,.06), 0 8px 24px rgba(15,23,42,.06)All Card components
shadow-smTailwind defaultPrimary buttons, active nav items
shadow-xlTailwind defaultModals

Define shadow-soft in tailwind.config.js. Never replace it with Tailwind's built-in shadow-md or shadow-lg for cards — the custom token is intentionally less dramatic.


3. Layout System

3.1 Shell

text
┌──────────────────────────────────────────────────┐
│  Sidebar (fixed, w-56 = 224px, full height)      │
│  bg-white · border-r border-ink-200              │
│  ┌────────────────────────────────────────────┐  │
│  │  Main content area (ml-56, flex-1)         │  │
│  │  px-6 py-8                                 │  │
│  │  ┌──────────────────────────────────────┐  │  │
│  │  │  Content wrapper (mx-auto max-w-5xl) │  │  │
│  │  └──────────────────────────────────────┘  │  │
│  └────────────────────────────────────────────┘  │
└──────────────────────────────────────────────────┘
  • Shell background: bg-ink-50 (not white — provides depth for white cards)
  • Sidebar: fixed inset-y-0 left-0 z-10 w-56 — never change the fixed width
  • Content offset: ml-56 — matches sidebar width exactly
  • Content padding: px-6 py-8
  • Max content width: max-w-5xl (64rem / 1024px) — enforce on every page

3.2 Page Structure

Every module page follows this vertical structure:

text
Page heading row          (flex, space-between, col→row at lg:)
  ├── Title + description
  └── ModuleGlobalValueCard (max-w-[17rem], lg:ml-auto)

space-y-6

Card 1 (form inputs)
  ├── CardHeader
  └── CardBody
        └── grid gap-5 sm:grid-cols-2

Card 2 (results)
  ├── CardHeader
  └── CardBody
        └── grid gap-4 sm:grid-cols-2 lg:grid-cols-N

3.3 Responsive Grid

ContextMobileTablet (sm: 640px)Desktop (lg: 1024px)
Form fields1 col2 col2 col
Module status cards (RiPage)1 col2 col4 col
Main result cards1 col1 col3 col
Intervention panels1 col1 col2 col

Only two breakpoints are used: sm: and lg:. Do not add md: or xl: unless there is a specific, documented reason.

3.4 Auth Pages

Auth pages (Login, SignUp, ForgotPassword, ResetPassword) use a distinct layout:

  • Full-viewport gradient background: bg-gradient-to-br from-brand-900 via-brand-800 to-ink-900
  • Centered card: min-h-screen flex items-center justify-center px-4 py-12
  • Card max-width: max-w-lg w-full
  • Decorative radial overlay: [background-image:radial-gradient(circle_at_20%_20%,white_0,transparent_45%)]

This is the only place gradient backgrounds are used. All other pages use bg-ink-50.


4. Component Specifications

4.1 Button

File: src/components/ui/Button.tsx

Three variants only. Do not add variants without updating this spec.

VariantUse caseKey classes
primary (default)Main CTA — one per viewbg-brand-800 text-white hover:bg-brand-700 shadow-sm
secondaryCancel, secondary actionsbg-white border border-ink-200 text-ink-900 hover:bg-ink-50
ghostTertiary, inline, icon-adjacenttext-ink-700 hover:bg-ink-100

Base classes (all variants):

text
inline-flex items-center justify-center gap-2
rounded-xl px-4 py-2.5 text-sm font-semibold
transition focus:outline-none
focus-visible:ring-2 focus-visible:ring-offset-2
disabled:cursor-not-allowed disabled:opacity-50

Loading state:

  • Set disabled attribute
  • Change label text to "A processar…" (or context-specific equivalent)
  • Never use a spinner unless the wait exceeds ~3s

Icon buttons (square):

text
size-11 aspect-square rounded-xl border border-ink-200 bg-ink-50
text-ink-500 hover:bg-ink-100

4.2 Card

File: src/components/ui/Card.tsx

The fundamental layout unit for all content sections.

text
Card:       rounded-2xl border border-ink-200/80 bg-white shadow-soft
CardHeader: border-b border-ink-100 px-6 py-4  (flex items-center justify-between)
CardBody:   px-6 py-5
  • Title in CardHeader: text-lg font-semibold text-ink-900
  • Description in CardHeader: mt-1 text-sm text-ink-500
  • Right slot in CardHeader: for action buttons or toggle controls

Cards should not be nested. If a sub-panel is needed inside a card, use a Result Box (§4.5).

4.3 Form Fields

File: src/components/ui/Field.tsx

Label

tsx
<label className="block text-sm font-medium text-ink-700">

Always block (not inline). Always above the field, never beside it (except in narrow inline contexts).

Select

text
mt-1 w-full max-w-md rounded-xl border border-ink-200 bg-white
px-3 py-2.5 text-sm text-ink-900 shadow-sm outline-none
transition focus:border-brand-700 focus:ring-2 focus:ring-brand-700/20

Text Input (inline in pages, not via Field component)

text
mt-1 w-full max-w-md rounded-xl border border-ink-200 bg-white
px-3 py-2.5 text-sm text-ink-900 outline-none ring-brand-800/0
transition focus:border-brand-800 focus:ring-4 focus:ring-brand-800/15

Error / Missing Field State

Add ring-2 ring-red-400 to the input/select element when validation fails. Show an error message immediately below using the Error Alert (§4.8).

Disabled State

Add Tailwind disabled:opacity-50 disabled:cursor-not-allowed — never hide a disabled field.

4.4 Password Input

File: src/components/ui/PasswordInput.tsx

Extends Text Input with an absolute-positioned toggle button:

text
absolute inset-y-0 right-0 flex items-center px-3
text-ink-400 hover:text-ink-600

Toggle uses SVG inline (no icon library). tabIndex={-1} to keep tab order on the field. Use wherever passwords are entered: Login, SignUp, ResetPassword.

4.5 Result Box

An inline display panel for a single computed value. Used inside CardBody.

text
rounded-xl border px-4 py-3

Label: text-xs font-semibold uppercase tracking-wide [color]
Value: mt-1 tabular-nums text-2xl font-semibold [color]

Color variants:

StateBorderBackgroundLabel / Value color
Primary resultborder-brand-200bg-brand-50text-brand-700 / text-brand-800
Neutralborder-ink-200bg-ink-50text-ink-500 / text-ink-900
Successborder-emerald-200bg-emerald-50text-emerald-700 / text-emerald-800
Warningborder-amber-200bg-amber-50text-amber-700 / text-amber-800
Errorborder-red-200bg-red-50text-red-700 / text-red-800
Infoborder-sky-200bg-sky-50text-sky-700 / text-sky-900

Stale state: Value becomes color: rgba(100, 116, 139, 0.55) (inline style). This communicates the value is outdated without removing it. Show a "Valor desatualizado" overline label.

4.6 ModuleGlobalValueCard

File: src/components/ui/ModuleGlobalValueCard.tsx

Standalone result card displayed in the page header. Shows the current module's global computed value.

text
w-full max-w-[17rem] lg:ml-auto
rounded-2xl border border-ink-200 bg-white px-6 py-5 shadow-sm

Label: text-center text-xs font-semibold uppercase tracking-wide text-ink-500
Value: mt-2 text-center text-4xl font-bold tabular-nums
  - Fresh: text-brand-800
  - Stale: rgba(100, 116, 139, 0.55)

Subscribes to MODULE_RESULTS_UPDATED_EVENT. Shows "Valor desatualizado" when stale. One per module page, always in the page header row.

4.7 Collapsible Section

Used inside factor section pages (POI, DPI, ESCI). Groups related fields under a toggleable header.

Header:

text
flex items-center justify-between px-0 py-2 cursor-pointer select-none

Chevron icon:

text
transition-transform duration-300
rotate-180 (when collapsed) → no rotation (when expanded)

Collapse animation (CSS grid trick):

text
grid transition-[grid-template-rows] duration-300 ease-in-out
grid-rows-[0fr] (collapsed) → grid-rows-[1fr] (expanded)

State persisted in sessionStorage with key collapse:{formKey}.

4.8 Alert / Banner Messages

Used for inline status feedback within pages. Four semantic types:

TypeClasses
Errorrounded-xl border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-800
Warningrounded-xl border border-amber-200 bg-amber-50 px-3 py-2 text-sm text-amber-800
Successrounded-xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-800
Inforounded-xl border border-sky-200 bg-sky-50 px-4 py-3 text-sm text-sky-700

Always add role="alert" to error messages. Use role="status" for non-critical notices.

Auto-dismiss success banners: fade out after 3s using transition-opacity duration-500 opacity-0.

4.9 Modal / Dialog

Triggered from AppLayout.tsx. Three modals currently: session expired, clear session, logout confirm.

Overlay:

text
fixed inset-0 z-50 flex items-center justify-center bg-ink-950/40 px-4

Panel:

text
w-full max-w-lg rounded-2xl border border-ink-200 bg-white p-6 shadow-xl
role="dialog" aria-modal="true" aria-labelledby="modal-title"

Structure inside panel:

text
h2 (id="modal-title"): text-xl font-bold text-ink-900
p (description):        mt-3 text-sm leading-6 text-ink-600
action row:             mt-6 flex flex-wrap justify-end gap-3

Use z-[60] only for the session-expired modal (must always appear on top). All others use z-50.

4.10 Navigation Sidebar

File: src/components/AppLayout.tsx

text
fixed inset-y-0 left-0 z-10 flex w-56 flex-col border-r border-ink-200 bg-white

Sections (top to bottom):

  1. Brand area (border-b border-ink-200 px-4 py-5) — logo + app name
  2. Navigation (flex-1 px-3 py-4 space-y-3) — grouped nav links
  3. Session actions (px-3 pb-4) — buttons at bottom

Nav group label:

text
px-3 mb-1 text-[10px] font-semibold uppercase tracking-widest text-ink-400

Nav link states:

text
Active:   bg-brand-800 text-white shadow-sm rounded-lg px-3 py-2
Inactive: text-ink-700 hover:bg-ink-100 rounded-lg px-3 py-2

User avatar:

text
h-8 w-8 rounded-full bg-brand-800 text-sm font-bold text-white
flex items-center justify-center

Displays the first letter of the username.

4.11 Data Table (proposed — not yet implemented)

For the /admin/log and /admin/users pages. Define now so future implementation is consistent.

Container:

text
w-full overflow-x-auto rounded-2xl border border-ink-200 bg-white shadow-soft

Table:

text
w-full text-sm border-collapse

Header row:

text
border-b border-ink-200 bg-ink-50
th: px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-ink-500

Body rows:

text
tr: border-b border-ink-100 hover:bg-ink-50/50 transition
td: px-4 py-3 text-sm text-ink-700

Do not stripe rows — use the subtle hover state for scan-ability instead.

4.12 Badge / Tag (proposed — not yet implemented)

For displaying role labels, status chips, and event types in the access log.

Inline badge:

text
inline-flex items-center rounded-md px-2 py-0.5
text-xs font-semibold
VariantClasses
Neutralbg-ink-100 text-ink-700
Successbg-emerald-100 text-emerald-800
Warningbg-amber-100 text-amber-800
Errorbg-red-100 text-red-800
Brandbg-brand-100 text-brand-800

ID badge (circular, as in InterventionsPage):

text
flex h-5 w-6 shrink-0 items-center justify-center
rounded-full bg-ink-100 text-xs font-bold text-ink-700

5. State & Feedback Patterns

5.1 Loading State

  • Disable the submit button immediately on click.
  • Change button label to context-specific Portuguese text (e.g., "A calcular…", "A guardar…", "A iniciar sessão…").
  • Never show a spinner unless the operation takes more than ~3 seconds (backend constraint).
  • Do not gray out the entire form — only the button.

5.2 Stale Data

When inputs change after a calculation has been performed:

  • The old result remains visible but fades to rgba(100, 116, 139, 0.55).
  • A "Valor desatualizado" label appears in amber above or beside the result.
  • A red banner appears on RiPage when any module input changes.
  • The stale value is stored in module-results-stale in sessionStorage.
  • Cleared on successful recalculation.

Do not remove stale results entirely — engineers need to see the previous value to understand the magnitude of the change.

5.3 Validation Errors

  • Show ring-2 ring-red-400 on the specific field that failed.
  • Show an Error Alert (§4.8) immediately below the field or at the top of the section.
  • Use role="alert" so screen readers announce the error.
  • Keep the error visible until the field is corrected and re-submitted.

5.4 Success Confirmation

  • Use a Success Alert (§4.8) that auto-dismisses after 3 seconds.
  • Use transition-opacity duration-500 for the fade.
  • Do not navigate away automatically after success unless the action is destructive (e.g., account deletion).

5.5 Empty / No Results State

When a module has not been calculated yet:

  • The result area shows (em dash) in text-ink-300.
  • No error is shown — absence of a result is normal before first calculation.
  • The ModuleGlobalValueCard shows the empty state without a stale indicator.

6. Form Conventions

6.1 Field Grid Layout

All form sections use a 2-column responsive grid:

text
grid gap-5 sm:grid-cols-2

Fields that are logically wide (e.g., a text area, or a field with very long labels) may span both columns using sm:col-span-2.

Never use 3-column form grids.

6.2 Conditional Fields

Fields that depend on other field values use visibleWhen predicates in the definitions file. When a field is hidden:

  • It is removed from the DOM (not just opacity-0) — avoids stale values being submitted.
  • Its corresponding result is excluded from module average computation.

6.3 Field "Não se aplica" (Not Applicable)

0.00 is the canonical "not applicable" sentinel. Fields with value 0.00 are excluded from module averages. This is not an error state. Do not mark 0.00 fields as invalid.

6.4 Label Conventions (PT-PT)

  • All user-facing text, labels, and error messages: Portuguese (PT-PT).
  • Internal code identifiers and comments: English.
  • Portuguese: "Não se aplica", "Valor desatualizado", "A calcular…", "Índice de Risco".

7. Scenario Section Tinting

The CTI module uses background tints to visually separate input scenario groups. This is a targeted exception to the "no background color in forms" rule.

ScenarioBorderBackgroundHeading color
CI (Main compartment)border-ink-200bg-whitetext-ink-700
VHE (Horizontal evacuation)border-sky-300bg-sky-50/30text-sky-800
VVE (Vertical evacuation)border-emerald-300bg-emerald-50/30text-emerald-800

This pattern is only used in CtiPage. Do not apply scenario tinting to other modules.


8. Accessibility Standards

Required for every new component

  • Interactive elements must have visible focus styles: focus-visible:ring-2 focus-visible:ring-offset-2.
  • Error messages must use role="alert".
  • Modal dialogs must use role="dialog", aria-modal="true", aria-labelledby.
  • Icon-only buttons must have aria-label.
  • Form fields must have associated <label> elements (for attribute matching id).
  • Color is never the sole differentiator — pair color with text or icon.

Color contrast

  • All body text (text-ink-600 on bg-white) must meet WCAG AA (4.5:1 for small text).
  • brand-800 (#991b1b) on bg-white: confirmed to meet AA. Do not lighten brand colors below brand-700.

Keyboard navigation

  • Tab order must be logical (follows visual flow).
  • Modals must trap focus while open.
  • Collapsible sections: toggle button must be keyboard-activatable (it already is via <button>).

9. Anti-Patterns

The following are explicitly prohibited in this codebase:

Anti-patternReason
Raw gray-* / slate-* tokensUse ink-* instead — consistent palette
text-base (16px) body textBaseline is text-sm (14px)
Nested cardsBreaks visual hierarchy — use Result Boxes (§4.5) inside cards
Gradient backgrounds outside auth pagesDecorative; breaks data-density aesthetic
Inline style= for colors (except stale state opacity)Use Tailwind tokens
New Tailwind breakpoints (md:, xl:) without documented reasonOnly sm: and lg:
Four or more button variantsCreates visual noise
Animations longer than 300msSlows engineering workflows
Conditional display via opacity-0 (invisible but in DOM)Use conditional rendering — avoids submitting hidden stale values
text-center on form labelsLabels are always left-aligned

10. File Locations

PurposeFile
Tailwind config (tokens, font, shadow)app/frontend/tailwind.config.js
Button componentapp/frontend/src/components/ui/Button.tsx
Card componentapp/frontend/src/components/ui/Card.tsx
Label + Selectapp/frontend/src/components/ui/Field.tsx
Password inputapp/frontend/src/components/ui/PasswordInput.tsx
Module value cardapp/frontend/src/components/ui/ModuleGlobalValueCard.tsx
Shell layout + modalsapp/frontend/src/components/AppLayout.tsx
Risk scale color mapapp/frontend/src/pages/RiPage.tsxscaleClass()

11. Future Component Backlog

Components not yet built but planned:

ComponentPriorityNotes
Data TableHighNeeded for /admin/log and /admin/users
Badge / TagMediumRole labels, event types in access log
TooltipLowFor explaining field abbreviations (e.g., POI_CC)
Loading SkeletonLowFor async data loads > 1s
PaginationLowFor access log if records exceed 50

This document describes the current system and codifies conventions for future work. Update it whenever a new component is introduced or a pattern changes.