---
name: development-guidelines
description: Reference guide for development best practices established from recurring bug patterns. Use this skill whenever a developer is building forms, multi-locale features, preview screens, popups, overlays, auth flows, environment-specific logic, reusing existing logic for new roles or entities, managing packages, async action buttons, listing screens with pagination/sort/filter, file uploads, email fields, tab-based screens, modals, toast notifications, or any screen with clickable elements. Also use when a developer asks about best practices, coding standards, or how to handle a specific implementation scenario covered by these guidelines.
---

# Development Guidelines

Established from recurring bug pattern analysis. These guidelines must be applied proactively during development — not reactively after bugs are found.

---

## 1. Multi-Locale Number Formatting

When building for multiple locales, identify how each locale formats numbers — including digit grouping separators, decimal separators, and symbol placement — and ensure input fields and display values for **currency, percentages, and quantities** handle locale-specific formatting correctly rather than defaulting to a single format.

> Example: Polish locale uses a space as digit grouping separator and a comma as decimal separator. `1,000.00` (English) renders as `1 000,00` (Polish).

---

## 2. Preview Variant for Interactive Components

Any component that can appear in a preview screen — such as **dropdowns, text inputs, date pickers, selects, and toggles** — should implement a `preview` variant that:

- Preserves the full visual appearance (shape, icon, styling)
- Strips all interaction at the component level — no event handlers, no focus, no open state

Consumer screens pass `variant="preview"` rather than manually suppressing behaviour per screen.

---

## 3. Popup and Overlay Positioning

Never calculate popup or overlay positions manually using screen or pixel dimensions.

Always establish a CSS positioning context:
- `position: relative` on the parent trigger container
- `position: absolute` on the popup/overlay child

Let CSS handle placement. Manual pixel calculations are fragile and break under browser zoom, display scaling, and dynamic layout changes such as nav drawer open/close.

Popups and overlays must also remain correctly anchored when the page is scrolled. Based on requirements, implement one of two behaviours:
- The popup moves with the trigger by using a correct CSS positioning context
- The popup closes on scroll via a scroll event listener

Never leave scroll behaviour undefined — an unanchored popup that drifts on scroll is always a bug.

---

## 4. Shared Validation Message Constants

Validation error messages must **never** be defined inline within individual screens or components.

All validation messages must be:
- Defined as shared constants or i18n keys
- Referenced from a central file

This ensures consistency across screens and guarantees that translations are managed in one place, not duplicated per page. This applies to all reused validation rules — zip code, email, phone number, required field errors, and so on.

---

## 5. Field State Specification Per Screen in Tickets

Every ticket involving a form must explicitly define the state of each field for that specific screen. For every field, three things must be stated:

1. **Visible or hidden** — does the field appear on this screen?
2. **Editable or read-only** — can the user interact with it?
3. **Mandatory or optional** — is it required to submit?

The same field can behave differently across screens. This must be a conscious, documented decision per screen — not assumed to be consistent. If a ticket does not specify field states, raise and clarify before implementation.

---

## 6. Auth Provider Input Constraints

When building any input field that feeds into a third-party auth provider (e.g. **Auth0, Cognito**), look up and apply the provider's input constraints at the frontend level before implementation. This includes:

- Maximum field lengths
- Allowed characters
- Format rules for email, password, username, and similar fields

Frontend validation must mirror these constraints so violations are caught early with specific, user-friendly messages — never a generic error surfaced from a failed auth request.

---

## 7. Environment-Specific Behaviour

When implementing any feature that behaves differently across environments — such as OTP delivery, bypass logic, feature flags, or mock services — the expected behaviour must be **explicitly defined in the requirements before implementation begins**.

The specification must cover:
- Which environments send real communication vs bypass
- Whether the behaviour applies to all users or a specific subset
- What the expected state is in each environment (dev, QA, pre-prod, prod)

If env-specific behaviour is not specified in the ticket, raise and clarify it before writing the logic. Do not assume or infer.

---

## 8. Reusing Logic Across Roles or Entities

When reusing any logic — queries, lookups, services, or data-access layers — for a new role, entity, or context, explicitly audit every table reference, data source, and query within that logic and verify each one is correct for the new context.

