Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -360,8 +360,38 @@ lands, reviewers should flag any new `style={{ ... }}` prop.

---

## Code Style

### Frontend Styling

Frontend styling conventions are documented in
[`frontend/docs/styling.md`](frontend/docs/styling.md). Please read that
document before making UI changes.

**No inline styles.** Do not use the `style` prop on JSX elements. All
styling must go through the project's approved mechanisms (e.g. Tailwind
utility classes, CSS Modules, or the shared design-system components).
Inline styles bypass theming, break static analysis, and make visual
regression testing unreliable. If you believe an inline style is
unavoidable, raise it in the PR description and get maintainer sign-off
before merging.

---

## Security

Please **do not** report security vulnerabilities through public GitHub issues,
discussions, or pull requests.

Instead, report them privately via **GitHub Security Advisories** for this
repository:

- https://github.com/solutions-plug/predictIQ/security/advisories/new

This is the verified, monitored channel for security reports. See
[`SECURITY.md`](SECURITY.md) for the full disclosure policy and response
timelines.

If you discover a security vulnerability, please **do not** open a public issue.
Instead, report it privately to the maintainers so it can be triaged and fixed
before disclosure.
11 changes: 11 additions & 0 deletions frontend/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
module.exports = {
extends: ['next/core-web-vitals'],
rules: {
'react/forbid-dom-props': [
'error',
{
forbid: ['style'],
},
],
},
};
8 changes: 7 additions & 1 deletion frontend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

This package contains the Handsoff web frontend.

## Documentation

- [Styling conventions](../docs/styling.md) — CSP-safe styling rules and conventions for frontend contributors.
- [Internationalization (i18n) guide](./I18N_GUIDE.md) — how to add and manage translations in the frontend.
- [`docs/`](./docs/) — additional frontend documentation.

## Styling conventions (CSP-safe, no inline styles)

### Why: the CSP constraint
Expand Down Expand Up @@ -55,4 +61,4 @@ a review blocker.

- [Internationalization (i18n) guide](./I18N_GUIDE.md) — how to add and manage
translations in the frontend.
- [`docs/`](./docs/) — additional frontend documentation.
- [`docs/`](./docs/) — additional frontend documentation.
158 changes: 116 additions & 42 deletions frontend/docs/styling.md
Original file line number Diff line number Diff line change
@@ -1,60 +1,134 @@
# Frontend Styling Conventions
# Styling Guide

## The rule: no inline `style` props
## CSP Constraint: Inline `style` Props Are Not Allowed

Do **not** use the `style` prop on React components or DOM elements for anything
that affects rendering:
This project runs under a strict **Content Security Policy (CSP)** in production. The policy does **not** include `'unsafe-inline'` for styles, which means:

```jsx
// ❌ Don't do this
<div style={{ marginTop: 8, color: 'red' }}>…</div>
- Inline `style` attributes on DOM elements are **stripped or blocked** by the browser.
- React's `style={{ ... }}` prop compiles down to an inline `style` attribute, so it is affected by the same restriction.
- The failure is **silent**: no exception is thrown, no console error is guaranteed, and the element simply renders without the intended styles. This makes it easy to ship a broken UI without noticing during development (where CSP is often relaxed).

Because of this, **do not use the `style` prop** on DOM elements or on components that forward it to the DOM. Use CSS classes or CSS modules instead.

## Required Pattern: CSS Classes / CSS Modules

The migration established the following pattern for all styling:

1. **Static styles** live in a stylesheet (global CSS or a CSS module).
2. **Dynamic styles** are expressed by toggling **class names**, not by computing inline style objects.
3. Components receive class names via the `className` prop.

### Incorrect

```tsx
// ❌ Inline style prop — stripped/blocked by CSP in production.
function ProgressBar({ percent }: { percent: number }) {
return (
<div
className="progress"
style={{ width: `${percent}%` }} // silently ignored under CSP
/>
);
}
```

```tsx
// ❌ Passing a style object through to a DOM element.
function Badge({ color, children }: { color: string; children: React.ReactNode }) {
return <span style={{ color }}>{children}</span>;
}
```

Instead, express styling through CSS classes (or CSS modules) and apply them via
`className`:
### Correct

Use a CSS module (or global class) and toggle classes:

```css
/* ProgressBar.module.css */
.progress {
height: 4px;
background: var(--color-accent);
}

```jsx
// ✅ Do this
<div className="stack-sm text-danger">…</div>
.width0 { width: 0%; }
.width25 { width: 25%; }
.width50 { width: 50%; }
.width75 { width: 75%; }
.width100 { width: 100%; }
```

