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>
76 lines
3.3 KiB
Markdown
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,
|
|
});
|
|
```
|