Do not assume reused logic is context-neutral. Even minor references left pointing to the original entity can cause silent failures that are difficult to diagnose.

> Example: Auth lookup logic built for admin (querying the admin table) must have all table references updated when reused for technician login.

---

## 9. Package Version Pinning

Pin all dependency versions to exact values in `package.json` — avoid range specifiers (`^`, `~`) for packages that are frequently updated or have a history of breaking changes.

- Exact pinning ensures every environment — dev, QA, pre-prod, prod — installs and runs the same version of every dependency
- Package upgrades must be deliberate and tested decisions, not automatic resolutions at install time
- Commit the lock file (`package-lock.json` or `yarn.lock`) to version control to further guarantee consistent installs across environments

---

## 10. Button State During Async Actions

Any button that triggers an async operation — form submission, API call, file upload, or any action with a processing delay — must be **disabled immediately on click** and remain disabled until the operation completes or fails.

- Prevents duplicate submissions and repeated API calls
- The button should reflect its in-progress state visually (e.g. loading indicator, disabled styling) so the user knows the action is being processed
- Re-enable the button once the operation resolves — whether success or failure

---

## 11. URL State Persistence for Listing Screens

On any listing screen, all user-driven state must be synchronised to the URL as query parameters. This applies to:

- **Search queries** — e.g. `?search=value`
- **Sort parameters** — e.g. `?sort=asc` or `?sort=desc`
- **Active filters** — e.g. `?filter-type=active`
- **Pagination** — e.g. `?page=2&page-size=10`

URL parameters must be:
- Written to the URL immediately when the user applies a state change
- Read on page load to restore the previous state
- Named in kebab-case (e.g. `page-size`, not `pageSize`)

This ensures state persists across page refreshes, supports browser back/forward navigation, and allows users to share or bookmark specific filtered, sorted, or paginated views.

---

## 12. Filter and Pagination Interaction

Whenever a filter, search query, or any state change that affects the result set is applied, always reset the page index to page 1.

Filter and pagination are interdependent — the current page index is only valid for the current result set. Changing the filter produces a new result set, and the previous page index must not be carried over. This applies to all listing screens that combine filtering and pagination.

---

## 13. Request Cancellation and Sequencing

When a feature can trigger multiple API calls in rapid succession, always implement request cancellation or response sequencing to ensure only the latest request's response is applied to the UI.

- Use an `AbortController` to cancel in-flight requests when a new one is triggered, or track a request sequence counter and discard any response that does not match the latest sequence
- Never assume responses will arrive in the order they were sent

---

## 14. Toast and Notification Deduplication

The toast/notification system must deduplicate messages — if the same toast is already visible or was recently shown, a subsequent trigger of the same message must not stack or spawn a new instance.

- Implement deduplication by assigning a unique ID per toast type and checking for an existing active toast with the same ID before spawning a new one
- This is distinct from button state management — retries and event-driven triggers can legitimately fire multiple times, so the toast layer itself must be resilient to repeated calls

---

## 15. Modal Focus Management

When implementing any modal or dialog, follow these three rules for focus management:

- **On open** — move focus into the modal, to the first focusable element or the modal's heading
- **While open** — trap focus inside the modal so tabbing does not reach elements behind it
- **On close** — return focus to the element that triggered the modal to open

This is a baseline accessibility requirement, not an optional enhancement.

---

## 16. Timezone Handling for Multi-Region Applications

Never store or rely on the browser's local timezone for date and time values. Always follow the UTC + known timezone pattern:

| Date type | On persist | On display |
|---|---|---|
| System-generated (created at, updated at) | Store as UTC | Convert from UTC to the business-defined timezone for display |
| User-input (schedules, appointments) | Convert to UTC before storing; store the source timezone explicitly alongside the UTC value | Convert from UTC back to the required timezone using the stored timezone reference |

Key rules:
- Never use the browser's local timezone as the source of truth
- For user-input dates where the intended local time matters, always store the timezone explicitly alongside the UTC value — do not infer it
- The business-defined timezone must be a project-level configuration, not hardcoded per component

---

## 17. Access Control Must Be Enforced at Both Frontend and Backend

