tempest-react-sdk 0.50.0 → 0.52.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/dist/audio/sfx-pool.cjs +1 -1
  2. package/dist/audio/sfx-pool.cjs.map +1 -1
  3. package/dist/audio/sfx-pool.js +36 -36
  4. package/dist/audio/sfx-pool.js.map +1 -1
  5. package/dist/auth/refresh-queue.cjs +1 -1
  6. package/dist/auth/refresh-queue.cjs.map +1 -1
  7. package/dist/auth/refresh-queue.js +6 -6
  8. package/dist/auth/refresh-queue.js.map +1 -1
  9. package/dist/components/BarList/BarList.cjs +1 -1
  10. package/dist/components/BarList/BarList.cjs.map +1 -1
  11. package/dist/components/BarList/BarList.js +6 -1
  12. package/dist/components/BarList/BarList.js.map +1 -1
  13. package/dist/components/BarList/bar-list-model.cjs +1 -1
  14. package/dist/components/BarList/bar-list-model.cjs.map +1 -1
  15. package/dist/components/BarList/bar-list-model.js +8 -8
  16. package/dist/components/BarList/bar-list-model.js.map +1 -1
  17. package/dist/components/Markdown/markdown-parse.cjs +5 -5
  18. package/dist/components/Markdown/markdown-parse.cjs.map +1 -1
  19. package/dist/components/Markdown/markdown-parse.js +66 -63
  20. package/dist/components/Markdown/markdown-parse.js.map +1 -1
  21. package/dist/components/Scheduler/Scheduler.cjs +1 -1
  22. package/dist/components/Scheduler/Scheduler.cjs.map +1 -1
  23. package/dist/components/Scheduler/Scheduler.js +1 -1
  24. package/dist/components/Scheduler/Scheduler.js.map +1 -1
  25. package/dist/components/Scheduler/Scheduler.module.cjs.map +1 -1
  26. package/dist/components/Scheduler/Scheduler.module.js.map +1 -1
  27. package/dist/components/Sidebar/Sidebar.cjs +1 -1
  28. package/dist/components/Sidebar/Sidebar.cjs.map +1 -1
  29. package/dist/components/Sidebar/Sidebar.js +25 -45
  30. package/dist/components/Sidebar/Sidebar.js.map +1 -1
  31. package/dist/components/Sidebar/Sidebar.module.cjs.map +1 -1
  32. package/dist/components/Sidebar/Sidebar.module.js.map +1 -1
  33. package/dist/components/Sidebar/SidebarEntry.cjs +2 -0
  34. package/dist/components/Sidebar/SidebarEntry.cjs.map +1 -0
  35. package/dist/components/Sidebar/SidebarEntry.js +40 -0
  36. package/dist/components/Sidebar/SidebarEntry.js.map +1 -0
  37. package/dist/http/api-client.cjs +1 -1
  38. package/dist/http/api-client.cjs.map +1 -1
  39. package/dist/http/api-client.js +84 -71
  40. package/dist/http/api-client.js.map +1 -1
  41. package/dist/http/describe-api-error.cjs +1 -1
  42. package/dist/http/describe-api-error.cjs.map +1 -1
  43. package/dist/http/describe-api-error.js +4 -2
  44. package/dist/http/describe-api-error.js.map +1 -1
  45. package/dist/http/timeout.cjs +2 -0
  46. package/dist/http/timeout.cjs.map +1 -0
  47. package/dist/http/timeout.js +22 -0
  48. package/dist/http/timeout.js.map +1 -0
  49. package/dist/http/use-describe-api-error.cjs +1 -1
  50. package/dist/http/use-describe-api-error.cjs.map +1 -1
  51. package/dist/http/use-describe-api-error.js +3 -2
  52. package/dist/http/use-describe-api-error.js.map +1 -1
  53. package/dist/icons/material-symbols.cjs.map +1 -1
  54. package/dist/icons/material-symbols.js.map +1 -1
  55. package/dist/offline/create-offline-database.cjs +2 -0
  56. package/dist/offline/create-offline-database.cjs.map +1 -0
  57. package/dist/offline/create-offline-database.js +29 -0
  58. package/dist/offline/create-offline-database.js.map +1 -0
  59. package/dist/offline/create-offline-store.cjs +1 -1
  60. package/dist/offline/create-offline-store.cjs.map +1 -1
  61. package/dist/offline/create-offline-store.js +29 -25
  62. package/dist/offline/create-offline-store.js.map +1 -1
  63. package/dist/styles.css +1 -1
  64. package/dist/tempest-react-sdk.cjs +1 -1
  65. package/dist/tempest-react-sdk.d.ts +286 -13
  66. package/dist/tempest-react-sdk.js +43 -42
  67. package/dist/utils/csv.cjs.map +1 -1
  68. package/dist/utils/csv.js.map +1 -1
  69. package/dist/utils/format.cjs +1 -1
  70. package/dist/utils/format.cjs.map +1 -1
  71. package/dist/utils/format.js +8 -3
  72. package/dist/utils/format.js.map +1 -1
  73. package/dist/vite/tempest-pwa-dev-sw.cjs +1 -1
  74. package/dist/vite/tempest-pwa-dev-sw.cjs.map +1 -1
  75. package/dist/vite/tempest-pwa-dev-sw.js +49 -41
  76. package/dist/vite/tempest-pwa-dev-sw.js.map +1 -1
  77. package/package.json +1 -1
@@ -2,25 +2,27 @@ import { randomId as e } from "../utils/ids.js";
2
2
  import { buildApiUrl as t } from "./build-url.js";
3
3
  import { TempestApiError as n, buildApiError as r, isRetriableStatus as i } from "./errors.js";
4
4
  import { retry as a } from "./retry.js";
5
+ import { withTimeout as o } from "./timeout.js";
5
6
  //#region src/http/api-client.ts
