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

76 lines
3.3 KiB
Markdown

# TypeScript Style
## Related guides
| Topic | Guide |
|-------|--------|
| GraphQL data (the default for app data) | [`contrib/claude/relay.md`](relay.md) |
| Forms and file inputs | [`contrib/claude/forms.md`](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.
```typescript
// 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`](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`](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.
```ts
// 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.
```ts
// 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,
});
```