Never rely solely on hiding or disabling UI elements to restrict access. Any action that is restricted for a role must be guarded at both layers:

- **Frontend** — hide or disable the UI element so the restricted action is not presented to the user
- **Backend** — guard the corresponding route and API endpoint so that even a direct URL or API call is rejected for unauthorised roles

Frontend access control is a UX convenience, not a security measure. The backend is the only reliable enforcement point.

---

## 18. Debounce on Search and Autocomplete Inputs

Always apply a debounce to any input that triggers an API call on change — including search fields, autocomplete inputs, and live-filter inputs.

- The API call must only fire after the user has paused typing, not on every keystroke
- A debounce delay of **300ms** is the standard default for all search inputs
- Debounce reduces unnecessary calls but does not eliminate overlapping responses — combine with request cancellation (see guideline 13) for full coverage

---

## 19. Fixed-Precision Arithmetic for Display and Calculations

Never use raw floating-point arithmetic directly for display or calculation of precision-sensitive values.

- **Display** — always format numeric values to the required number of decimal places as specified in requirements before rendering to the user
- **Calculations** — use a fixed-precision or decimal library (e.g. `decimal.js`, `big.js`) for any arithmetic involving currency, tax, discounts, or other precision-sensitive values
- **Requirements** — the required decimal precision must be explicitly defined per field in the ticket; if not specified, raise and clarify before implementation

---

## 20. Pagination Pattern and Digit-Length Handling

**1. Use the right pagination pattern for the context**

| Pattern | Use for | Includes |
|---|---|---|
| Stepper pagination | Sequential fixed-length flows — onboarding, wizards, multi-step forms, carousels | Step indicator only — no page-size control, no total count |
| Data grid / numbered pagination | Variable-size datasets — tables, lists, search results | Page-size selector, range/total count, prev/next/first/last, numbered range with ellipsis truncation |

Do not use stepper pagination for tables or lists. If it is reused there, all digit-length and ellipsis rules below are mandatory.

**2. Handle all digit lengths when using numbered pagination**

- Use flexible sizing — `min-width` in `ch` units or auto-width with padding — so 1, 2, 3, and 4+ digit page numbers all get correct space automatically. Never use fixed `px` widths for page number slots
- Recalculate and reflow container width whenever the digit count of visible page numbers changes — not just once at initial render
- Use flex `gap` between all pagination items rather than `margin`
- Where possible, reserve space for the maximum expected digit length upfront rather than expanding reactively

**3. Ellipsis truncation logic must be consistent**

- Ellipsis (`...`) must only appear when 2 or more consecutive pages are hidden — never for a gap of exactly 1
- A single hidden page between two shown pages must render as that page number, not disappear. `1 2 4` is a bug — the correct rendering is `1 2 3 4` or `1 2 … 4`

---

## 21. Data Refetch on Tab Focus

Any screen that displays data fetched from an API must refetch that data when the browser tab regains focus.

- Listen for the `visibilitychange` or `window focus` event and trigger a refetch when the tab becomes active
- This ensures that users working across multiple browser tabs always see up-to-date data, regardless of what changes were made in other tabs during the inactive period

---

## 22. Route-Scoped State for Side Nav Screens

All screen-level state must be scoped to its own route and never shared across routes.

- When navigating via the side nav, the incoming screen must always initialise with a clean state and trigger its own API call
- Never rely on or inherit state from the previously active route
- This applies even when two screens display visually similar data (e.g. Admin listing and Technician listing) — they are distinct contexts and must be treated as such

---

## 23. Toast Dismissal Behaviour

All toast and notification messages must support the following dismissal triggers:

- **Explicit dismiss** — a close/dismiss button must always be present on the toast
- **Scroll dismiss** — when the user scrolls the page, any active toast must automatically dismiss
- **Auto-dismiss** — toasts should auto-dismiss after a reasonable timeout (typically 3–5 seconds) unless the message requires explicit user acknowledgement (e.g. a critical error)

Toasts must never block content or remain on screen indefinitely without a clear dismissal path.

---

## 24. File Upload Content Validation

Never rely solely on file extension for upload validation. Always validate the actual file content using:

