Files
probo/contrib/claude/ts-style.md
Émile Ré 393c538de1 Fill frontend rule gaps and broaden v2 tokens
Add the frontend guides the v2 UI kit and compliance-portal need but
that the first rework left uncovered: forms, routing, client state, and
permission-gated UI.

forms.md documents a tiered approach on Base UI Field/Form -- native
constraints, then a validate function, then zod parsed in onSubmit, and
react-hook-form only for large or dynamic forms -- and drops the custom
useFormWithSchema wrapper. routing.md covers @probo/routes, navigation,
typed params, URL-as-state, redirects, auth/protected routes, and the
folded-in no-outlet-context rule. state-management.md gives a decision
order across Relay, URL, local state, context, and zustand.
permissions.md gates UI on the canUpdate/canDelete permission(action:)
fields without re-encoding authorization in the client.

Rename v2-colors.md to v2-tokens.md and add the typography, radius,
shadow, and native-spacing scales alongside color. Extend ui.md with
user feedback, empty-state, and accessibility sections; standardize
toasts on Base UI's Toast (Toast.useToastManager) and retire the legacy
useToast across ui.md, forms.md, error-handling.md, and relay.md. Add an
Intl formatting section to i18n.md and a non-Relay HTTP / file
upload-download section to ts-style.md. Update the AGENTS.md index and
the v2-color-scale cursor rule for the new and renamed guides.

Signed-off-by: Émile Ré <emile@probo.com>
2026-06-26 18:52:05 +02:00

3.3 KiB

TypeScript Style

Topic Guide
GraphQL data (the default for app data) contrib/claude/relay.md
Forms and file inputs contrib/claude/forms.md

URL and query parameter construction

Never build URLs with template literals, string concatenation, or string formatting. Always use the URL and URLSearchParams APIs.

  • Use new URL() to construct or parse full URLs.
  • Use URLSearchParams to build query strings.
  • Use .pathname, .searchParams, and other URL properties to modify parts of a URL safely.
  • Use encodeURIComponent for dynamic path segments.
// Bad — template literal
const endpoint = `https://api.example.com/users/${userId}?active=${active}`;

// Bad — string concatenation
const endpoint = "https://api.example.com/orgs/" + orgId + "/members";

// Bad — query params via string concat
const qs = "?domain=" + domain + "&limit=100";

// Good — URL object
const url = new URL("https://api.example.com");
url.pathname = `/users/${encodeURIComponent(userId)}`;
url.searchParams.set("active", String(active));

// Good — URLSearchParams for query strings
const params = new URLSearchParams();
params.set("domain", domain);
params.set("limit", "100");
const qs = params.toString();

Non-Relay HTTP

Application data is GraphQL via Relay (see contrib/claude/relay.md) — that is the default and covers almost everything. Reach for fetch only for the cases GraphQL does not handle: binary uploads/downloads, REST endpoints exposed by the backend, and health/probe calls.

Rules:

  • Build the endpoint with URL (see above). Derive the host/path from app config (e.g. import.meta.env.VITE_API_URL, a pathPrefix helper), never a hardcoded string.
  • Use fetch directly; do not add an HTTP client dependency for a handful of calls.
  • Always check response.ok and throw a typed error on failure so it can be caught (try/catch in the handler — see contrib/claude/error-handling.md) and surfaced via a toast.
  • Send credentials/auth the same way the Relay environment does (e.g. credentials: "include"); do not invent a second auth scheme.
// Good — URL-built endpoint, ok check, typed failure
const url = new URL(buildEndpoint());
url.pathname = `/api/console/v1/documents/${encodeURIComponent(documentId)}/download`;

const response = await fetch(url, { credentials: "include" });
if (!response.ok) {
  throw new Error(`Download failed: ${response.status}`);
}
const blob = await response.blob();

File upload / download

  • Uploads: collect the file with react-dropzone (already a @probo/ui dependency) or a native <input type="file">, then send a FormData body via fetch (or the GraphQL upload mechanism if the schema exposes one). Validate type/size client-side with the @probo/helpers fileAccept helpers before sending; the server still validates.
  • Downloads: prefer a direct link to a backend URL when the endpoint streams a file; use fetch + blob only when you must read the bytes (e.g. to rename or post-process). Revoke any URL.createObjectURL you create.
// Bad — template-literal URL, no ok check, ad-hoc auth header
const res = await fetch(`${base}/upload?id=${id}`, {
  headers: { Authorization: "Bearer " + token },
  body: file,
});