6
- var o = /* @__PURE__ */ new Set([
7
+ var s = /* @__PURE__ */ new Set([
7
8
  "GET",
8
9
  "HEAD",
9
10
  "OPTIONS"
10
11
  ]);
11
- function s(e, t) {
12
- return !o.has(t) || !(e instanceof n) ? !1 : i(e.status);
12
+ function c(e, t) {
13
+ return !s.has(t) || !(e instanceof n) ? !1 : i(e.status);
13
14
  }
14
- function c(e) {
15
+ function l(e) {
15
16
  return e ? e === !0 ? {} : e : null;
16
17
  }
17
- function l(e) {
18
+ var u = 15e3, d = 3e5;
19
+ function f(e) {
18
20
  return typeof FormData < "u" && e instanceof FormData;
19
21
  }
20
- function u(e) {
22
+ function p(e) {
21
23
  return Math.round(performance.now() - e);
22
24
  }
23
- async function d(e, t) {
25
+ async function m(e, t) {
24
26
  let i;
25
27
  try {
26
28
  i = await e.clone().json();
@@ -33,122 +35,133 @@ async function d(e, t) {
33
35
  }
34
36
  return new n(r(e.status, i, e.headers, t));
35
37
  }
36
- function f(n) {
37
- let r = n.fetcher ?? globalThis.fetch.bind(globalThis);
38
- function i() {
39
- let e = n.getToken?.();
38
+ function h(r) {
39
+ let i = r.fetcher ?? globalThis.fetch.bind(globalThis);
40
+ function s() {
41
+ let e = r.getToken?.();
40
42
  return e ? { Authorization: `Bearer ${e}` } : {};
41
43
  }
42
- async function o(e, a, o) {
43
- let { body: s, params: c, headers: u, ...d } = a, f = l(s), p = {
44
- ...f ? {} : { "Content-Type": "application/json" },
45
- ...o ? { "X-Request-ID": o } : {},
46
- ...n.headers,
47
- ...i(),
48
- ...u
49
- }, m = {
50
- ...d,
51
- headers: p,
52
- credentials: n.withCredentials ? "include" : d.credentials,
53
- body: s == null ? void 0 : f ? s : JSON.stringify(s)
54
- };
55
- return r(t(n.baseURL, e, {
56
- prefix: n.prefix,
57
- params: c
58
- }), m);
44
+ async function h(e, a, c) {
45
+ let { body: l, params: p, headers: m, signal: h, timeout: g, ..._ } = a, v = f(l), y = v ? r.uploadTimeout : r.timeout, b = g === void 0 ? y === void 0 ? v ? d : u : y : g, x = {
46
+ ...v ? {} : { "Content-Type": "application/json" },
47
+ ...c ? { "X-Request-ID": c } : {},
48
+ ...r.headers,
49
+ ...s(),
50
+ ...m
51
+ }, S = o(h, b), C = {
52
+ ..._,
53
+ signal: S.signal,
54
+ headers: x,
55
+ credentials: r.withCredentials ? "include" : _.credentials,
56
+ body: l == null ? void 0 : v ? l : JSON.stringify(l)
57
+ }, w = t(r.baseURL, e, {
58
+ prefix: r.prefix,
59
+ params: p
60
+ });
61
+ try {
62
+ return await i(w, C);
63
+ } catch (e) {
64
+ throw S.timedOut() ? new n({
65
+ status: 0,
66
+ detail: `A requisição excedeu ${b}ms e foi abandonada.`
67
+ }) : e;
68
+ } finally {
69
+ S.dispose();
70
+ }
59
71
  }
60
- async function f(e, t, r, i) {
61
- let a = n.logger;
62
- if (!a) return o(e, t, r);
63
- let s = performance.now();
72
+ async function g(e, t, n, i) {
73
+ let a = r.logger;
74
+ if (!a) return h(e, t, n);
75
+ let o = performance.now();
64
76
  try {
65
- let n = await o(e, t, r), c = {
66
- requestId: r,
67
- status: n.status,
68
- ms: u(s)
69
- }, l = `${i} ${e} → ${n.status}`;
70
- return n.status >= 400 ? a.warn(l, c) : a.debug(l, c), n;
77
+ let r = await h(e, t, n), s = {
78
+ requestId: n,
79
+ status: r.status,
80
+ ms: p(o)
81
+ }, c = `${i} ${e} → ${r.status}`;
82
+ return r.status >= 400 ? a.warn(c, s) : a.debug(c, s), r;
71
83
  } catch (t) {
72
84
  throw a.warn(`${i} ${e} → no response`, {
73
- requestId: r,
74
- ms: u(s),
85
+ requestId: n,
86
+ ms: p(o),
75
87
  error: t
76
88
  }), t;
77
89
  }
78
90
  }
79
- async function p(e, t) {
80
- if (n.onUnauthorized) {
81
- n.logger?.warn("unauthorized — calling onUnauthorized", {
91
+ async function _(e, t) {
92
+ if (r.onUnauthorized) {
93
+ r.logger?.warn("unauthorized — calling onUnauthorized", {
82
94
  requestId: t,
83
95
  status: e.status
84
96
  });
85
97
  try {
86
- await n.onUnauthorized(e);
87
- } catch (r) {
88
- n.logger?.warn("onUnauthorized threw — keeping the original response error", {
98
+ await r.onUnauthorized(e);
99
+ } catch (n) {
100
+ r.logger?.warn("onUnauthorized threw — keeping the original response error", {
89
101
  requestId: t,
90
102
  status: e.status,
91
- error: r
103
+ error: n
92
104
  });
93
105
  }
94
106
  }
95
107
  }
96
- async function m(t, r) {
97
- let i = n.requestId ? n.requestId() : e(), a = (r.method ?? "GET").toUpperCase(), o = await f(t, r, i, a);
108
+ async function v(t, n) {
109
+ let i = r.requestId ? r.requestId() : e(), a = (n.method ?? "GET").toUpperCase(), o = await g(t, n, i, a);
98
110
  if (o.status === 401) {
99
- if (n.refresh) {
111
+ if (r.refresh) {
100
112
  try {
101
- await n.refresh(), o = await f(t, r, i, a);
113
+ await r.refresh(), o = await g(t, n, i, a);
102
114
  } catch {
103
- throw await p(o, i), await d(o, i);
115
+ throw await _(o, i), await m(o, i);
104
116
  }
105
- o.status === 401 && await p(o, i);
106
- } else await p(o, i);
117
+ o.status === 401 && await _(o, i);
118
+ } else await _(o, i);
107
119
  }
108
- if (!o.ok) throw await d(o, i);
120
+ if (!o.ok) throw await m(o, i);
109
121
  if (o.status !== 204) return (o.headers.get("content-type") ?? "").includes("application/json") ? await o.json() : await o.text();
110
122
  }
111
- async function h(e, t = {}) {
112
- let r = c(n.retry);
113
- if (!r) return m(e, t);
123
+ async function y(e, t = {}) {
124
+ let n = l(r.retry);
125
+ if (!n) return v(e, t);
114
126
  let i = (t.method ?? "GET").toUpperCase();
115
- return a(() => m(e, t), {
116
- ...r,
117
- shouldRetry: r.shouldRetry ?? ((e) => s(e, i))
127
+ return a(() => v(e, t), {
128
+ ...n,
129
+ shouldRetry: n.shouldRetry ?? ((e) => c(e, i))
118
130
  });
119
131
  }
120
- async function g(e, t, n = "POST") {
121
- return h(e, {
132
+ async function b(e, t, n = "POST", r) {
133
+ return y(e, {
134
+ ...r,
122
135
  method: n,
123
136
  body: t
124
137
  });
125
138
  }
126
139
  return {
127
- request: h,
128
- get: (e, t) => h(e, {
140
+ request: y,
141
+ get: (e, t) => y(e, {
129
142
  ...t,
130
143
  method: "GET"
131
144
  }),
132
- post: (e, t) => h(e, {
145
+ post: (e, t) => y(e, {
133
146
  ...t,
134
147
  method: "POST"
135
148
  }),
136
- put: (e, t) => h(e, {
149
+ put: (e, t) => y(e, {
137
150
  ...t,
138
151
  method: "PUT"
139
152
  }),
140
- patch: (e, t) => h(e, {
153
+ patch: (e, t) => y(e, {
141
154
  ...t,
142
155
  method: "PATCH"
143
156
  }),
144
- delete: (e, t) => h(e, {
157
+ delete: (e, t) => y(e, {
145
158
  ...t,
146
159
  method: "DELETE"
147
160
  }),
148
- upload: g
161
+ upload: b
149
162
  };
150
163
  }
151
164
  //#endregion
152
- export { f as createApiClient };
165
+ export { h as createApiClient };
153
166
 
154
167
  //# sourceMappingURL=api-client.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"api-client.js","names":[],"sources":["../../src/http/api-client.ts"],"sourcesContent":["/**\n * @tempest-limits function-lines — createApiClient is over the limit and every line\n * is a request-lifecycle concern the client cannot delegate: base URL joining, the\n * auth header, the 401 refresh-and-replay, the opt-in retry wrapper and the response\n * parsing that turns a failure into a typed error.\n */\nimport { randomId } from \"../utils\";\nimport { buildApiUrl } from \"./build-url\";\nimport { buildApiError, TempestApiError, isRetriableStatus } from \"./errors\";\nimport { retry as retryWithBackoff } from \"./retry\";\nimport type { RetryOptions } from \"./retry\";\nimport type { ApiClient, ApiClientConfig, RequestOptions } from \"./types\";\n\n/**\n * Methods the built-in retry policy will replay.\n *\n * `PUT` and `DELETE` are idempotent on paper but stay out: a backend that logs,\n * bills, or fires a webhook per call still sees two, so replaying them is a\n * decision the caller makes through `shouldRetry`, not a default.\n */\nconst IDEMPOTENT_METHODS: ReadonlySet<string> = new Set([\"GET\", \"HEAD\", \"OPTIONS\"]);\n\n/**\n * Sub-500 statuses worth a second attempt: a network failure (status `0`), a\n * request timeout, a too-early replay, and a rate limit — which usually carries\n * the `Retry-After` the backoff already honours.\n */\n\n/**\n * The built-in retry policy, used when `retry` is `true` or is options carrying\n * no `shouldRetry` of their own.\n *\n * Conservative on purpose. Replaying a write can duplicate it, and replaying a\n * `400` or a `403` cannot fix a bad payload or a permission the caller does not\n * have — it only spends the user's time before showing the same error.\n *\n * @param error - Whatever the attempt threw.\n * @param method - The upper-cased HTTP method of the request.\n * @returns Whether the client should try again.\n */\nfunction isRetriableFailure(error: unknown, method: string): boolean {\n if (!IDEMPOTENT_METHODS.has(method)) return false;\n if (!(error instanceof TempestApiError)) return false;\n return isRetriableStatus(error.status);\n}\n\n/**\n * Normalize the `retry` config into options, or `null` when retrying is off.\n *\n * @param config - The `retry` field as the caller wrote it.\n * @returns Retry options to use, or `null` to run a single attempt.\n */\nfunction resolveRetry(config: boolean | RetryOptions | undefined): RetryOptions | null {\n if (!config) return null;\n return config === true ? {} : config;\n}\n\nfunction isFormData(body: unknown): body is FormData {\n return typeof FormData !== \"undefined\" && body instanceof FormData;\n}\n\n/**\n * Milliseconds since a `performance.now()` reading, rounded.\n *\n * `performance.now()` rather than `Date.now()` because this measures a\n * duration: the wall clock can step sideways mid-request (an NTP correction, a\n * VM resuming, the user changing the clock) and turn a 40 ms call into a\n * negative number. The monotonic clock cannot.\n *\n * @param startedAt - The reading taken before the work started.\n * @returns Whole milliseconds elapsed.\n */\nfunction elapsedMs(startedAt: number): number {\n return Math.round(performance.now() - startedAt);\n}\n\nasync function parseError(response: Response, sentRequestId?: string): Promise<TempestApiError> {\n let body: unknown;\n try {\n body = await response.clone().json();\n } catch {\n try {\n body = await response.text();\n } catch {\n body = null;\n }\n }\n return new TempestApiError(\n buildApiError(response.status, body, response.headers, sentRequestId),\n );\n}\n\n/**\n * Create a typed HTTP client backed by `fetch`.\n *\n * Handles JSON serialization, query params, bearer auth via `getToken`, uploads\n * via `FormData`, and throws a typed `ApiError` on any non-2xx response.\n *\n * **Expired sessions.** A `401` with `refresh` configured awaits the refresh and\n * replays the request once. `onUnauthorized` fires whenever that path ends\n * unauthorized anyway — the refresh threw, or the replay came back `401` — which\n * is the signal to clear the session. Without `refresh`, the first `401` calls\n * it directly.\n *\n * **Retries** are off unless you set `retry`. See {@link ApiClientConfig.retry}\n * for the built-in policy; it never replays a write.\n *\n * **Logging** is off unless you pass a `logger`. With one, every finished attempt\n * writes a line — `debug` under 400, `warn` from 400 up, plus a `warn` when\n * `onUnauthorized` fires — carrying `requestId`, `status` and elapsed `ms`, and\n * never a body, header or query string. The level and the destination belong to\n * the logger, not to a boolean here.\n *\n * @example\n * const api = createApiClient({\n * baseURL: import.meta.env.VITE_API_URL,\n * getToken: () => useAuthStore.getState().token,\n * refresh,\n * onUnauthorized: () => useAuthStore.getState().logout(),\n * logger: createLogger({ level: import.meta.env.DEV ? \"debug\" : \"warn\" }).child(\"http\"),\n * retry: true,\n * });\n *\n * @param config - Base URL plus the optional auth, retry and fetch hooks.\n * @returns A client with `request`/`get`/`post`/`put`/`patch`/`delete`/`upload`.\n */\nexport function createApiClient(config: ApiClientConfig): ApiClient {\n const fetcher = config.fetcher ?? globalThis.fetch.bind(globalThis);\n\n function authHeaders(): Record<string, string> {\n const token = config.getToken?.();\n return token ? { Authorization: `Bearer ${token}` } : {};\n }\n\n async function rawRequest(\n path: string,\n options: RequestOptions,\n requestId?: string,\n ): Promise<Response> {\n const { body, params, headers, ...rest } = options;\n const isForm = isFormData(body);\n\n const finalHeaders: Record<string, string> = {\n ...(isForm ? {} : { \"Content-Type\": \"application/json\" }),\n ...(requestId ? { \"X-Request-ID\": requestId } : {}),\n ...config.headers,\n ...authHeaders(),\n ...(headers as Record<string, string> | undefined),\n };\n\n const init: RequestInit = {\n ...rest,\n headers: finalHeaders,\n credentials: config.withCredentials ? \"include\" : rest.credentials,\n body:\n body === undefined || body === null\n ? undefined\n : isForm\n ? (body as FormData)\n : JSON.stringify(body),\n };\n\n return fetcher(buildApiUrl(config.baseURL, path, { prefix: config.prefix, params }), init);\n }\n\n async function send(\n path: string,\n options: RequestOptions,\n requestId: string,\n method: string,\n ): Promise<Response> {\n const log = config.logger;\n if (!log) return rawRequest(path, options, requestId);\n\n const startedAt = performance.now();\n try {\n const response = await rawRequest(path, options, requestId);\n const entry = { requestId, status: response.status, ms: elapsedMs(startedAt) };\n const line = `${method} ${path} → ${response.status}`;\n if (response.status >= 400) log.warn(line, entry);\n else log.debug(line, entry);\n return response;\n } catch (error) {\n log.warn(`${method} ${path} → no response`, {\n requestId,\n ms: elapsedMs(startedAt),\n error,\n });\n throw error;\n }\n }\n\n async function notifyUnauthorized(response: Response, requestId: string): Promise<void> {\n if (!config.onUnauthorized) return;\n config.logger?.warn(`unauthorized — calling onUnauthorized`, {\n requestId,\n status: response.status,\n });\n try {\n await config.onUnauthorized(response);\n } catch (error) {\n config.logger?.warn(`onUnauthorized threw — keeping the original response error`, {\n requestId,\n status: response.status,\n error,\n });\n }\n }\n\n async function attempt<T>(path: string, options: RequestOptions): Promise<T> {\n const requestId = config.requestId ? config.requestId() : randomId();\n const method = (options.method ?? \"GET\").toUpperCase();\n let response = await send(path, options, requestId, method);\n\n if (response.status === 401) {\n if (config.refresh) {\n try {\n await config.refresh();\n response = await send(path, options, requestId, method);\n } catch {\n await notifyUnauthorized(response, requestId);\n throw await parseError(response, requestId);\n }\n if (response.status === 401) {\n await notifyUnauthorized(response, requestId);\n }\n } else {\n await notifyUnauthorized(response, requestId);\n }\n }\n\n if (!response.ok) {\n throw await parseError(response, requestId);\n }\n\n if (response.status === 204) {\n return undefined as T;\n }\n\n const contentType = response.headers.get(\"content-type\") ?? \"\";\n if (contentType.includes(\"application/json\")) {\n return (await response.json()) as T;\n }\n return (await response.text()) as unknown as T;\n }\n\n async function request<T>(path: string, options: RequestOptions = {}): Promise<T> {\n const retryOptions = resolveRetry(config.retry);\n if (!retryOptions) return attempt<T>(path, options);\n\n const method = (options.method ?? \"GET\").toUpperCase();\n return retryWithBackoff(() => attempt<T>(path, options), {\n ...retryOptions,\n shouldRetry:\n retryOptions.shouldRetry ?? ((error: unknown) => isRetriableFailure(error, method)),\n });\n }\n\n async function upload<T>(\n path: string,\n formData: FormData,\n method: \"POST\" | \"PUT\" | \"PATCH\" = \"POST\",\n ): Promise<T> {\n return request<T>(path, { method, body: formData });\n }\n\n return {\n request,\n get: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"GET\" }),\n post: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"POST\" }),\n put: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"PUT\" }),\n patch: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"PATCH\" }),\n delete: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"DELETE\" }),\n upload,\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"api-client.js","names":[],"sources":["../../src/http/api-client.ts"],"sourcesContent":["/**\n * @tempest-limits function-lines — createApiClient is over the limit and every line\n * is a request-lifecycle concern the client cannot delegate: base URL joining, the\n * auth header, the 401 refresh-and-replay, the opt-in retry wrapper and the response\n * parsing that turns a failure into a typed error.\n */\nimport { randomId } from \"../utils\";\nimport { buildApiUrl } from \"./build-url\";\nimport { buildApiError, TempestApiError, isRetriableStatus } from \"./errors\";\nimport { retry as retryWithBackoff } from \"./retry\";\nimport type { RetryOptions } from \"./retry\";\nimport { withTimeout } from \"./timeout\";\nimport type { ApiClient, ApiClientConfig, RequestOptions } from \"./types\";\n\n/**\n * Methods the built-in retry policy will replay.\n *\n * `PUT` and `DELETE` are idempotent on paper but stay out: a backend that logs,\n * bills, or fires a webhook per call still sees two, so replaying them is a\n * decision the caller makes through `shouldRetry`, not a default.\n */\nconst IDEMPOTENT_METHODS: ReadonlySet<string> = new Set([\"GET\", \"HEAD\", \"OPTIONS\"]);\n\n/**\n * Sub-500 statuses worth a second attempt: a network failure (status `0`), a\n * request timeout, a too-early replay, and a rate limit — which usually carries\n * the `Retry-After` the backoff already honours.\n */\n\n/**\n * The built-in retry policy, used when `retry` is `true` or is options carrying\n * no `shouldRetry` of their own.\n *\n * Conservative on purpose. Replaying a write can duplicate it, and replaying a\n * `400` or a `403` cannot fix a bad payload or a permission the caller does not\n * have — it only spends the user's time before showing the same error.\n *\n * @param error - Whatever the attempt threw.\n * @param method - The upper-cased HTTP method of the request.\n * @returns Whether the client should try again.\n */\nfunction isRetriableFailure(error: unknown, method: string): boolean {\n if (!IDEMPOTENT_METHODS.has(method)) return false;\n if (!(error instanceof TempestApiError)) return false;\n return isRetriableStatus(error.status);\n}\n\n/**\n * Normalize the `retry` config into options, or `null` when retrying is off.\n *\n * @param config - The `retry` field as the caller wrote it.\n * @returns Retry options to use, or `null` to run a single attempt.\n */\nfunction resolveRetry(config: boolean | RetryOptions | undefined): RetryOptions | null {\n if (!config) return null;\n return config === true ? {} : config;\n}\n\n/** Default milliseconds before a request is abandoned. */\nconst DEFAULT_TIMEOUT = 15_000;\n\n/** Default milliseconds before a `FormData` request is abandoned. */\nconst DEFAULT_UPLOAD_TIMEOUT = 300_000;\n\nfunction isFormData(body: unknown): body is FormData {\n return typeof FormData !== \"undefined\" && body instanceof FormData;\n}\n\n/**\n * Milliseconds since a `performance.now()` reading, rounded.\n *\n * `performance.now()` rather than `Date.now()` because this measures a\n * duration: the wall clock can step sideways mid-request (an NTP correction, a\n * VM resuming, the user changing the clock) and turn a 40 ms call into a\n * negative number. The monotonic clock cannot.\n *\n * @param startedAt - The reading taken before the work started.\n * @returns Whole milliseconds elapsed.\n */\nfunction elapsedMs(startedAt: number): number {\n return Math.round(performance.now() - startedAt);\n}\n\nasync function parseError(response: Response, sentRequestId?: string): Promise<TempestApiError> {\n let body: unknown;\n try {\n body = await response.clone().json();\n } catch {\n try {\n body = await response.text();\n } catch {\n body = null;\n }\n }\n return new TempestApiError(\n buildApiError(response.status, body, response.headers, sentRequestId),\n );\n}\n\n/**\n * Create a typed HTTP client backed by `fetch`.\n *\n * Handles JSON serialization, query params, bearer auth via `getToken`, uploads\n * via `FormData`, and throws a typed `ApiError` on any non-2xx response.\n *\n * **Expired sessions.** A `401` with `refresh` configured awaits the refresh and\n * replays the request once. `onUnauthorized` fires whenever that path ends\n * unauthorized anyway — the refresh threw, or the replay came back `401` — which\n * is the signal to clear the session. Without `refresh`, the first `401` calls\n * it directly.\n *\n * **Retries** are off unless you set `retry`. See {@link ApiClientConfig.retry}\n * for the built-in policy; it never replays a write.\n *\n * **Logging** is off unless you pass a `logger`. With one, every finished attempt\n * writes a line — `debug` under 400, `warn` from 400 up, plus a `warn` when\n * `onUnauthorized` fires — carrying `requestId`, `status` and elapsed `ms`, and\n * never a body, header or query string. The level and the destination belong to\n * the logger, not to a boolean here.\n *\n * @example\n * const api = createApiClient({\n * baseURL: import.meta.env.VITE_API_URL,\n * getToken: () => useAuthStore.getState().token,\n * refresh,\n * onUnauthorized: () => useAuthStore.getState().logout(),\n * logger: createLogger({ level: import.meta.env.DEV ? \"debug\" : \"warn\" }).child(\"http\"),\n * retry: true,\n * });\n *\n * @param config - Base URL plus the optional auth, retry and fetch hooks.\n * @returns A client with `request`/`get`/`post`/`put`/`patch`/`delete`/`upload`.\n */\nexport function createApiClient(config: ApiClientConfig): ApiClient {\n const fetcher = config.fetcher ?? globalThis.fetch.bind(globalThis);\n\n function authHeaders(): Record<string, string> {\n const token = config.getToken?.();\n return token ? { Authorization: `Bearer ${token}` } : {};\n }\n\n async function rawRequest(\n path: string,\n options: RequestOptions,\n requestId?: string,\n ): Promise<Response> {\n const { body, params, headers, signal, timeout, ...rest } = options;\n const isForm = isFormData(body);\n const configured = isForm ? config.uploadTimeout : config.timeout;\n const fallback = isForm ? DEFAULT_UPLOAD_TIMEOUT : DEFAULT_TIMEOUT;\n const limit =\n timeout !== undefined ? timeout : configured !== undefined ? configured : fallback;\n\n const finalHeaders: Record<string, string> = {\n ...(isForm ? {} : { \"Content-Type\": \"application/json\" }),\n ...(requestId ? { \"X-Request-ID\": requestId } : {}),\n ...config.headers,\n ...authHeaders(),\n ...(headers as Record<string, string> | undefined),\n };\n\n const timed = withTimeout(signal, limit);\n const init: RequestInit = {\n ...rest,\n signal: timed.signal,\n headers: finalHeaders,\n credentials: config.withCredentials ? \"include\" : rest.credentials,\n body:\n body === undefined || body === null\n ? undefined\n : isForm\n ? (body as FormData)\n : JSON.stringify(body),\n };\n\n const url = buildApiUrl(config.baseURL, path, { prefix: config.prefix, params });\n try {\n return await fetcher(url, init);\n } catch (cause) {\n if (timed.timedOut()) {\n throw new TempestApiError({\n status: 0,\n detail: `A requisição excedeu ${limit}ms e foi abandonada.`,\n });\n }\n throw cause;\n } finally {\n timed.dispose();\n }\n }\n\n async function send(\n path: string,\n options: RequestOptions,\n requestId: string,\n method: string,\n ): Promise<Response> {\n const log = config.logger;\n if (!log) return rawRequest(path, options, requestId);\n\n const startedAt = performance.now();\n try {\n const response = await rawRequest(path, options, requestId);\n const entry = { requestId, status: response.status, ms: elapsedMs(startedAt) };\n const line = `${method} ${path} → ${response.status}`;\n if (response.status >= 400) log.warn(line, entry);\n else log.debug(line, entry);\n return response;\n } catch (error) {\n log.warn(`${method} ${path} → no response`, {\n requestId,\n ms: elapsedMs(startedAt),\n error,\n });\n throw error;\n }\n }\n\n async function notifyUnauthorized(response: Response, requestId: string): Promise<void> {\n if (!config.onUnauthorized) return;\n config.logger?.warn(`unauthorized — calling onUnauthorized`, {\n requestId,\n status: response.status,\n });\n try {\n await config.onUnauthorized(response);\n } catch (error) {\n config.logger?.warn(`onUnauthorized threw — keeping the original response error`, {\n requestId,\n status: response.status,\n error,\n });\n }\n }\n\n async function attempt<T>(path: string, options: RequestOptions): Promise<T> {\n const requestId = config.requestId ? config.requestId() : randomId();\n const method = (options.method ?? \"GET\").toUpperCase();\n let response = await send(path, options, requestId, method);\n\n if (response.status === 401) {\n if (config.refresh) {\n try {\n await config.refresh();\n response = await send(path, options, requestId, method);\n } catch {\n await notifyUnauthorized(response, requestId);\n throw await parseError(response, requestId);\n }\n if (response.status === 401) {\n await notifyUnauthorized(response, requestId);\n }\n } else {\n await notifyUnauthorized(response, requestId);\n }\n }\n\n if (!response.ok) {\n throw await parseError(response, requestId);\n }\n\n if (response.status === 204) {\n return undefined as T;\n }\n\n const contentType = response.headers.get(\"content-type\") ?? \"\";\n if (contentType.includes(\"application/json\")) {\n return (await response.json()) as T;\n }\n return (await response.text()) as unknown as T;\n }\n\n async function request<T>(path: string, options: RequestOptions = {}): Promise<T> {\n const retryOptions = resolveRetry(config.retry);\n if (!retryOptions) return attempt<T>(path, options);\n\n const method = (options.method ?? \"GET\").toUpperCase();\n return retryWithBackoff(() => attempt<T>(path, options), {\n ...retryOptions,\n shouldRetry:\n retryOptions.shouldRetry ?? ((error: unknown) => isRetriableFailure(error, method)),\n });\n }\n\n async function upload<T>(\n path: string,\n formData: FormData,\n method: \"POST\" | \"PUT\" | \"PATCH\" = \"POST\",\n options?: Omit<RequestOptions, \"body\" | \"method\">,\n ): Promise<T> {\n return request<T>(path, { ...options, method, body: formData });\n }\n\n return {\n request,\n get: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"GET\" }),\n post: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"POST\" }),\n put: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"PUT\" }),\n patch: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"PATCH\" }),\n delete: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"DELETE\" }),\n upload,\n };\n}\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- const e=require("./errors.cjs");var t={offline:`Sem conexão com o servidor. Verifique sua internet e tente de novo.`,validation:`Confira os campos destacados e tente de novo.`},n=`tempest.error.offline`,r=`tempest.error.validation`;function i(){return typeof navigator<`u`&&navigator.onLine===!1}function a(n,r,a){let o=a?.offline??t.offline;if(e.isApiError(n)){if(n.status===0)return o;if(n.fields&&Object.keys(n.fields).length>0)return a?.validation??t.validation;let i=n.detail.trim();return i!==``&&i!==e.syntheticDetail(n.status)?i:`${r} (HTTP ${n.status})`}return i()?o:r}exports.API_ERROR_OFFLINE_KEY=n,exports.API_ERROR_VALIDATION_KEY=r,exports.DEFAULT_API_ERROR_STRINGS=t,exports.describeApiError=a;
1
+ const e=require("./errors.cjs");var t={offline:`Sem conexão com o servidor. Verifique sua internet e tente de novo.`,validation:`Confira os campos destacados e tente de novo.`},n=`tempest.error.offline`,r=`tempest.error.validation`;function i(){return typeof navigator<`u`&&navigator.onLine===!1}function a(n,r,a){let o=a?.offline??t.offline;if(e.isApiError(n)){let i=n.code===void 0?void 0:a?.codes?.[n.code];if(i!==void 0)return i;if(n.status===0)return o;if(n.fields&&Object.keys(n.fields).length>0)return a?.validation??t.validation;let s=n.detail.trim();return a?.useDetail!==!1&&s!==``&&s!==e.syntheticDetail(n.status)?s:`${r} (HTTP ${n.status})`}return i()?o:r}exports.API_ERROR_OFFLINE_KEY=n,exports.API_ERROR_VALIDATION_KEY=r,exports.DEFAULT_API_ERROR_STRINGS=t,exports.describeApiError=a;
2
2
  //# sourceMappingURL=describe-api-error.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"describe-api-error.cjs","names":[],"sources":["../../src/http/describe-api-error.ts"],"sourcesContent":["// The last mile of error handling: a typed error is what code reads, a sentence\n// is what a person reads, and every app was writing the funnel between the two.\n// The case everyone forgets is the request that never reached the server, which\n// without special handling renders as the nonsense \"erro 0\".\n\nimport { isApiError, syntheticDetail } from \"./errors\";\n\n/** The fixed sentences {@link describeApiError} may need. */\nexport interface ApiErrorStrings {\n /** Shown when the request never reached the server. */\n offline: string;\n /**\n * Shown when the backend rejected the payload field by field.\n *\n * The per-field messages are on `error.fields`, to be attached to the inputs\n * themselves; this sentence is what the toast says.\n */\n validation: string;\n}\n\n/**\n * PT-BR defaults, used when no strings are supplied and no catalog answers.\n *\n * The SDK's copy is pt-BR everywhere else (`FilterBar`, `DataTable`, `Chat`), so\n * the default here matches rather than introducing an English string that only\n * shows up on a network failure.\n */\nexport const DEFAULT_API_ERROR_STRINGS: ApiErrorStrings = {\n offline: \"Sem conexão com o servidor. Verifique sua internet e tente de novo.\",\n validation: \"Confira os campos destacados e tente de novo.\",\n};\n\n/**\n * Translation key the {@link useDescribeApiError} hook looks up.\n *\n * A catalog that does not define it falls back to\n * {@link DEFAULT_API_ERROR_STRINGS}, because `t` returns the key itself when the\n * lookup misses and printing `tempest.error.offline` at the user would be worse\n * than printing pt-BR at them.\n */\nexport const API_ERROR_OFFLINE_KEY = \"tempest.error.offline\";\n\n/**\n * Translation key for the validation sentence, looked up the same way as\n * {@link API_ERROR_OFFLINE_KEY}.\n */\nexport const API_ERROR_VALIDATION_KEY = \"tempest.error.validation\";\n\n/**\n * Whether the browser currently reports itself as offline.\n *\n * `fetch` rejects a network failure with a plain `TypeError` whose message\n * differs per browser (\"Failed to fetch\", \"NetworkError when attempting to fetch\n * resource.\", \"Load failed\"), so sniffing the message is not portable. The online\n * flag is, and it is the signal that matters for the sentence being chosen.\n *\n * @returns `true` only when the environment positively says it is offline.\n */\nfunction browserIsOffline(): boolean {\n return typeof navigator !== \"undefined\" && navigator.onLine === false;\n}\n\n/**\n * Turn any caught value into a sentence worth showing.\n *\n * The funnel, in order:\n *\n * 1. A request that never reached the server — `status === 0`, or a non-API\n * error thrown while the browser reports itself offline — produces the\n * offline sentence. This is the step apps skip, and skipping it renders\n * \"erro 0\" or a raw `TypeError` at the user.\n * 2. A validation rejection — `error.fields` is set — produces the validation\n * sentence, **not** `detail`. On a `422` the `detail` line is assembled from\n * the backend's field paths and the validator's own wording\n * (`\"items.0.price: Input should be greater than 0\"`), which is right for a\n * log and wrong for a person: it is half English in a pt-BR screen and it\n * names internals. The per-field messages stay on `fields`, where a form can\n * attach them to the inputs that failed.\n * 3. The backend's own `detail`, which is the most specific thing available and\n * is already written for a person.\n * 4. `fallback`, with `(HTTP <status>)` appended when a status is known, so the\n * screenshot in the support ticket carries the one fact a developer needs.\n *\n * Pure on purpose: it works in an interceptor, in a logger and anywhere outside\n * the React tree. {@link useDescribeApiError} is the same funnel with the\n * sentences resolved through `I18nProvider`.\n *\n * @example\n * catch (error) {\n * toast(describeApiError(error, \"Não foi possível salvar o pedido\"));\n * }\n *\n * @param error - The caught value, of any shape.\n * @param fallback - What to say when the error carries nothing better.\n * @param strings - Overrides for the fixed sentences.\n * @returns A sentence to show the user.\n */\nexport function describeApiError(\n error: unknown,\n fallback: string,\n strings?: Partial<ApiErrorStrings>,\n): string {\n const offline = strings?.offline ?? DEFAULT_API_ERROR_STRINGS.offline;\n\n if (isApiError(error)) {\n if (error.status === 0) return offline;\n if (error.fields && Object.keys(error.fields).length > 0) {\n return strings?.validation ?? DEFAULT_API_ERROR_STRINGS.validation;\n }\n const detail = error.detail.trim();\n if (detail !== \"\" && detail !== syntheticDetail(error.status)) return detail;\n return `${fallback} (HTTP ${error.status})`;\n }\n\n if (browserIsOffline()) return offline;\n\n return fallback;\n}\n"],"mappings":"gCA2BA,IAAa,EAA6C,CACtD,QAAS,sEACT,WAAY,+CAChB,EAUa,EAAwB,wBAMxB,EAA2B,2BAYxC,SAAS,GAA4B,CACjC,OAAO,OAAO,UAAc,KAAe,UAAU,SAAW,EACpE,CAqCA,SAAgB,EACZ,EACA,EACA,EACM,CACN,IAAM,EAAU,GAAS,SAAW,EAA0B,QAE9D,GAAI,EAAA,WAAW,CAAK,EAAG,CACnB,GAAI,EAAM,SAAW,EAAG,OAAO,EAC/B,GAAI,EAAM,QAAU,OAAO,KAAK,EAAM,MAAM,CAAC,CAAC,OAAS,EACnD,OAAO,GAAS,YAAc,EAA0B,WAE5D,IAAM,EAAS,EAAM,OAAO,KAAK,EAEjC,OADI,IAAW,IAAM,IAAW,EAAA,gBAAgB,EAAM,MAAM,EAAU,EAC/D,GAAG,EAAS,SAAS,EAAM,OAAO,EAC7C,CAIA,OAFI,EAAiB,EAAU,EAExB,CACX"}
1
+ {"version":3,"file":"describe-api-error.cjs","names":[],"sources":["../../src/http/describe-api-error.ts"],"sourcesContent":["// The last mile of error handling: a typed error is what code reads, a sentence\n// is what a person reads, and every app was writing the funnel between the two.\n// The case everyone forgets is the request that never reached the server, which\n// without special handling renders as the nonsense \"erro 0\".\n\nimport { isApiError, syntheticDetail } from \"./errors\";\n\n/** The fixed sentences {@link describeApiError} may need. */\nexport interface ApiErrorStrings {\n /** Shown when the request never reached the server. */\n offline: string;\n /**\n * Shown when the backend rejected the payload field by field.\n *\n * The per-field messages are on `error.fields`, to be attached to the inputs\n * themselves; this sentence is what the toast says.\n */\n validation: string;\n}\n\n/**\n * PT-BR defaults, used when no strings are supplied and no catalog answers.\n *\n * The SDK's copy is pt-BR everywhere else (`FilterBar`, `DataTable`, `Chat`), so\n * the default here matches rather than introducing an English string that only\n * shows up on a network failure.\n */\nexport const DEFAULT_API_ERROR_STRINGS: ApiErrorStrings = {\n offline: \"Sem conexão com o servidor. Verifique sua internet e tente de novo.\",\n validation: \"Confira os campos destacados e tente de novo.\",\n};\n\n/**\n * Translation key the {@link useDescribeApiError} hook looks up.\n *\n * A catalog that does not define it falls back to\n * {@link DEFAULT_API_ERROR_STRINGS}, because `t` returns the key itself when the\n * lookup misses and printing `tempest.error.offline` at the user would be worse\n * than printing pt-BR at them.\n */\nexport const API_ERROR_OFFLINE_KEY = \"tempest.error.offline\";\n\n/**\n * Translation key for the validation sentence, looked up the same way as\n * {@link API_ERROR_OFFLINE_KEY}.\n */\nexport const API_ERROR_VALIDATION_KEY = \"tempest.error.validation\";\n\n/**\n * Everything {@link describeApiError} accepts beyond the error and the fallback.\n *\n * Extends the fixed sentences rather than sitting beside them, so a caller that\n * already passed `{ offline, validation }` keeps compiling untouched.\n */\nexport interface DescribeApiErrorOptions extends Partial<ApiErrorStrings> {\n /**\n * Maps the backend's programmatic `code` to a sentence in your language.\n *\n * The client already surfaces `code` on `ApiError`, but without this every\n * app writes the same `switch` over it. A hit here wins over every other\n * step: it is the only sentence written for that exact case, by someone who\n * knew both the backend contract and the screen it lands on.\n */\n codes?: Readonly<Record<string, string>>;\n /**\n * Whether the backend's `detail` may be shown when no `code` matched.\n * Default `true`.\n *\n * Set it to `false` when `detail` is written for developers rather than\n * users, or when it could echo internals — the result is then always either\n * a sentence you wrote or the fallback.\n */\n useDetail?: boolean;\n}\n\n/**\n * Whether the browser currently reports itself as offline.\n *\n * `fetch` rejects a network failure with a plain `TypeError` whose message\n * differs per browser (\"Failed to fetch\", \"NetworkError when attempting to fetch\n * resource.\", \"Load failed\"), so sniffing the message is not portable. The online\n * flag is, and it is the signal that matters for the sentence being chosen.\n *\n * @returns `true` only when the environment positively says it is offline.\n */\nfunction browserIsOffline(): boolean {\n return typeof navigator !== \"undefined\" && navigator.onLine === false;\n}\n\n/**\n * Turn any caught value into a sentence worth showing.\n *\n * The funnel, in order:\n *\n * 0. `codes[error.code]` — the sentence you wrote for that exact backend case.\n * Checked first because nothing the funnel derives can beat it, and because a\n * request that never landed carries no `code` for it to shadow.\n * 1. A request that never reached the server — `status === 0`, or a non-API\n * error thrown while the browser reports itself offline — produces the\n * offline sentence. This is the step apps skip, and skipping it renders\n * \"erro 0\" or a raw `TypeError` at the user.\n * 2. A validation rejection — `error.fields` is set — produces the validation\n * sentence, **not** `detail`. On a `422` the `detail` line is assembled from\n * the backend's field paths and the validator's own wording\n * (`\"items.0.price: Input should be greater than 0\"`), which is right for a\n * log and wrong for a person: it is half English in a pt-BR screen and it\n * names internals. The per-field messages stay on `fields`, where a form can\n * attach them to the inputs that failed.\n * 3. The backend's own `detail`, which is the most specific thing available and\n * is already written for a person — unless `useDetail: false` says that text\n * is for developers.\n * 4. `fallback`, with `(HTTP <status>)` appended when a status is known, so the\n * screenshot in the support ticket carries the one fact a developer needs.\n *\n * Pure on purpose: it works in an interceptor, in a logger and anywhere outside\n * the React tree. {@link useDescribeApiError} is the same funnel with the\n * sentences resolved through `I18nProvider`.\n *\n * @example\n * catch (error) {\n * toast(describeApiError(error, \"Não foi possível salvar o pedido\"));\n * }\n *\n * @example\n * catch (error) {\n * toast(\n * describeApiError(error, \"Não foi possível se candidatar\", {\n * codes: {\n * SERVICE_FULL: \"Este serviço atingiu o limite de vagas.\",\n * CANDIDATE_ALREADY_EXISTS: \"Você já se candidatou a este serviço.\",\n * },\n * useDetail: false,\n * }),\n * );\n * }\n *\n * @param error - The caught value, of any shape.\n * @param fallback - What to say when the error carries nothing better.\n * @param options - A `codes` catalog, `useDetail`, and overrides for the fixed\n * sentences.\n * @returns A sentence to show the user.\n */\nexport function describeApiError(\n error: unknown,\n fallback: string,\n options?: DescribeApiErrorOptions,\n): string {\n const offline = options?.offline ?? DEFAULT_API_ERROR_STRINGS.offline;\n\n if (isApiError(error)) {\n const mapped = error.code === undefined ? undefined : options?.codes?.[error.code];\n if (mapped !== undefined) return mapped;\n if (error.status === 0) return offline;\n if (error.fields && Object.keys(error.fields).length > 0) {\n return options?.validation ?? DEFAULT_API_ERROR_STRINGS.validation;\n }\n const detail = error.detail.trim();\n if (\n options?.useDetail !== false &&\n detail !== \"\" &&\n detail !== syntheticDetail(error.status)\n ) {\n return detail;\n }\n return `${fallback} (HTTP ${error.status})`;\n }\n\n if (browserIsOffline()) return offline;\n\n return fallback;\n}\n"],"mappings":"gCA2BA,IAAa,EAA6C,CACtD,QAAS,sEACT,WAAY,+CAChB,EAUa,EAAwB,wBAMxB,EAA2B,2BAuCxC,SAAS,GAA4B,CACjC,OAAO,OAAO,UAAc,KAAe,UAAU,SAAW,EACpE,CAuDA,SAAgB,EACZ,EACA,EACA,EACM,CACN,IAAM,EAAU,GAAS,SAAW,EAA0B,QAE9D,GAAI,EAAA,WAAW,CAAK,EAAG,CACnB,IAAM,EAAS,EAAM,OAAS,IAAA,GAAY,IAAA,GAAY,GAAS,QAAQ,EAAM,MAC7E,GAAI,IAAW,IAAA,GAAW,OAAO,EACjC,GAAI,EAAM,SAAW,EAAG,OAAO,EAC/B,GAAI,EAAM,QAAU,OAAO,KAAK,EAAM,MAAM,CAAC,CAAC,OAAS,EACnD,OAAO,GAAS,YAAc,EAA0B,WAE5D,IAAM,EAAS,EAAM,OAAO,KAAK,EAQjC,OANI,GAAS,YAAc,IACvB,IAAW,IACX,IAAW,EAAA,gBAAgB,EAAM,MAAM,EAEhC,EAEJ,GAAG,EAAS,SAAS,EAAM,OAAO,EAC7C,CAIA,OAFI,EAAiB,EAAU,EAExB,CACX"}
@@ -10,10 +10,12 @@ function a() {
10
10
  function o(r, i, o) {
11
11
  let s = o?.offline ?? n.offline;
12
12
  if (e(r)) {
13
+ let e = r.code === void 0 ? void 0 : o?.codes?.[r.code];
14
+ if (e !== void 0) return e;
13
15
  if (r.status === 0) return s;
14
16
  if (r.fields && Object.keys(r.fields).length > 0) return o?.validation ?? n.validation;
15
- let e = r.detail.trim();
16
- return e !== "" && e !== t(r.status) ? e : `${i} (HTTP ${r.status})`;
17
+ let a = r.detail.trim();
18
+ return o?.useDetail !== !1 && a !== "" && a !== t(r.status) ? a : `${i} (HTTP ${r.status})`;
17
19
  }
18
20
  return a() ? s : i;
19
21
  }
@@ -1 +1 @@
1
- {"version":3,"file":"describe-api-error.js","names":[],"sources":["../../src/http/describe-api-error.ts"],"sourcesContent":["// The last mile of error handling: a typed error is what code reads, a sentence\n// is what a person reads, and every app was writing the funnel between the two.\n// The case everyone forgets is the request that never reached the server, which\n// without special handling renders as the nonsense \"erro 0\".\n\nimport { isApiError, syntheticDetail } from \"./errors\";\n\n/** The fixed sentences {@link describeApiError} may need. */\nexport interface ApiErrorStrings {\n /** Shown when the request never reached the server. */\n offline: string;\n /**\n * Shown when the backend rejected the payload field by field.\n *\n * The per-field messages are on `error.fields`, to be attached to the inputs\n * themselves; this sentence is what the toast says.\n */\n validation: string;\n}\n\n/**\n * PT-BR defaults, used when no strings are supplied and no catalog answers.\n *\n * The SDK's copy is pt-BR everywhere else (`FilterBar`, `DataTable`, `Chat`), so\n * the default here matches rather than introducing an English string that only\n * shows up on a network failure.\n */\nexport const DEFAULT_API_ERROR_STRINGS: ApiErrorStrings = {\n offline: \"Sem conexão com o servidor. Verifique sua internet e tente de novo.\",\n validation: \"Confira os campos destacados e tente de novo.\",\n};\n\n/**\n * Translation key the {@link useDescribeApiError} hook looks up.\n *\n * A catalog that does not define it falls back to\n * {@link DEFAULT_API_ERROR_STRINGS}, because `t` returns the key itself when the\n * lookup misses and printing `tempest.error.offline` at the user would be worse\n * than printing pt-BR at them.\n */\nexport const API_ERROR_OFFLINE_KEY = \"tempest.error.offline\";\n\n/**\n * Translation key for the validation sentence, looked up the same way as\n * {@link API_ERROR_OFFLINE_KEY}.\n */\nexport const API_ERROR_VALIDATION_KEY = \"tempest.error.validation\";\n\n/**\n * Whether the browser currently reports itself as offline.\n *\n * `fetch` rejects a network failure with a plain `TypeError` whose message\n * differs per browser (\"Failed to fetch\", \"NetworkError when attempting to fetch\n * resource.\", \"Load failed\"), so sniffing the message is not portable. The online\n * flag is, and it is the signal that matters for the sentence being chosen.\n *\n * @returns `true` only when the environment positively says it is offline.\n */\nfunction browserIsOffline(): boolean {\n return typeof navigator !== \"undefined\" && navigator.onLine === false;\n}\n\n/**\n * Turn any caught value into a sentence worth showing.\n *\n * The funnel, in order:\n *\n * 1. A request that never reached the server — `status === 0`, or a non-API\n * error thrown while the browser reports itself offline — produces the\n * offline sentence. This is the step apps skip, and skipping it renders\n * \"erro 0\" or a raw `TypeError` at the user.\n * 2. A validation rejection — `error.fields` is set — produces the validation\n * sentence, **not** `detail`. On a `422` the `detail` line is assembled from\n * the backend's field paths and the validator's own wording\n * (`\"items.0.price: Input should be greater than 0\"`), which is right for a\n * log and wrong for a person: it is half English in a pt-BR screen and it\n * names internals. The per-field messages stay on `fields`, where a form can\n * attach them to the inputs that failed.\n * 3. The backend's own `detail`, which is the most specific thing available and\n * is already written for a person.\n * 4. `fallback`, with `(HTTP <status>)` appended when a status is known, so the\n * screenshot in the support ticket carries the one fact a developer needs.\n *\n * Pure on purpose: it works in an interceptor, in a logger and anywhere outside\n * the React tree. {@link useDescribeApiError} is the same funnel with the\n * sentences resolved through `I18nProvider`.\n *\n * @example\n * catch (error) {\n * toast(describeApiError(error, \"Não foi possível salvar o pedido\"));\n * }\n *\n * @param error - The caught value, of any shape.\n * @param fallback - What to say when the error carries nothing better.\n * @param strings - Overrides for the fixed sentences.\n * @returns A sentence to show the user.\n */\nexport function describeApiError(\n error: unknown,\n fallback: string,\n strings?: Partial<ApiErrorStrings>,\n): string {\n const offline = strings?.offline ?? DEFAULT_API_ERROR_STRINGS.offline;\n\n if (isApiError(error)) {\n if (error.status === 0) return offline;\n if (error.fields && Object.keys(error.fields).length > 0) {\n return strings?.validation ?? DEFAULT_API_ERROR_STRINGS.validation;\n }\n const detail = error.detail.trim();\n if (detail !== \"\" && detail !== syntheticDetail(error.status)) return detail;\n return `${fallback} (HTTP ${error.status})`;\n }\n\n if (browserIsOffline()) return offline;\n\n return fallback;\n}\n"],"mappings":";;AA2BA,IAAa,IAA6C;CACtD,SAAS;CACT,YAAY;AAChB,GAUa,IAAwB,yBAMxB,IAA2B;AAYxC,SAAS,IAA4B;CACjC,OAAO,OAAO,YAAc,OAAe,UAAU,WAAW;AACpE;AAqCA,SAAgB,EACZ,GACA,GACA,GACM;CACN,IAAM,IAAU,GAAS,WAAW,EAA0B;CAE9D,IAAI,EAAW,CAAK,GAAG;EACnB,IAAI,EAAM,WAAW,GAAG,OAAO;EAC/B,IAAI,EAAM,UAAU,OAAO,KAAK,EAAM,MAAM,CAAC,CAAC,SAAS,GACnD,OAAO,GAAS,cAAc,EAA0B;EAE5D,IAAM,IAAS,EAAM,OAAO,KAAK;EAEjC,OADI,MAAW,MAAM,MAAW,EAAgB,EAAM,MAAM,IAAU,IAC/D,GAAG,EAAS,SAAS,EAAM,OAAO;CAC7C;CAIA,OAFI,EAAiB,IAAU,IAExB;AACX"}
1
+ {"version":3,"file":"describe-api-error.js","names":[],"sources":["../../src/http/describe-api-error.ts"],"sourcesContent":["// The last mile of error handling: a typed error is what code reads, a sentence\n// is what a person reads, and every app was writing the funnel between the two.\n// The case everyone forgets is the request that never reached the server, which\n// without special handling renders as the nonsense \"erro 0\".\n\nimport { isApiError, syntheticDetail } from \"./errors\";\n\n/** The fixed sentences {@link describeApiError} may need. */\nexport interface ApiErrorStrings {\n /** Shown when the request never reached the server. */\n offline: string;\n /**\n * Shown when the backend rejected the payload field by field.\n *\n * The per-field messages are on `error.fields`, to be attached to the inputs\n * themselves; this sentence is what the toast says.\n */\n validation: string;\n}\n\n/**\n * PT-BR defaults, used when no strings are supplied and no catalog answers.\n *\n * The SDK's copy is pt-BR everywhere else (`FilterBar`, `DataTable`, `Chat`), so\n * the default here matches rather than introducing an English string that only\n * shows up on a network failure.\n */\nexport const DEFAULT_API_ERROR_STRINGS: ApiErrorStrings = {\n offline: \"Sem conexão com o servidor. Verifique sua internet e tente de novo.\",\n validation: \"Confira os campos destacados e tente de novo.\",\n};\n\n/**\n * Translation key the {@link useDescribeApiError} hook looks up.\n *\n * A catalog that does not define it falls back to\n * {@link DEFAULT_API_ERROR_STRINGS}, because `t` returns the key itself when the\n * lookup misses and printing `tempest.error.offline` at the user would be worse\n * than printing pt-BR at them.\n */\nexport const API_ERROR_OFFLINE_KEY = \"tempest.error.offline\";\n\n/**\n * Translation key for the validation sentence, looked up the same way as\n * {@link API_ERROR_OFFLINE_KEY}.\n */\nexport const API_ERROR_VALIDATION_KEY = \"tempest.error.validation\";\n\n/**\n * Everything {@link describeApiError} accepts beyond the error and the fallback.\n *\n * Extends the fixed sentences rather than sitting beside them, so a caller that\n * already passed `{ offline, validation }` keeps compiling untouched.\n */\nexport interface DescribeApiErrorOptions extends Partial<ApiErrorStrings> {\n /**\n * Maps the backend's programmatic `code` to a sentence in your language.\n *\n * The client already surfaces `code` on `ApiError`, but without this every\n * app writes the same `switch` over it. A hit here wins over every other\n * step: it is the only sentence written for that exact case, by someone who\n * knew both the backend contract and the screen it lands on.\n */\n codes?: Readonly<Record<string, string>>;\n /**\n * Whether the backend's `detail` may be shown when no `code` matched.\n * Default `true`.\n *\n * Set it to `false` when `detail` is written for developers rather than\n * users, or when it could echo internals — the result is then always either\n * a sentence you wrote or the fallback.\n */\n useDetail?: boolean;\n}\n\n/**\n * Whether the browser currently reports itself as offline.\n *\n * `fetch` rejects a network failure with a plain `TypeError` whose message\n * differs per browser (\"Failed to fetch\", \"NetworkError when attempting to fetch\n * resource.\", \"Load failed\"), so sniffing the message is not portable. The online\n * flag is, and it is the signal that matters for the sentence being chosen.\n *\n * @returns `true` only when the environment positively says it is offline.\n */\nfunction browserIsOffline(): boolean {\n return typeof navigator !== \"undefined\" && navigator.onLine === false;\n}\n\n/**\n * Turn any caught value into a sentence worth showing.\n *\n * The funnel, in order:\n *\n * 0. `codes[error.code]` — the sentence you wrote for that exact backend case.\n * Checked first because nothing the funnel derives can beat it, and because a\n * request that never landed carries no `code` for it to shadow.\n * 1. A request that never reached the server — `status === 0`, or a non-API\n * error thrown while the browser reports itself offline — produces the\n * offline sentence. This is the step apps skip, and skipping it renders\n * \"erro 0\" or a raw `TypeError` at the user.\n * 2. A validation rejection — `error.fields` is set — produces the validation\n * sentence, **not** `detail`. On a `422` the `detail` line is assembled from\n * the backend's field paths and the validator's own wording\n * (`\"items.0.price: Input should be greater than 0\"`), which is right for a\n * log and wrong for a person: it is half English in a pt-BR screen and it\n * names internals. The per-field messages stay on `fields`, where a form can\n * attach them to the inputs that failed.\n * 3. The backend's own `detail`, which is the most specific thing available and\n * is already written for a person — unless `useDetail: false` says that text\n * is for developers.\n * 4. `fallback`, with `(HTTP <status>)` appended when a status is known, so the\n * screenshot in the support ticket carries the one fact a developer needs.\n *\n * Pure on purpose: it works in an interceptor, in a logger and anywhere outside\n * the React tree. {@link useDescribeApiError} is the same funnel with the\n * sentences resolved through `I18nProvider`.\n *\n * @example\n * catch (error) {\n * toast(describeApiError(error, \"Não foi possível salvar o pedido\"));\n * }\n *\n * @example\n * catch (error) {\n * toast(\n * describeApiError(error, \"Não foi possível se candidatar\", {\n * codes: {\n * SERVICE_FULL: \"Este serviço atingiu o limite de vagas.\",\n * CANDIDATE_ALREADY_EXISTS: \"Você já se candidatou a este serviço.\",\n * },\n * useDetail: false,\n * }),\n * );\n * }\n *\n * @param error - The caught value, of any shape.\n * @param fallback - What to say when the error carries nothing better.\n * @param options - A `codes` catalog, `useDetail`, and overrides for the fixed\n * sentences.\n * @returns A sentence to show the user.\n */\nexport function describeApiError(\n error: unknown,\n fallback: string,\n options?: DescribeApiErrorOptions,\n): string {\n const offline = options?.offline ?? DEFAULT_API_ERROR_STRINGS.offline;\n\n if (isApiError(error)) {\n const mapped = error.code === undefined ? undefined : options?.codes?.[error.code];\n if (mapped !== undefined) return mapped;\n if (error.status === 0) return offline;\n if (error.fields && Object.keys(error.fields).length > 0) {\n return options?.validation ?? DEFAULT_API_ERROR_STRINGS.validation;\n }\n const detail = error.detail.trim();\n if (\n options?.useDetail !== false &&\n detail !== \"\" &&\n detail !== syntheticDetail(error.status)\n ) {\n return detail;\n }\n return `${fallback} (HTTP ${error.status})`;\n }\n\n if (browserIsOffline()) return offline;\n\n return fallback;\n}\n"],"mappings":";;AA2BA,IAAa,IAA6C;CACtD,SAAS;CACT,YAAY;AAChB,GAUa,IAAwB,yBAMxB,IAA2B;AAuCxC,SAAS,IAA4B;CACjC,OAAO,OAAO,YAAc,OAAe,UAAU,WAAW;AACpE;AAuDA,SAAgB,EACZ,GACA,GACA,GACM;CACN,IAAM,IAAU,GAAS,WAAW,EAA0B;CAE9D,IAAI,EAAW,CAAK,GAAG;EACnB,IAAM,IAAS,EAAM,SAAS,KAAA,IAAY,KAAA,IAAY,GAAS,QAAQ,EAAM;EAC7E,IAAI,MAAW,KAAA,GAAW,OAAO;EACjC,IAAI,EAAM,WAAW,GAAG,OAAO;EAC/B,IAAI,EAAM,UAAU,OAAO,KAAK,EAAM,MAAM,CAAC,CAAC,SAAS,GACnD,OAAO,GAAS,cAAc,EAA0B;EAE5D,IAAM,IAAS,EAAM,OAAO,KAAK;EAQjC,OANI,GAAS,cAAc,MACvB,MAAW,MACX,MAAW,EAAgB,EAAM,MAAM,IAEhC,IAEJ,GAAG,EAAS,SAAS,EAAM,OAAO;CAC7C;CAIA,OAFI,EAAiB,IAAU,IAExB;AACX"}
@@ -0,0 +1,2 @@
1
+ function e(e,t){if(t===null)return{signal:e??void 0,timedOut:()=>!1,dispose:()=>{}};let n=new AbortController,r=!1,i=setTimeout(()=>{r=!0,n.abort()},t),a=()=>n.abort();return e&&(e.aborted?n.abort():e.addEventListener(`abort`,a,{once:!0})),{signal:n.signal,timedOut:()=>r,dispose:()=>{clearTimeout(i),e?.removeEventListener(`abort`,a)}}}exports.withTimeout=e;
2
+ //# sourceMappingURL=timeout.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"timeout.cjs","names":[],"sources":["../../src/http/timeout.ts"],"sourcesContent":["/**\n * The timeout half of `createApiClient`'s request plumbing.\n *\n * It lives beside the client rather than inside it because it is the one piece\n * with no knowledge of the SDK: given a caller's signal and a deadline it hands\n * back a signal to pass `fetch`, a way to tell whose abort fired, and the\n * cleanup. That makes it testable on its own and keeps the client file about\n * requests.\n */\n\n/** A signal to hand `fetch`, plus how to read the outcome and clean up. */\nexport interface TimedSignal {\n /** Pass this to `fetch`. */\n signal: AbortSignal | undefined;\n /** Whether the abort came from the timeout rather than the caller. */\n timedOut: () => boolean;\n /** Clear the timer and drop the listener. Always call it. */\n dispose: () => void;\n}\n\n/**\n * Compose the caller's signal with a timeout, tracking which one fires.\n *\n * Written with an explicit flag rather than `AbortSignal.timeout()`, whose\n * `TimeoutError` reason would tell the two apart for free. Two reasons: this\n * needs no `AbortSignal.any`, which is Baseline 2024 and would raise the\n * package's support floor for one line of convenience, and the flag is read\n * directly instead of through a reason string that a polyfill could reshape.\n *\n * A caller signal that is already aborted aborts immediately, so a request never\n * goes out for a query react-query has already cancelled.\n *\n * @param signal - The caller's signal, if any.\n * @param ms - Timeout in milliseconds, or `null` to only forward the signal.\n * @returns The composed signal, the outcome reader, and the cleanup.\n */\nexport function withTimeout(\n signal: AbortSignal | null | undefined,\n ms: number | null,\n): TimedSignal {\n if (ms === null) {\n return { signal: signal ?? undefined, timedOut: () => false, dispose: () => {} };\n }\n\n const controller = new AbortController();\n let timedOut = false;\n const timer = setTimeout(() => {\n timedOut = true;\n controller.abort();\n }, ms);\n const onAbort = (): void => controller.abort();\n\n if (signal) {\n if (signal.aborted) controller.abort();\n else signal.addEventListener(\"abort\", onAbort, { once: true });\n }\n\n return {\n signal: controller.signal,\n timedOut: () => timedOut,\n dispose: () => {\n clearTimeout(timer);\n signal?.removeEventListener(\"abort\", onAbort);\n },\n };\n}\n"],"mappings":"AAoCA,SAAgB,EACZ,EACA,EACW,CACX,GAAI,IAAO,KACP,MAAO,CAAE,OAAQ,GAAU,IAAA,GAAW,aAAgB,GAAO,YAAe,CAAC,CAAE,EAGnF,IAAM,EAAa,IAAI,gBACnB,EAAW,GACT,EAAQ,eAAiB,CAC3B,EAAW,GACX,EAAW,MAAM,CACrB,EAAG,CAAE,EACC,MAAsB,EAAW,MAAM,EAO7C,OALI,IACI,EAAO,QAAS,EAAW,MAAM,EAChC,EAAO,iBAAiB,QAAS,EAAS,CAAE,KAAM,EAAK,CAAC,GAG1D,CACH,OAAQ,EAAW,OACnB,aAAgB,EAChB,YAAe,CACX,aAAa,CAAK,EAClB,GAAQ,oBAAoB,QAAS,CAAO,CAChD,CACJ,CACJ"}
@@ -0,0 +1,22 @@
1
+ //#region src/http/timeout.ts
2
+ function e(e, t) {
3
+ if (t === null) return {
4
+ signal: e ?? void 0,
5
+ timedOut: () => !1,
6
+ dispose: () => {}
7
+ };
8
+ let n = new AbortController(), r = !1, i = setTimeout(() => {
9
+ r = !0, n.abort();
10
+ }, t), a = () => n.abort();
11
+ return e && (e.aborted ? n.abort() : e.addEventListener("abort", a, { once: !0 })), {
12
+ signal: n.signal,
13
+ timedOut: () => r,
14
+ dispose: () => {
15
+ clearTimeout(i), e?.removeEventListener("abort", a);
16
+ }
17
+ };
18
+ }
19
+ //#endregion
20
+ export { e as withTimeout };
21
+
22
+ //# sourceMappingURL=timeout.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"timeout.js","names":[],"sources":["../../src/http/timeout.ts"],"sourcesContent":["/**\n * The timeout half of `createApiClient`'s request plumbing.\n *\n * It lives beside the client rather than inside it because it is the one piece\n * with no knowledge of the SDK: given a caller's signal and a deadline it hands\n * back a signal to pass `fetch`, a way to tell whose abort fired, and the\n * cleanup. That makes it testable on its own and keeps the client file about\n * requests.\n */\n\n/** A signal to hand `fetch`, plus how to read the outcome and clean up. */\nexport interface TimedSignal {\n /** Pass this to `fetch`. */\n signal: AbortSignal | undefined;\n /** Whether the abort came from the timeout rather than the caller. */\n timedOut: () => boolean;\n /** Clear the timer and drop the listener. Always call it. */\n dispose: () => void;\n}\n\n/**\n * Compose the caller's signal with a timeout, tracking which one fires.\n *\n * Written with an explicit flag rather than `AbortSignal.timeout()`, whose\n * `TimeoutError` reason would tell the two apart for free. Two reasons: this\n * needs no `AbortSignal.any`, which is Baseline 2024 and would raise the\n * package's support floor for one line of convenience, and the flag is read\n * directly instead of through a reason string that a polyfill could reshape.\n *\n * A caller signal that is already aborted aborts immediately, so a request never\n * goes out for a query react-query has already cancelled.\n *\n * @param signal - The caller's signal, if any.\n * @param ms - Timeout in milliseconds, or `null` to only forward the signal.\n * @returns The composed signal, the outcome reader, and the cleanup.\n */\nexport function withTimeout(\n signal: AbortSignal | null | undefined,\n ms: number | null,\n): TimedSignal {\n if (ms === null) {\n return { signal: signal ?? undefined, timedOut: () => false, dispose: () => {} };\n }\n\n const controller = new AbortController();\n let timedOut = false;\n const timer = setTimeout(() => {\n timedOut = true;\n controller.abort();\n }, ms);\n const onAbort = (): void => controller.abort();\n\n if (signal) {\n if (signal.aborted) controller.abort();\n else signal.addEventListener(\"abort\", onAbort, { once: true });\n }\n\n return {\n signal: controller.signal,\n timedOut: () => timedOut,\n dispose: () => {\n clearTimeout(timer);\n signal?.removeEventListener(\"abort\", onAbort);\n },\n };\n}\n"],"mappings":";AAoCA,SAAgB,EACZ,GACA,GACW;CACX,IAAI,MAAO,MACP,OAAO;EAAE,QAAQ,KAAU,KAAA;EAAW,gBAAgB;EAAO,eAAe,CAAC;CAAE;CAGnF,IAAM,IAAa,IAAI,gBAAgB,GACnC,IAAW,IACT,IAAQ,iBAAiB;EAE3B,AADA,IAAW,IACX,EAAW,MAAM;CACrB,GAAG,CAAE,GACC,UAAsB,EAAW,MAAM;CAO7C,OALI,MACI,EAAO,UAAS,EAAW,MAAM,IAChC,EAAO,iBAAiB,SAAS,GAAS,EAAE,MAAM,GAAK,CAAC,IAG1D;EACH,QAAQ,EAAW;EACnB,gBAAgB;EAChB,eAAe;GAEX,AADA,aAAa,CAAK,GAClB,GAAQ,oBAAoB,SAAS,CAAO;EAChD;CACJ;AACJ"}
@@ -1,2 +1,2 @@
1
- const e=require("./describe-api-error.cjs"),t=require("../i18n/I18nProvider.cjs");let n=require("react");function r(t,n,r){let i=e.DEFAULT_API_ERROR_STRINGS[r];return t?.(n,void 0,{default:i})??i}function i(){let i=t.useOptionalI18n()?.t;return(0,n.useCallback)((t,n)=>e.describeApiError(t,n,{offline:r(i,e.API_ERROR_OFFLINE_KEY,`offline`),validation:r(i,e.API_ERROR_VALIDATION_KEY,`validation`)}),[i])}exports.useDescribeApiError=i;
1
+ const e=require("./describe-api-error.cjs"),t=require("../i18n/I18nProvider.cjs");let n=require("react");function r(t,n,r){let i=e.DEFAULT_API_ERROR_STRINGS[r];return t?.(n,void 0,{default:i})??i}function i(){let i=t.useOptionalI18n()?.t;return(0,n.useCallback)((t,n,a)=>e.describeApiError(t,n,{offline:r(i,e.API_ERROR_OFFLINE_KEY,`offline`),validation:r(i,e.API_ERROR_VALIDATION_KEY,`validation`),...a}),[i])}exports.useDescribeApiError=i;
2
2
  //# sourceMappingURL=use-describe-api-error.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"use-describe-api-error.cjs","names":[],"sources":["../../src/http/use-describe-api-error.ts"],"sourcesContent":["import { useCallback } from \"react\";\n\nimport type { I18n } from \"../i18n/create-i18n\";\nimport { useOptionalI18n } from \"../i18n/I18nProvider\";\nimport {\n API_ERROR_OFFLINE_KEY,\n API_ERROR_VALIDATION_KEY,\n DEFAULT_API_ERROR_STRINGS,\n describeApiError,\n} from \"./describe-api-error\";\n\n/**\n * {@link describeApiError} with its fixed sentences resolved through the active\n * `I18nProvider`.\n *\n * The funnel is not duplicated — this hook only supplies the strings and calls\n * the pure function. Which is also why both exist: the pure one runs in an\n * interceptor or a logger, where there is no React tree to read a context from,\n * and this one runs in a component without every caller passing translations\n * down by hand.\n *\n * Works with no provider at all: i18n is opt-in in this SDK, so a missing\n * provider — or a catalog that never defined `tempest.error.offline` — falls\n * back to the pt-BR default rather than crashing or printing the raw key.\n *\n * @example\n * const describe = useDescribeApiError();\n * const { mutate } = useMutation({\n * mutationFn: save,\n * onError: (error) => toast(describe(error, t(\"orders.saveFailed\"))),\n * });\n *\n * @returns A stable `(error, fallback) => string` function.\n */\n/**\n * Look one sentence up in the active catalog, falling back to the pt-BR default.\n *\n * The miss is the i18n layer's answer, through `t`'s `default`. It used to be\n * re-derived here by comparing the result against the key that had just been\n * passed in, which is wrong for a catalog that maps a key to itself —\n * `{ \"tempest.error.offline\": \"tempest.error.offline\" }`, which is what a\n * machine-generated or placeholder catalog produces. There the app's own\n * translation lost to the pt-BR default. Only the catalog's owner can answer\n * \"was this key defined\", and `lookup()` already knows.\n *\n * @param translate - The active catalog's `t`, if any provider is mounted.\n * @param key - The translation key to look up.\n * @param fallbackKey - Which {@link DEFAULT_API_ERROR_STRINGS} entry to use on a miss.\n * @returns The sentence to hand to `describeApiError`.\n */\nfunction resolve(\n translate: I18n[\"t\"] | undefined,\n key: string,\n fallbackKey: keyof typeof DEFAULT_API_ERROR_STRINGS,\n): string {\n const fallback = DEFAULT_API_ERROR_STRINGS[fallbackKey];\n return translate?.(key, undefined, { default: fallback }) ?? fallback;\n}\n\nexport function useDescribeApiError(): (error: unknown, fallback: string) => string {\n const i18n = useOptionalI18n();\n const translate = i18n?.t;\n\n return useCallback(\n (error: unknown, fallback: string) => {\n return describeApiError(error, fallback, {\n offline: resolve(translate, API_ERROR_OFFLINE_KEY, \"offline\"),\n validation: resolve(translate, API_ERROR_VALIDATION_KEY, \"validation\"),\n });\n },\n [translate],\n );\n}\n"],"mappings":"yGAkDA,SAAS,EACL,EACA,EACA,EACM,CACN,IAAM,EAAW,EAAA,0BAA0B,GAC3C,OAAO,IAAY,EAAK,IAAA,GAAW,CAAE,QAAS,CAAS,CAAC,GAAK,CACjE,CAEA,SAAgB,GAAoE,CAEhF,IAAM,EADO,EAAA,gBACK,CAAA,EAAM,EAExB,OAAA,EAAO,EAAA,YAAA,EACF,EAAgB,IACN,EAAA,iBAAiB,EAAO,EAAU,CACrC,QAAS,EAAQ,EAAW,EAAA,sBAAuB,SAAS,EAC5D,WAAY,EAAQ,EAAW,EAAA,yBAA0B,YAAY,CACzE,CAAC,EAEL,CAAC,CAAS,CACd,CACJ"}
1
+ {"version":3,"file":"use-describe-api-error.cjs","names":[],"sources":["../../src/http/use-describe-api-error.ts"],"sourcesContent":["import { useCallback } from \"react\";\n\nimport type { I18n } from \"../i18n/create-i18n\";\nimport { useOptionalI18n } from \"../i18n/I18nProvider\";\nimport type { DescribeApiErrorOptions } from \"./describe-api-error\";\nimport {\n API_ERROR_OFFLINE_KEY,\n API_ERROR_VALIDATION_KEY,\n DEFAULT_API_ERROR_STRINGS,\n describeApiError,\n} from \"./describe-api-error\";\n\n/**\n * {@link describeApiError} with its fixed sentences resolved through the active\n * `I18nProvider`.\n *\n * The funnel is not duplicated — this hook only supplies the strings and calls\n * the pure function. Which is also why both exist: the pure one runs in an\n * interceptor or a logger, where there is no React tree to read a context from,\n * and this one runs in a component without every caller passing translations\n * down by hand.\n *\n * Works with no provider at all: i18n is opt-in in this SDK, so a missing\n * provider — or a catalog that never defined `tempest.error.offline` — falls\n * back to the pt-BR default rather than crashing or printing the raw key.\n *\n * The per-call options are the pure function's, so a screen that knows the\n * backend codes it can hit passes them at the call site while the translated\n * sentences keep coming from the provider.\n *\n * @example\n * const describe = useDescribeApiError();\n * const { mutate } = useMutation({\n * mutationFn: save,\n * onError: (error) => toast(describe(error, t(\"orders.saveFailed\"))),\n * });\n *\n * @returns A stable `(error, fallback, options?) => string` function.\n */\n/**\n * Look one sentence up in the active catalog, falling back to the pt-BR default.\n *\n * The miss is the i18n layer's answer, through `t`'s `default`. It used to be\n * re-derived here by comparing the result against the key that had just been\n * passed in, which is wrong for a catalog that maps a key to itself —\n * `{ \"tempest.error.offline\": \"tempest.error.offline\" }`, which is what a\n * machine-generated or placeholder catalog produces. There the app's own\n * translation lost to the pt-BR default. Only the catalog's owner can answer\n * \"was this key defined\", and `lookup()` already knows.\n *\n * @param translate - The active catalog's `t`, if any provider is mounted.\n * @param key - The translation key to look up.\n * @param fallbackKey - Which {@link DEFAULT_API_ERROR_STRINGS} entry to use on a miss.\n * @returns The sentence to hand to `describeApiError`.\n */\nfunction resolve(\n translate: I18n[\"t\"] | undefined,\n key: string,\n fallbackKey: keyof typeof DEFAULT_API_ERROR_STRINGS,\n): string {\n const fallback = DEFAULT_API_ERROR_STRINGS[fallbackKey];\n return translate?.(key, undefined, { default: fallback }) ?? fallback;\n}\n\nexport function useDescribeApiError(): (\n error: unknown,\n fallback: string,\n options?: DescribeApiErrorOptions,\n) => string {\n const i18n = useOptionalI18n();\n const translate = i18n?.t;\n\n return useCallback(\n (error: unknown, fallback: string, options?: DescribeApiErrorOptions) => {\n return describeApiError(error, fallback, {\n offline: resolve(translate, API_ERROR_OFFLINE_KEY, \"offline\"),\n validation: resolve(translate, API_ERROR_VALIDATION_KEY, \"validation\"),\n ...options,\n });\n },\n [translate],\n );\n}\n"],"mappings":"yGAuDA,SAAS,EACL,EACA,EACA,EACM,CACN,IAAM,EAAW,EAAA,0BAA0B,GAC3C,OAAO,IAAY,EAAK,IAAA,GAAW,CAAE,QAAS,CAAS,CAAC,GAAK,CACjE,CAEA,SAAgB,GAIJ,CAER,IAAM,EADO,EAAA,gBACK,CAAA,EAAM,EAExB,OAAA,EAAO,EAAA,YAAA,EACF,EAAgB,EAAkB,IACxB,EAAA,iBAAiB,EAAO,EAAU,CACrC,QAAS,EAAQ,EAAW,EAAA,sBAAuB,SAAS,EAC5D,WAAY,EAAQ,EAAW,EAAA,yBAA0B,YAAY,EACrE,GAAG,CACP,CAAC,EAEL,CAAC,CAAS,CACd,CACJ"}
@@ -8,9 +8,10 @@ function o(e, t, r) {
8
8
  }
9
9
  function s() {
10
10
  let n = i()?.t;
11
- return a((i, a) => r(i, a, {
11
+ return a((i, a, s) => r(i, a, {
12
12
  offline: o(n, e, "offline"),
13
- validation: o(n, t, "validation")
13
+ validation: o(n, t, "validation"),
14
+ ...s
14
15
  }), [n]);
15
16
  }
16
17
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"use-describe-api-error.js","names":[],"sources":["../../src/http/use-describe-api-error.ts"],"sourcesContent":["import { useCallback } from \"react\";\n\nimport type { I18n } from \"../i18n/create-i18n\";\nimport { useOptionalI18n } from \"../i18n/I18nProvider\";\nimport {\n API_ERROR_OFFLINE_KEY,\n API_ERROR_VALIDATION_KEY,\n DEFAULT_API_ERROR_STRINGS,\n describeApiError,\n} from \"./describe-api-error\";\n\n/**\n * {@link describeApiError} with its fixed sentences resolved through the active\n * `I18nProvider`.\n *\n * The funnel is not duplicated — this hook only supplies the strings and calls\n * the pure function. Which is also why both exist: the pure one runs in an\n * interceptor or a logger, where there is no React tree to read a context from,\n * and this one runs in a component without every caller passing translations\n * down by hand.\n *\n * Works with no provider at all: i18n is opt-in in this SDK, so a missing\n * provider — or a catalog that never defined `tempest.error.offline` — falls\n * back to the pt-BR default rather than crashing or printing the raw key.\n *\n * @example\n * const describe = useDescribeApiError();\n * const { mutate } = useMutation({\n * mutationFn: save,\n * onError: (error) => toast(describe(error, t(\"orders.saveFailed\"))),\n * });\n *\n * @returns A stable `(error, fallback) => string` function.\n */\n/**\n * Look one sentence up in the active catalog, falling back to the pt-BR default.\n *\n * The miss is the i18n layer's answer, through `t`'s `default`. It used to be\n * re-derived here by comparing the result against the key that had just been\n * passed in, which is wrong for a catalog that maps a key to itself —\n * `{ \"tempest.error.offline\": \"tempest.error.offline\" }`, which is what a\n * machine-generated or placeholder catalog produces. There the app's own\n * translation lost to the pt-BR default. Only the catalog's owner can answer\n * \"was this key defined\", and `lookup()` already knows.\n *\n * @param translate - The active catalog's `t`, if any provider is mounted.\n * @param key - The translation key to look up.\n * @param fallbackKey - Which {@link DEFAULT_API_ERROR_STRINGS} entry to use on a miss.\n * @returns The sentence to hand to `describeApiError`.\n */\nfunction resolve(\n translate: I18n[\"t\"] | undefined,\n key: string,\n fallbackKey: keyof typeof DEFAULT_API_ERROR_STRINGS,\n): string {\n const fallback = DEFAULT_API_ERROR_STRINGS[fallbackKey];\n return translate?.(key, undefined, { default: fallback }) ?? fallback;\n}\n\nexport function useDescribeApiError(): (error: unknown, fallback: string) => string {\n const i18n = useOptionalI18n();\n const translate = i18n?.t;\n\n return useCallback(\n (error: unknown, fallback: string) => {\n return describeApiError(error, fallback, {\n offline: resolve(translate, API_ERROR_OFFLINE_KEY, \"offline\"),\n validation: resolve(translate, API_ERROR_VALIDATION_KEY, \"validation\"),\n });\n },\n [translate],\n );\n}\n"],"mappings":";;;;AAkDA,SAAS,EACL,GACA,GACA,GACM;CACN,IAAM,IAAW,EAA0B;CAC3C,OAAO,IAAY,GAAK,KAAA,GAAW,EAAE,SAAS,EAAS,CAAC,KAAK;AACjE;AAEA,SAAgB,IAAoE;CAEhF,IAAM,IADO,EACK,CAAA,EAAM;CAExB,OAAO,GACF,GAAgB,MACN,EAAiB,GAAO,GAAU;EACrC,SAAS,EAAQ,GAAW,GAAuB,SAAS;EAC5D,YAAY,EAAQ,GAAW,GAA0B,YAAY;CACzE,CAAC,GAEL,CAAC,CAAS,CACd;AACJ"}
1
+ {"version":3,"file":"use-describe-api-error.js","names":[],"sources":["../../src/http/use-describe-api-error.ts"],"sourcesContent":["import { useCallback } from \"react\";\n\nimport type { I18n } from \"../i18n/create-i18n\";\nimport { useOptionalI18n } from \"../i18n/I18nProvider\";\nimport type { DescribeApiErrorOptions } from \"./describe-api-error\";\nimport {\n API_ERROR_OFFLINE_KEY,\n API_ERROR_VALIDATION_KEY,\n DEFAULT_API_ERROR_STRINGS,\n describeApiError,\n} from \"./describe-api-error\";\n\n/**\n * {@link describeApiError} with its fixed sentences resolved through the active\n * `I18nProvider`.\n *\n * The funnel is not duplicated — this hook only supplies the strings and calls\n * the pure function. Which is also why both exist: the pure one runs in an\n * interceptor or a logger, where there is no React tree to read a context from,\n * and this one runs in a component without every caller passing translations\n * down by hand.\n *\n * Works with no provider at all: i18n is opt-in in this SDK, so a missing\n * provider — or a catalog that never defined `tempest.error.offline` — falls\n * back to the pt-BR default rather than crashing or printing the raw key.\n *\n * The per-call options are the pure function's, so a screen that knows the\n * backend codes it can hit passes them at the call site while the translated\n * sentences keep coming from the provider.\n *\n * @example\n * const describe = useDescribeApiError();\n * const { mutate } = useMutation({\n * mutationFn: save,\n * onError: (error) => toast(describe(error, t(\"orders.saveFailed\"))),\n * });\n *\n * @returns A stable `(error, fallback, options?) => string` function.\n */\n/**\n * Look one sentence up in the active catalog, falling back to the pt-BR default.\n *\n * The miss is the i18n layer's answer, through `t`'s `default`. It used to be\n * re-derived here by comparing the result against the key that had just been\n * passed in, which is wrong for a catalog that maps a key to itself —\n * `{ \"tempest.error.offline\": \"tempest.error.offline\" }`, which is what a\n * machine-generated or placeholder catalog produces. There the app's own\n * translation lost to the pt-BR default. Only the catalog's owner can answer\n * \"was this key defined\", and `lookup()` already knows.\n *\n * @param translate - The active catalog's `t`, if any provider is mounted.\n * @param key - The translation key to look up.\n * @param fallbackKey - Which {@link DEFAULT_API_ERROR_STRINGS} entry to use on a miss.\n * @returns The sentence to hand to `describeApiError`.\n */\nfunction resolve(\n translate: I18n[\"t\"] | undefined,\n key: string,\n fallbackKey: keyof typeof DEFAULT_API_ERROR_STRINGS,\n): string {\n const fallback = DEFAULT_API_ERROR_STRINGS[fallbackKey];\n return translate?.(key, undefined, { default: fallback }) ?? fallback;\n}\n\nexport function useDescribeApiError(): (\n error: unknown,\n fallback: string,\n options?: DescribeApiErrorOptions,\n) => string {\n const i18n = useOptionalI18n();\n const translate = i18n?.t;\n\n return useCallback(\n (error: unknown, fallback: string, options?: DescribeApiErrorOptions) => {\n return describeApiError(error, fallback, {\n offline: resolve(translate, API_ERROR_OFFLINE_KEY, \"offline\"),\n validation: resolve(translate, API_ERROR_VALIDATION_KEY, \"validation\"),\n ...options,\n });\n },\n [translate],\n );\n}\n"],"mappings":";;;;AAuDA,SAAS,EACL,GACA,GACA,GACM;CACN,IAAM,IAAW,EAA0B;CAC3C,OAAO,IAAY,GAAK,KAAA,GAAW,EAAE,SAAS,EAAS,CAAC,KAAK;AACjE;AAEA,SAAgB,IAIJ;CAER,IAAM,IADO,EACK,CAAA,EAAM;CAExB,OAAO,GACF,GAAgB,GAAkB,MACxB,EAAiB,GAAO,GAAU;EACrC,SAAS,EAAQ,GAAW,GAAuB,SAAS;EAC5D,YAAY,EAAQ,GAAW,GAA0B,YAAY;EACrE,GAAG;CACP,CAAC,GAEL,CAAC,CAAS,CACd;AACJ"}
@@ -1 +1 @@
1
- {"version":3,"file":"material-symbols.cjs","names":[],"sources":["../../src/icons/material-symbols.ts"],"sourcesContent":["import type { IconName } from \"./generated/icon-name\";\n\n/**\n * The slug `fromMaterialSymbol` falls back to when a code is unknown.\n *\n * A neutral glyph rather than nothing: a category created in an admin with a\n * code this table has not learned yet still has to draw something, or it opens a\n * hole in the grid and the bug reads as a layout problem instead of a missing\n * mapping.\n *\n * Lucide's `circle-help` is a deprecated alias of this slug. `<Icon>` resolves\n * aliases, so both render — but the bridge emits the canonical name, since a\n * value that gets persisted should not be one lucide has already renamed.\n */\nexport const MATERIAL_SYMBOL_FALLBACK: IconName = \"circle-question-mark\";\n\n/**\n * Material Symbols code → lucide slug, for backends that store `icon_code`.\n *\n * Every Python backend we write stores a category icon as a Material Symbol\n * (`build`, `format_paint`, `electrical_services`) — the vocabulary Flutter,\n * Android and the administrative seeds already speak. The SDK speaks lucide in\n * kebab-case. The two lists do not meet, and the failure is nastier than an\n * empty screen: a handful of codes collide by accident, so roughly one row in\n * ten draws the right icon and the bug reads as \"some icons went missing\".\n *\n * **Still not the full vocabulary.** Material Symbols publishes ~6100 names and\n * almost none of them will ever appear in an `icon_code` of ours, so this grows\n * on demand, one hand-written pair at a time — a map generated by name\n * heuristics gets it badly wrong, starting with `build`, which is a wrench in\n * Material Symbols and nothing to do with construction.\n *\n * **How the codes were chosen.** Two lots, merged. The first was the trades seed\n * the bridge started as. The second is the head of Material Symbols' own\n * published popularity ranking (`fonts.google.com/metadata/icons`), which is\n * measured usage rather than a guess at which names a seed will contain — every\n * key was checked to exist in that vocabulary before landing.\n *\n * **Where the two lots disagreed**, six keys, the tie went to whichever mapping\n * keeps two distinct Material Symbols names distinct in lucide: `error` →\n * `circle-alert` because `cancel` already owns `circle-x`; `payments` →\n * `banknote` because `account_balance_wallet` already owns `wallet`;\n * `receipt_long` → `receipt-text` because `receipt` already owns `receipt`;\n * `construction` → itself because `engineering` already owns `hard-hat`; and\n * `today` → `calendar` with `event` → `calendar-days`, splitting the generic\n * calendar names from the dated one.\n *\n * Every target is checked mechanically against the slug list by\n * `material-symbols.test.ts`, so a pair pointing at a name lucide does not ship\n * — or at one it has since deprecated — fails the suite instead of reaching a\n * grid. What no test can check is whether the *chosen* icon is the right\n * metaphor, which is why pairs go in by hand.\n *\n * **Approximations, deliberate and not one-to-one** (the full list lives in\n * `docs/icons.md`):\n *\n * - `plumbing` → `shower-head`, because lucide has no pipe.\n * - `build`, `handyman` and `hardware` all land on `wrench` — Material Symbols\n * distinguishes the tools, lucide does not.\n * - `pedal_bike`, `two_wheeler` and `delivery_dining` all land on `bike`.\n * - `dentistry` → `face-slightly-smiling`, since lucide ships no tooth.\n * - `iron` → `shirt` and `cleaning_services` → `spray-can`: the activity, not\n * the implement.\n * - `roofing` → `house`, the same glyph as `home`, because lucide has no roof.\n *\n * The identity pairs are here on purpose. Their names happen to match in both\n * vocabularies, so they already render today — leaving them out would send them\n * to {@link MATERIAL_SYMBOL_FALLBACK} and make this bridge a regression for\n * exactly the codes that used to work.\n *\n * @see fromMaterialSymbol — the lookup you normally call.\n */\nexport const materialToLucide: Readonly<Record<string, IconName>> = {\n ac_unit: \"snowflake\",\n account_balance: \"landmark\",\n account_balance_wallet: \"wallet\",\n account_box: \"square-user\",\n account_circle: \"circle-user\",\n add: \"plus\",\n add_circle: \"circle-plus\",\n add_circle_outline: \"circle-plus\",\n add_shopping_cart: \"shopping-cart\",\n admin_panel_settings: \"shield-user\",\n analytics: \"chart-column\",\n apps: \"layout-grid\",\n arrow_back: \"arrow-left\",\n arrow_back_ios: \"chevron-left\",\n arrow_back_ios_new: \"chevron-left\",\n arrow_downward: \"arrow-down\",\n arrow_drop_down: \"chevron-down\",\n arrow_forward: \"arrow-right\",\n arrow_forward_ios: \"chevron-right\",\n arrow_right: \"chevron-right\",\n arrow_right_alt: \"arrow-right\",\n arrow_upward: \"arrow-up\",\n article: \"newspaper\",\n assignment: \"clipboard-list\",\n attach_file: \"paperclip\",\n attach_money: \"dollar-sign\",\n autorenew: \"refresh-cw\",\n badge: \"id-card\",\n balance: \"scale\",\n bar_chart: \"chart-column\",\n battery_full: \"battery-full\",\n bluetooth: \"bluetooth\",\n bolt: \"zap\",\n bookmark: \"bookmark\",\n brush: \"brush\",\n build: \"wrench\",\n business: \"building-2\",\n cake: \"cake\",\n calculate: \"calculator\",\n calendar_month: \"calendar\",\n calendar_today: \"calendar\",\n call: \"phone\",\n campaign: \"megaphone\",\n cancel: \"circle-x\",\n car_repair: \"car\",\n carpenter: \"hammer\",\n category: \"shapes\",\n chair: \"armchair\",\n chat: \"message-circle\",\n check: \"check\",\n check_box: \"square-check\",\n check_box_outline_blank: \"square\",\n check_circle: \"circle-check\",\n check_circle_outline: \"circle-check\",\n chevron_left: \"chevron-left\",\n chevron_right: \"chevron-right\",\n child_care: \"baby\",\n circle: \"circle\",\n cleaning_services: \"spray-can\",\n clear: \"x\",\n close: \"x\",\n cloud: \"cloud\",\n code: \"code\",\n computer: \"monitor\",\n construction: \"construction\",\n content_copy: \"copy\",\n content_cut: \"scissors\",\n credit_card: \"credit-card\",\n dark_mode: \"moon\",\n dashboard: \"layout-dashboard\",\n date_range: \"calendar-range\",\n delete: \"trash-2\",\n delete_forever: \"trash-2\",\n delete_outline: \"trash-2\",\n delivery_dining: \"bike\",\n dentistry: \"face-slightly-smiling\",\n description: \"file-text\",\n directions_car: \"car\",\n done: \"check\",\n done_all: \"check-check\",\n door_front: \"door-open\",\n download: \"download\",\n east: \"arrow-right\",\n edit: \"pencil\",\n edit_note: \"pencil-line\",\n electrical_services: \"plug-zap\",\n email: \"mail\",\n emoji_events: \"trophy\",\n engineering: \"hard-hat\",\n error: \"circle-alert\",\n error_outline: \"circle-alert\",\n event: \"calendar-days\",\n expand_less: \"chevron-up\",\n expand_more: \"chevron-down\",\n explore: \"compass\",\n face: \"face-slightly-smiling\",\n fact_check: \"clipboard-check\",\n favorite: \"heart\",\n favorite_border: \"heart\",\n file_download: \"download\",\n file_upload: \"upload\",\n filter_alt: \"funnel\",\n filter_list: \"funnel\",\n fingerprint: \"fingerprint-pattern\",\n fitness_center: \"dumbbell\",\n flight: \"plane\",\n folder: \"folder\",\n format_list_bulleted: \"list\",\n format_paint: \"paint-roller\",\n forum: \"messages-square\",\n gavel: \"gavel\",\n grade: \"star\",\n grass: \"sprout\",\n grid_view: \"layout-grid\",\n group: \"users\",\n groups: \"users\",\n handyman: \"wrench\",\n hardware: \"wrench\",\n help: \"circle-question-mark\",\n help_outline: \"circle-question-mark\",\n highlight_off: \"circle-x\",\n history: \"rotate-ccw-clock\",\n home: \"house\",\n hotel: \"bed-double\",\n image: \"image\",\n info: \"info\",\n insights: \"chart-line\",\n inventory: \"package\",\n inventory_2: \"package\",\n iron: \"shirt\",\n key: \"key\",\n kitchen: \"refrigerator\",\n language: \"globe\",\n laptop: \"laptop\",\n light_mode: \"sun\",\n lightbulb: \"lightbulb\",\n link: \"link\",\n list: \"list\",\n list_alt: \"list\",\n local_bar: \"wine\",\n local_cafe: \"coffee\",\n local_florist: \"flower\",\n local_gas_station: \"fuel\",\n local_laundry_service: \"washing-machine\",\n local_offer: \"tag\",\n local_shipping: \"truck\",\n local_taxi: \"car-taxi-front\",\n location_on: \"map-pin\",\n lock: \"lock\",\n lock_open: \"lock-open\",\n login: \"log-in\",\n logout: \"log-out\",\n mail: \"mail\",\n mail_outline: \"mail\",\n manage_accounts: \"user-cog\",\n map: \"map\",\n medical_services: \"stethoscope\",\n menu: \"menu\",\n menu_book: \"book-open\",\n mic: \"mic\",\n mode_edit: \"pencil\",\n monetization_on: \"circle-dollar-sign\",\n more_horiz: \"ellipsis\",\n more_vert: \"ellipsis-vertical\",\n music_note: \"music\",\n navigate_next: \"chevron-right\",\n note_add: \"file-plus\",\n notifications: \"bell\",\n notifications_active: \"bell-ring\",\n open_in_new: \"external-link\",\n paid: \"circle-dollar-sign\",\n palette: \"palette\",\n pause: \"pause\",\n payments: \"banknote\",\n pedal_bike: \"bike\",\n people: \"users\",\n perm_identity: \"user\",\n person: \"user\",\n person_add: \"user-plus\",\n person_outline: \"user\",\n pest_control: \"bug\",\n pets: \"paw-print\",\n phone: \"phone\",\n phone_iphone: \"smartphone\",\n photo_camera: \"camera\",\n picture_as_pdf: \"file-text\",\n pie_chart: \"chart-pie\",\n place: \"map-pin\",\n play_arrow: \"play\",\n play_circle: \"circle-play\",\n play_circle_filled: \"circle-play\",\n plumbing: \"shower-head\",\n power_settings_new: \"power\",\n print: \"printer\",\n public: \"globe\",\n qr_code_scanner: \"scan-line\",\n question_answer: \"messages-square\",\n radio_button_checked: \"circle-dot\",\n radio_button_unchecked: \"circle\",\n receipt: \"receipt\",\n receipt_long: \"receipt-text\",\n refresh: \"refresh-cw\",\n remove: \"minus\",\n remove_circle_outline: \"circle-minus\",\n reorder: \"list\",\n report_problem: \"triangle-alert\",\n restart_alt: \"rotate-ccw\",\n restaurant: \"utensils\",\n roofing: \"house\",\n room: \"map-pin\",\n router: \"router\",\n save: \"save\",\n savings: \"piggy-bank\",\n schedule: \"clock\",\n school: \"graduation-cap\",\n search: \"search\",\n security: \"shield-check\",\n send: \"send\",\n settings: \"settings\",\n share: \"share-2\",\n shield: \"shield\",\n shopping_bag: \"shopping-bag\",\n shopping_cart: \"shopping-cart\",\n smartphone: \"smartphone\",\n soap: \"soap-dispenser-droplet\",\n sort: \"arrow-down-up\",\n spa: \"flower-2\",\n star: \"star\",\n star_border: \"star\",\n star_rate: \"star\",\n store: \"store\",\n storefront: \"store\",\n support_agent: \"headset\",\n sync: \"refresh-cw\",\n task_alt: \"circle-check\",\n timer: \"timer\",\n today: \"calendar\",\n toggle_on: \"toggle-right\",\n translate: \"languages\",\n trending_up: \"trending-up\",\n tune: \"sliders-horizontal\",\n tv: \"tv\",\n two_wheeler: \"bike\",\n upload: \"upload\",\n upload_file: \"upload\",\n vaccines: \"syringe\",\n verified: \"badge-check\",\n verified_user: \"shield-check\",\n videocam: \"video\",\n view_list: \"list\",\n visibility: \"eye\",\n visibility_off: \"eye-off\",\n vpn_key: \"key-round\",\n warehouse: \"warehouse\",\n warning: \"triangle-alert\",\n warning_amber: \"triangle-alert\",\n watch_later: \"clock\",\n water_drop: \"droplet\",\n wifi: \"wifi\",\n work: \"briefcase\",\n yard: \"trees\",\n};\n\n/**\n * Translate a Material Symbols code into a lucide slug, always returning one.\n *\n * Never returns `undefined`: an unknown code resolves to `fallback`, so a row\n * whose `icon_code` this table has not learned yet still renders. The input is\n * trimmed and lower-cased before lookup, because a seed written by hand is the\n * kind of source that carries stray whitespace and the odd capital.\n *\n * Nothing else in `/icons` imports this module, so an app that does not store\n * Material Symbols never pays for the table.\n *\n * @example\n * <Icon name={fromMaterialSymbol(category.icon_code)} size={20} />\n *\n * @example\n * <Icon name={fromMaterialSymbol(category.icon_code, \"folder\")} size={20} />\n *\n * @param code - A Material Symbols name, or `null`/`undefined` for a row that has none.\n * @param fallback - Slug to use when `code` is empty or unknown. Defaults to {@link MATERIAL_SYMBOL_FALLBACK}.\n * @returns A lucide slug `<Icon>` can render.\n */\nexport function fromMaterialSymbol(\n code: string | null | undefined,\n fallback: IconName = MATERIAL_SYMBOL_FALLBACK,\n): IconName {\n if (!code) return fallback;\n return materialToLucide[code.trim().toLowerCase()] ?? fallback;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"material-symbols.cjs","names":[],"sources":["../../src/icons/material-symbols.ts"],"sourcesContent":["/**\n * @tempest-limits file-lines — the file is one hand-written lookup table plus the\n * two functions that read it. Splitting a table by row count would put pairs that\n * belong together (`format_*`, `electrical_*`) in different files and make the\n * next hand-written pair a question of which file it lands in; the length is the\n * vocabulary's, not the code's.\n */\nimport type { IconName } from \"./generated/icon-name\";\n\n/**\n * The slug `fromMaterialSymbol` falls back to when a code is unknown.\n *\n * A neutral glyph rather than nothing: a category created in an admin with a\n * code this table has not learned yet still has to draw something, or it opens a\n * hole in the grid and the bug reads as a layout problem instead of a missing\n * mapping.\n *\n * Lucide's `circle-help` is a deprecated alias of this slug. `<Icon>` resolves\n * aliases, so both render — but the bridge emits the canonical name, since a\n * value that gets persisted should not be one lucide has already renamed.\n */\nexport const MATERIAL_SYMBOL_FALLBACK: IconName = \"circle-question-mark\";\n\n/**\n * Material Symbols code → lucide slug, for backends that store `icon_code`.\n *\n * Every Python backend we write stores a category icon as a Material Symbol\n * (`build`, `format_paint`, `electrical_services`) — the vocabulary Flutter,\n * Android and the administrative seeds already speak. The SDK speaks lucide in\n * kebab-case. The two lists do not meet, and the failure is nastier than an\n * empty screen: a handful of codes collide by accident, so roughly one row in\n * ten draws the right icon and the bug reads as \"some icons went missing\".\n *\n * **Still not the full vocabulary.** Material Symbols publishes ~6100 names and\n * almost none of them will ever appear in an `icon_code` of ours, so this grows\n * on demand, one hand-written pair at a time — a map generated by name\n * heuristics gets it badly wrong, starting with `build`, which is a wrench in\n * Material Symbols and nothing to do with construction.\n *\n * **How the codes were chosen.** Two lots, merged. The first was the trades seed\n * the bridge started as. The second is the head of Material Symbols' own\n * published popularity ranking (`fonts.google.com/metadata/icons`), which is\n * measured usage rather than a guess at which names a seed will contain — every\n * key was checked to exist in that vocabulary before landing.\n *\n * **Where the two lots disagreed**, six keys, the tie went to whichever mapping\n * keeps two distinct Material Symbols names distinct in lucide: `error` →\n * `circle-alert` because `cancel` already owns `circle-x`; `payments` →\n * `banknote` because `account_balance_wallet` already owns `wallet`;\n * `receipt_long` → `receipt-text` because `receipt` already owns `receipt`;\n * `construction` → itself because `engineering` already owns `hard-hat`; and\n * `today` → `calendar` with `event` → `calendar-days`, splitting the generic\n * calendar names from the dated one.\n *\n * Every target is checked mechanically against the slug list by\n * `material-symbols.test.ts`, so a pair pointing at a name lucide does not ship\n * — or at one it has since deprecated — fails the suite instead of reaching a\n * grid. What no test can check is whether the *chosen* icon is the right\n * metaphor, which is why pairs go in by hand.\n *\n * **Approximations, deliberate and not one-to-one** (the full list lives in\n * `docs/icons.md`):\n *\n * - `plumbing` → `shower-head`, because lucide has no pipe.\n * - `build`, `handyman` and `hardware` all land on `wrench` — Material Symbols\n * distinguishes the tools, lucide does not.\n * - `pedal_bike`, `two_wheeler` and `delivery_dining` all land on `bike`.\n * - `dentistry` → `face-slightly-smiling`, since lucide ships no tooth.\n * - `iron` → `shirt` and `cleaning_services` → `spray-can`: the activity, not\n * the implement.\n * - `roofing` → `house`, the same glyph as `home`, because lucide has no roof.\n *\n * The identity pairs are here on purpose. Their names happen to match in both\n * vocabularies, so they already render today — leaving them out would send them\n * to {@link MATERIAL_SYMBOL_FALLBACK} and make this bridge a regression for\n * exactly the codes that used to work.\n *\n * @see fromMaterialSymbol — the lookup you normally call.\n */\nexport const materialToLucide: Readonly<Record<string, IconName>> = {\n ac_unit: \"snowflake\",\n account_balance: \"landmark\",\n account_balance_wallet: \"wallet\",\n account_box: \"square-user\",\n account_circle: \"circle-user\",\n add: \"plus\",\n add_circle: \"circle-plus\",\n add_circle_outline: \"circle-plus\",\n add_shopping_cart: \"shopping-cart\",\n admin_panel_settings: \"shield-user\",\n analytics: \"chart-column\",\n apps: \"layout-grid\",\n arrow_back: \"arrow-left\",\n arrow_back_ios: \"chevron-left\",\n arrow_back_ios_new: \"chevron-left\",\n arrow_downward: \"arrow-down\",\n arrow_drop_down: \"chevron-down\",\n arrow_forward: \"arrow-right\",\n arrow_forward_ios: \"chevron-right\",\n arrow_right: \"chevron-right\",\n arrow_right_alt: \"arrow-right\",\n arrow_upward: \"arrow-up\",\n article: \"newspaper\",\n assignment: \"clipboard-list\",\n attach_file: \"paperclip\",\n attach_money: \"dollar-sign\",\n autorenew: \"refresh-cw\",\n badge: \"id-card\",\n balance: \"scale\",\n bar_chart: \"chart-column\",\n battery_full: \"battery-full\",\n bluetooth: \"bluetooth\",\n bolt: \"zap\",\n bookmark: \"bookmark\",\n brush: \"brush\",\n build: \"wrench\",\n business: \"building-2\",\n cake: \"cake\",\n calculate: \"calculator\",\n calendar_month: \"calendar\",\n calendar_today: \"calendar\",\n call: \"phone\",\n campaign: \"megaphone\",\n cancel: \"circle-x\",\n car_repair: \"car\",\n carpenter: \"hammer\",\n category: \"shapes\",\n chair: \"armchair\",\n chat: \"message-circle\",\n check: \"check\",\n check_box: \"square-check\",\n check_box_outline_blank: \"square\",\n check_circle: \"circle-check\",\n check_circle_outline: \"circle-check\",\n chevron_left: \"chevron-left\",\n chevron_right: \"chevron-right\",\n child_care: \"baby\",\n circle: \"circle\",\n cleaning_services: \"spray-can\",\n clear: \"x\",\n close: \"x\",\n cloud: \"cloud\",\n code: \"code\",\n computer: \"monitor\",\n construction: \"construction\",\n content_copy: \"copy\",\n content_cut: \"scissors\",\n credit_card: \"credit-card\",\n dark_mode: \"moon\",\n dashboard: \"layout-dashboard\",\n date_range: \"calendar-range\",\n delete: \"trash-2\",\n delete_forever: \"trash-2\",\n delete_outline: \"trash-2\",\n delivery_dining: \"bike\",\n dentistry: \"face-slightly-smiling\",\n description: \"file-text\",\n directions_car: \"car\",\n done: \"check\",\n done_all: \"check-check\",\n door_front: \"door-open\",\n download: \"download\",\n east: \"arrow-right\",\n edit: \"pencil\",\n edit_note: \"pencil-line\",\n electrical_services: \"plug-zap\",\n email: \"mail\",\n emoji_events: \"trophy\",\n engineering: \"hard-hat\",\n error: \"circle-alert\",\n error_outline: \"circle-alert\",\n event: \"calendar-days\",\n expand_less: \"chevron-up\",\n expand_more: \"chevron-down\",\n explore: \"compass\",\n face: \"face-slightly-smiling\",\n fact_check: \"clipboard-check\",\n favorite: \"heart\",\n favorite_border: \"heart\",\n file_download: \"download\",\n file_upload: \"upload\",\n filter_alt: \"funnel\",\n filter_list: \"funnel\",\n fingerprint: \"fingerprint-pattern\",\n fitness_center: \"dumbbell\",\n flight: \"plane\",\n folder: \"folder\",\n format_list_bulleted: \"list\",\n format_paint: \"paint-roller\",\n forum: \"messages-square\",\n gavel: \"gavel\",\n grade: \"star\",\n grass: \"sprout\",\n grid_view: \"layout-grid\",\n group: \"users\",\n groups: \"users\",\n handyman: \"wrench\",\n hardware: \"wrench\",\n help: \"circle-question-mark\",\n help_outline: \"circle-question-mark\",\n highlight_off: \"circle-x\",\n history: \"rotate-ccw-clock\",\n home: \"house\",\n hotel: \"bed-double\",\n image: \"image\",\n info: \"info\",\n insights: \"chart-line\",\n inventory: \"package\",\n inventory_2: \"package\",\n iron: \"shirt\",\n key: \"key\",\n kitchen: \"refrigerator\",\n language: \"globe\",\n laptop: \"laptop\",\n light_mode: \"sun\",\n lightbulb: \"lightbulb\",\n link: \"link\",\n list: \"list\",\n list_alt: \"list\",\n local_bar: \"wine\",\n local_cafe: \"coffee\",\n local_florist: \"flower\",\n local_gas_station: \"fuel\",\n local_laundry_service: \"washing-machine\",\n local_offer: \"tag\",\n local_shipping: \"truck\",\n local_taxi: \"car-taxi-front\",\n location_on: \"map-pin\",\n lock: \"lock\",\n lock_open: \"lock-open\",\n login: \"log-in\",\n logout: \"log-out\",\n mail: \"mail\",\n mail_outline: \"mail\",\n manage_accounts: \"user-cog\",\n map: \"map\",\n medical_services: \"stethoscope\",\n menu: \"menu\",\n menu_book: \"book-open\",\n mic: \"mic\",\n mode_edit: \"pencil\",\n monetization_on: \"circle-dollar-sign\",\n more_horiz: \"ellipsis\",\n more_vert: \"ellipsis-vertical\",\n music_note: \"music\",\n navigate_next: \"chevron-right\",\n note_add: \"file-plus\",\n notifications: \"bell\",\n notifications_active: \"bell-ring\",\n open_in_new: \"external-link\",\n paid: \"circle-dollar-sign\",\n palette: \"palette\",\n pause: \"pause\",\n payments: \"banknote\",\n pedal_bike: \"bike\",\n people: \"users\",\n perm_identity: \"user\",\n person: \"user\",\n person_add: \"user-plus\",\n person_outline: \"user\",\n pest_control: \"bug\",\n pets: \"paw-print\",\n phone: \"phone\",\n phone_iphone: \"smartphone\",\n photo_camera: \"camera\",\n picture_as_pdf: \"file-text\",\n pie_chart: \"chart-pie\",\n place: \"map-pin\",\n play_arrow: \"play\",\n play_circle: \"circle-play\",\n play_circle_filled: \"circle-play\",\n plumbing: \"shower-head\",\n power_settings_new: \"power\",\n print: \"printer\",\n public: \"globe\",\n qr_code_scanner: \"scan-line\",\n question_answer: \"messages-square\",\n radio_button_checked: \"circle-dot\",\n radio_button_unchecked: \"circle\",\n receipt: \"receipt\",\n receipt_long: \"receipt-text\",\n refresh: \"refresh-cw\",\n remove: \"minus\",\n remove_circle_outline: \"circle-minus\",\n reorder: \"list\",\n report_problem: \"triangle-alert\",\n restart_alt: \"rotate-ccw\",\n restaurant: \"utensils\",\n roofing: \"house\",\n room: \"map-pin\",\n router: \"router\",\n save: \"save\",\n savings: \"piggy-bank\",\n schedule: \"clock\",\n school: \"graduation-cap\",\n search: \"search\",\n security: \"shield-check\",\n send: \"send\",\n settings: \"settings\",\n share: \"share-2\",\n shield: \"shield\",\n shopping_bag: \"shopping-bag\",\n shopping_cart: \"shopping-cart\",\n smartphone: \"smartphone\",\n soap: \"soap-dispenser-droplet\",\n sort: \"arrow-down-up\",\n spa: \"flower-2\",\n star: \"star\",\n star_border: \"star\",\n star_rate: \"star\",\n store: \"store\",\n storefront: \"store\",\n support_agent: \"headset\",\n sync: \"refresh-cw\",\n task_alt: \"circle-check\",\n timer: \"timer\",\n today: \"calendar\",\n toggle_on: \"toggle-right\",\n translate: \"languages\",\n trending_up: \"trending-up\",\n tune: \"sliders-horizontal\",\n tv: \"tv\",\n two_wheeler: \"bike\",\n upload: \"upload\",\n upload_file: \"upload\",\n vaccines: \"syringe\",\n verified: \"badge-check\",\n verified_user: \"shield-check\",\n videocam: \"video\",\n view_list: \"list\",\n visibility: \"eye\",\n visibility_off: \"eye-off\",\n vpn_key: \"key-round\",\n warehouse: \"warehouse\",\n warning: \"triangle-alert\",\n warning_amber: \"triangle-alert\",\n watch_later: \"clock\",\n water_drop: \"droplet\",\n wifi: \"wifi\",\n work: \"briefcase\",\n yard: \"trees\",\n};\n\n/**\n * Translate a Material Symbols code into a lucide slug, always returning one.\n *\n * Never returns `undefined`: an unknown code resolves to `fallback`, so a row\n * whose `icon_code` this table has not learned yet still renders. The input is\n * trimmed and lower-cased before lookup, because a seed written by hand is the\n * kind of source that carries stray whitespace and the odd capital.\n *\n * Nothing else in `/icons` imports this module, so an app that does not store\n * Material Symbols never pays for the table.\n *\n * @example\n * <Icon name={fromMaterialSymbol(category.icon_code)} size={20} />\n *\n * @example\n * <Icon name={fromMaterialSymbol(category.icon_code, \"folder\")} size={20} />\n *\n * @param code - A Material Symbols name, or `null`/`undefined` for a row that has none.\n * @param fallback - Slug to use when `code` is empty or unknown. Defaults to {@link MATERIAL_SYMBOL_FALLBACK}.\n * @returns A lucide slug `<Icon>` can render.\n */\nexport function fromMaterialSymbol(\n code: string | null | undefined,\n fallback: IconName = MATERIAL_SYMBOL_FALLBACK,\n): IconName {\n if (!code) return fallback;\n return materialToLucide[code.trim().toLowerCase()] ?? fallback;\n}\n"],"mappings":"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"}