| Method | How it works | Where to apply |
|---|---|---|
| Magic bytes / file signature check | Read the first few bytes of the file and compare against known file type signatures (e.g. JPEG starts with `FF D8 FF`, PNG with `89 50 4E 47`) | Frontend (first line of defence) and backend (enforcement) |
| MIME type validation | Check the file's actual MIME type, not just the declared type from the browser | Backend |

- Frontend magic bytes check provides immediate user feedback before upload
- Backend validation is mandatory — frontend checks alone are not sufficient
- Both the extension and the actual file content must match the allowed types before the upload is accepted

---

## 25. Pointer Cursor on Clickable Elements

Every element that responds to a click must display `cursor: pointer` on hover. This includes:

- Buttons — including icon-only buttons and custom styled buttons
- Links and anchor elements
- Clickable table rows, list items, and cards
- Custom interactive components — dropdowns, toggles, checkboxes, radio buttons, tabs
- Any `div`, `span`, or non-semantic element with an `onClick` handler

Disabled elements must display `cursor: not-allowed`. Never leave a clickable element with the default text or arrow cursor.

---

## 26. Email Case Insensitivity

Email addresses must be treated as case-insensitive throughout the system. `user@example.com` and `User@example.com` are the same address.

The specific implementation approach depends on the project's auth and storage setup — any of the following are acceptable and must be applied consistently end-to-end:

- **Auth provider config** — configure the auth provider (e.g. Cognito, Auth0) to treat emails as case-insensitive at the identity level
- **Lowercase on input** — restrict the email input field to lowercase only, or automatically convert to lowercase before submission
- **Lowercase on storage** — convert to lowercase before storing and display the lowercase version consistently

The chosen approach must be defined in requirements before implementation and applied at every point the email is handled — input, storage, lookup, and display.

---

## 27. Frontend Validation as First Line of Defence

All forms must implement frontend validation as the first line of defence. This does not replace backend validation — the backend must always validate as the final enforcement point — but the frontend must catch obvious invalid input early. Frontend validation must cover at minimum:

- **Required fields** — show an inline error immediately when a required field is left empty on submit or on blur
- **Character limits** — enforce maximum (and minimum where applicable) length on input fields
- **Format validation** — validate expected formats (e.g. phone number digits only, postal code pattern) before submission
- **Type validation** — ensure numeric fields only accept numbers, date fields only accept valid dates

Validation errors must be specific and shown inline next to the relevant field — never rely on a generic submission error from the backend for something that could have been caught on the frontend.

---

## 28. Custom 404 and Invalid URL Handling

Every application must implement a custom 404 / not-found screen that is shown whenever a user navigates to an invalid or unrecognised route. This screen must:

- Be designed and built as a proper in-app screen — maintaining the application's header, navigation, and branding
- Clearly communicate that the page was not found, in a friendly and non-technical way
- Provide a clear path back — at minimum a link or button to return to the home or dashboard screen
- Be registered as a catch-all route in the router so it handles all unmatched paths

A custom 404 screen must be raised as a design requirement at the start of any new application — it must be implemented before the application goes to production.

---

## 29. Tab State Isolation

All segmented tabs must follow these rules regardless of tab type (form, list, or data):

| State type | On tab switch |
|---|---|
| Unsaved input (typed text, unsubmitted form fields, unapplied filters) | Always clear — unsaved state must never persist across a tab switch |
| Saved/applied state (applied filters, submitted values, confirmed selections) | Retain per tab — each tab remembers its own saved state independently |
| Another tab's state | Never bleed — tab A's state must never appear in tab B under any circumstance |

- Each tab must own its state independently — never share state between tabs
- Each tab must trigger its own API call when activated, not reuse another tab's response

---

## 30. Modal Outside-Click Behaviour

When implementing any modal, explicitly define and implement its outside-click behaviour based on its type:

- **Simple modals** (no data entry — e.g. confirmation, info, preview) — outside click closes immediately. No confirmation needed.
- **Data-entry modals** (form, multi-step, any user input) — outside click must trigger a confirmation prompt warning the user that unsaved data will be lost. The modal must not close until the user explicitly confirms. This applies even if the form is only partially filled.

The modal type and its outside-click behaviour must be specified in the ticket before implementation. Do not apply a single behaviour uniformly across all modals.