## Why: Content Security Policy strips inline styles
```tsx
// ✅ Class-based styling — CSP-safe.
import styles from "./ProgressBar.module.css";

const WIDTH_CLASS: Record<number, string> = {
0: styles.width0,
25: styles.width25,
50: styles.width50,
75: styles.width75,
100: styles.width100,
};

function ProgressBar({ percent }: { percent: number }) {
const widthClass = WIDTH_CLASS[percent] ?? styles.width0;
return <div className={`${styles.progress} ${widthClass}`} />;
}
```

```tsx
// ✅ Variant classes instead of inline color.
import styles from "./Badge.module.css";

function Badge({ variant, children }: { variant: "info" | "warn"; children: React.ReactNode }) {
return <span className={`${styles.badge} ${styles[variant]}`}>{children}</span>;
}
```

For values that are genuinely dynamic and cannot be enumerated as classes, prefer:

Our production Content Security Policy does not allow inline styles. When the
browser enforces that policy, inline `style` attributes are **silently dropped** —
the element still renders, but without the styling. This is not a build error and
not a test failure; it only shows up in production, which is exactly why it went
unnoticed for so long.
- A CSS custom property set via a class or a small set of classes, or
- A dedicated stylesheet rule keyed off a `data-*` attribute, or
- A CSS-in-JS solution that emits a real stylesheet (not inline attributes).

This was a real production bug. Two commits fixed it:
If you believe you have a case that truly requires an inline style, raise it for review before adding one — it will not work in production.

## Enforcement Gap

There is currently **no lint rule** that catches inline `style` props. This is why the CSP violations were able to land in the first place. To prevent regressions, add the following ESLint rules:

```jsonc
// .eslintrc (excerpt)
{
"rules": {
"react/forbid-dom-props": [
"error",
{ "forbid": [{ "propName": "style", "message": "Inline style props are blocked by CSP. Use a CSS class or CSS module instead." }] }
],
"react/forbid-component-props": [
"error",
{ "forbid": [{ "propName": "style", "message": "Inline style props are blocked by CSP. Use a CSS class or CSS module instead." }] }
]
}
}
```

- `5bd5e51` — moved AppShell chrome off inline styles (CSP was dropping them)
- `e80a15b` — migrated every remaining inline `style` prop to CSS classes
`react/forbid-dom-props` covers intrinsic elements (`<div style={...}>`), while `react/forbid-component-props` covers custom components that may forward `style` to the DOM. Both are needed to close the gap.

Because the failure mode is silent and environment-specific, the constraint is
easy to reintroduce by accident. That is why it is documented here: so a future
PR does not ship the same bug again.
Until such a rule is enabled, this convention is enforced by review. Please keep it in mind when reviewing frontend changes.

## The required pattern
## Historical Context

- Put styling in CSS (global stylesheets or CSS modules) and reference it with
`className`.
- For dynamic values that genuinely must vary at runtime, prefer toggling a
class (e.g. `className={isActive ? 'is-active' : ''}`) or setting a CSS custom
property through a class rather than writing an inline `style` object.
- If you believe you have a case that truly requires an inline style, raise it in
the PR description first — do not add one silently.
This constraint was discovered the hard way. Two commits fixed the fallout from inline styles that were silently dropped under CSP:

## Enforcement
- **`5bd5e51`** — initial fix replacing inline `style` props with CSS classes (moved AppShell chrome off inline styles).
- **`e80a15b`** — follow-up cleanup covering remaining components and edge cases (migrated every remaining inline `style` prop to CSS classes).

There is currently **no** lint rule or CI check that blocks inline `style` props,
so nothing mechanically prevents a regression. Adding one is recommended:
These commits are the reference for the pattern described above. When in doubt, look at how those changes were structured.

- ESLint's [`react/forbid-dom-props`](https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/forbid-dom-props.md)
can be configured with `forbid: ['style']` to flag inline `style` props on DOM
elements.
- For components that forward a `style` prop, `react/forbid-component-props`
covers the same case.
## Summary

Until such a rule is enabled, this convention is enforced by review. Please keep
it in mind when reviewing frontend changes.
- **Never** use the `style` prop on DOM elements or components that forward it to the DOM.
- Use **CSS classes / CSS modules** and toggle `className` for dynamic styling.
- The failure mode is **silent** — it will not show up in dev if CSP is relaxed.
- Add `react/forbid-dom-props` and `react/forbid-component-props` to enforce this automatically.
- Until enforcement is enabled, this convention is enforced by review.
3 changes: 2 additions & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
"eslint": "^9.20.0",
"eslint-config-next": "16.3.3",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-react": "^7.37.4",
"jest": "^30.4.2",
"jest-axe": "^10.0.0",
"jest-environment-jsdom": "^30.5.0",
Expand All @@ -72,4 +73,4 @@
"node": ">=18.0.0",
"npm": ">=9.0.0"
}
}
}
Loading