mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-09-30 09:13:11 +00:00
115 lines
6.2 KiB
Markdown
115 lines
6.2 KiB
Markdown
# Frontend maintainability conventions
|
|
|
|
## Conditional rendering
|
|
|
|
Use logical `&&` or the `when` prop on `Text` when JSX has only a rendered state and an empty
|
|
state. Use a ternary only when both branches render meaningful alternatives.
|
|
|
|
```tsx
|
|
{
|
|
error && <Alert severity="error">{error.message}</Alert>;
|
|
}
|
|
|
|
<Text variant="description" when={Boolean(description)}>
|
|
{description}
|
|
</Text>;
|
|
```
|
|
|
|
ESLint rejects `condition ? <Element /> : null`. This keeps one-sided conditions visually
|
|
distinct from real either-or UI decisions.
|
|
|
|
The left operand of `&&` must be entirely boolean. `&&` renders its left operand when it is falsy
|
|
and non-boolean (notably `{count && <X />}` renders `0`), so a type-aware ESLint rule,
|
|
`seahaven/no-non-boolean-jsx-and`, is enforced at `error` across the repository. It asks the
|
|
TypeScript checker for the type of the left operand and reports unless every union constituent is
|
|
boolean-like, so `boolean | undefined` and `string | null` fail just as `number` does. The rule
|
|
fails closed: if type services are unavailable in a governed TSX file it reports rather than
|
|
silently claiming safety. The `when` prop on `Text` is typed `boolean`, so TypeScript enforces the
|
|
same constraint at that component boundary.
|
|
|
|
Approved guard forms (choose by semantics, not as a blind codemod):
|
|
|
|
- Presence-only values where falsy means "absent" — errors, optional strings shown only when set
|
|
(descriptions, notes, reasons), and optional objects (locations, detail records): coerce with
|
|
`Boolean(value)` (or `Boolean(a || b)` for a composite presence test) before `&&`.
|
|
`{Boolean(error) && <Alert />}` and `{Boolean(description) && <Text />}` are the canonical forms.
|
|
- Values where `0` or `""` is meaningful, or where a non-null value must flow into a typed prop or
|
|
helper inside the branch: use an explicit nullish/range comparison so the operand is boolean and
|
|
TypeScript can still narrow. `{count > 0 && ...}`, `{value != null && ...}`, and
|
|
`{isEdit && id != null && <Panel vendorId={id} />}` keep `0`/`""` semantics and preserve
|
|
narrowing.
|
|
- Element-slot props (`icon`, `action`, `actions`) are typed `ReactElement`, not `ReactNode`: the
|
|
slot holds one element (or fragment), and the render branch coerces with `Boolean(prop)`. Do not
|
|
widen these back to `ReactNode`, since a slot is never a meaningful `0`/`""`.
|
|
|
|
Never weaken, disable, baseline, or add per-line exceptions to the rule. A new one-sided condition
|
|
that needs a non-boolean operand must be rewritten into one of the approved forms above.
|
|
|
|
## Typography and feedback
|
|
|
|
Use `Text` from `@/components/ui/text` for headings, paragraphs, descriptions, labels, captions,
|
|
code, and asynchronous feedback. It owns:
|
|
|
|
- semantic HTML for each visual variant;
|
|
- the display, body, and monospace font families;
|
|
- default, muted, error, success, and warning tones;
|
|
- accessible `alert` and `status` live regions for error and feedback text;
|
|
- one-sided conditional text through `when`.
|
|
|
|
The variant contract is non-overridable: `component`, `role`, and `aria-live` are omitted from
|
|
`TextProps` (TypeScript blocks them) and the remaining props are spread before the variant-owned
|
|
attributes so the variant element, tone, and live-region role always win at runtime. Use `as` to
|
|
choose the rendered element and `tone` to choose the color; do not pass `component`, `role`, or
|
|
`aria-live` directly.
|
|
|
|
Live-region behavior:
|
|
|
|
- `feedback` (polite `status`) keeps the region mounted and toggles its text content via `when`, so
|
|
the polite region exists before its content changes and is announced reliably. Mounting an
|
|
already-populated status node on demand is not announceable on most screen-reader/browser pairs.
|
|
- `error` (assertive `alert`) mounts on demand (`when={false}` unmounts it). Alert-on-mount is the
|
|
expected error pattern, so the shipped `when={Boolean(error)}` usages are correct.
|
|
|
|
ESLint rejects raw paragraph and heading elements. Existing MUI `Typography` usages remain valid,
|
|
but new shared UI should prefer `Text` so semantics and design tokens do not drift.
|
|
|
|
The `vp-error` CSS token is presentational chrome for vendor-portal error cards and may only appear
|
|
on the `Text` component. ESLint flags any non-`Text` JSX element whose `className` is a static
|
|
string literal containing `vp-error` (e.g. `<div className="vp-error">`, `<section
|
|
className="vp-error extra">`); pair the error message with `variant="error"`. The rule enforces the
|
|
static surface only. It cannot resolve dynamic or composed class values
|
|
(`className={cn("vp-error", ...)}`, template literals with expressions, or expression-wrapped
|
|
strings), so do not compose `vp-error` dynamically to bypass it — prefer `Text variant="error"`.
|
|
|
|
## Forms and mutations
|
|
|
|
Use the libraries already established in the application:
|
|
|
|
- React Hook Form owns field registration, touched/dirty state, and client form lifecycle.
|
|
- Zod owns form validation and inferred form value types.
|
|
- TanStack Query owns server reads and mutations, including pending/error state, cache
|
|
invalidation, and retries where safe.
|
|
|
|
Do not add TanStack Form alongside React Hook Form. It would create two form conventions without
|
|
removing any current dependency. Reconsider only as a deliberate repository-wide migration with
|
|
benchmarks, a codemod plan, and an approved deprecation path.
|
|
|
|
File uploads are not ordinary form fields. Keep file selection and client validation in a focused
|
|
component, and use a TanStack Query mutation for upload progress, errors, completion refresh, and
|
|
retry state. Do not place upload orchestration in a route-sized page component.
|
|
|
|
## Page state
|
|
|
|
Pages should compose focused state components instead of accumulating unrelated booleans:
|
|
|
|
- query loading, error, and empty states stay adjacent to the query result;
|
|
- mutation pending/error state belongs to the component that initiated the mutation;
|
|
- route pages coordinate sections and navigation;
|
|
- reusable sections own their interaction details;
|
|
- errors render inline with accessible feedback, with toasts reserved for cross-page outcomes.
|
|
|
|
## Enforcement and rollout
|
|
|
|
The lint rules are repository-wide and the initial violations were migrated in the same change.
|
|
`npm run lint`, `npm run build`, and the `Text` behavior tests are required gates. Future
|
|
maintainability rules must also land with a green migration rather than a warning-only backlog.
|