feat(client): add accessible design system and API client foundation
Add design tokens, base components (page header, section, status chip, empty state, loading, announcer), a11y composables (text scaling, announcements), high-contrast theme and a typed HTTP client with ProblemDetails error mapping.
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
import { ApiError, type ProblemDetails } from './errors'
|
||||
|
||||
const baseUrl = import.meta.env.VITE_API_BASE_URL ?? ''
|
||||
|
||||
let tokenProvider: () => string | null = () => null
|
||||
|
||||
/** Wires the API client to the session store without creating a hard dependency. */
|
||||
export function setAuthTokenProvider(provider: () => string | null): void {
|
||||
tokenProvider = provider
|
||||
}
|
||||
|
||||
export interface RequestOptions {
|
||||
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'
|
||||
body?: unknown
|
||||
headers?: Record<string, string>
|
||||
signal?: AbortSignal
|
||||
/** Attach the bearer token when available (default: true). */
|
||||
auth?: boolean
|
||||
}
|
||||
|
||||
async function parseBody(response: Response): Promise<unknown> {
|
||||
if (response.status === 204 || response.headers.get('content-length') === '0') {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const contentType = response.headers.get('content-type') ?? ''
|
||||
|
||||
if (!contentType.includes('application/json')) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
try {
|
||||
return await response.json()
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimal typed fetch wrapper: JSON in/out, bearer auth, RFC 7807 error mapping.
|
||||
* Throws `ApiError` on non-2xx responses.
|
||||
*/
|
||||
export async function request<T>(path: string, options: RequestOptions = {}): Promise<T> {
|
||||
const { method = 'GET', body, headers = {}, signal, auth = true } = options
|
||||
|
||||
const requestHeaders: Record<string, string> = { Accept: 'application/json', ...headers }
|
||||
const token = auth ? tokenProvider() : null
|
||||
|
||||
if (token) {
|
||||
requestHeaders.Authorization = `Bearer ${token}`
|
||||
}
|
||||
|
||||
if (body !== undefined) {
|
||||
requestHeaders['Content-Type'] = 'application/json'
|
||||
}
|
||||
|
||||
const response = await fetch(`${baseUrl}${path}`, {
|
||||
method,
|
||||
headers: requestHeaders,
|
||||
body: body === undefined ? undefined : JSON.stringify(body),
|
||||
signal,
|
||||
})
|
||||
|
||||
const payload = await parseBody(response)
|
||||
|
||||
if (!response.ok) {
|
||||
const problem = (payload as ProblemDetails | undefined) ?? null
|
||||
throw new ApiError(response.status, problem)
|
||||
}
|
||||
|
||||
return payload as T
|
||||
}
|
||||
Reference in New Issue
Block a user