@tpzdsp/next-toolkit 1.5.0 → 1.7.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.
@@ -1,6 +1,8 @@
1
1
  'use client';
2
2
 
3
- import { useState, useEffect, type ReactNode, useRef, useMemo } from 'react';
3
+ import { useState, useEffect, type ReactNode, useRef, useMemo, useCallback } from 'react';
4
+
5
+ import { KeyboardKeys } from '../../utils';
4
6
 
5
7
  type Position = 'center-left' | 'center-right' | 'center-top' | 'center-bottom';
6
8
 
@@ -11,6 +13,9 @@ export type SlidingPanelProps = {
11
13
  defaultOpen?: boolean;
12
14
  };
13
15
 
16
+ const TRAP_SELECTORS =
17
+ 'a[href]:not([aria-hidden="true"]):not([hidden]), button:not([disabled]):not([aria-hidden="true"]):not([hidden]), input:not([disabled]):not([aria-hidden="true"]):not([hidden]), textarea:not([disabled]):not([aria-hidden="true"]):not([hidden]), select:not([disabled]):not([aria-hidden="true"]):not([hidden]), [tabindex]:not([tabindex="-1"]):not([aria-hidden="true"]):not([hidden])';
18
+
14
19
  export const SlidingPanel = ({
15
20
  children,
16
21
  tabLabel = 'Open',
@@ -20,67 +25,16 @@ export const SlidingPanel = ({
20
25
  const [isVisible, setIsVisible] = useState(defaultOpen);
21
26
  const [panelDimensions, setPanelDimensions] = useState({ width: 0, height: 0 });
22
27
  const panelRef = useRef<HTMLDivElement>(null);
23
- const toggleBtnRef = useRef<HTMLButtonElement>(null);
24
-
25
- // Focus trap: cycle focus within panel when open
26
- useEffect(() => {
27
- if (!isVisible || !panelRef.current) {
28
- return;
29
- }
30
-
31
- const panel = panelRef.current;
32
- const focusableSelectors = [
33
- 'a[href]',
34
- 'button:not([disabled])',
35
- 'textarea:not([disabled])',
36
- 'input:not([disabled])',
37
- 'select:not([disabled])',
38
- '[tabindex]:not([tabindex="-1"])',
39
- ];
40
- const getFocusable = () =>
41
- Array.from(panel.querySelectorAll<HTMLElement>(focusableSelectors.join(','))).filter(
42
- (el) => !el.hasAttribute('disabled') && !el.getAttribute('aria-hidden'),
43
- );
44
-
45
- const handleKeyDown = (e: KeyboardEvent) => {
46
- if (e.key === 'Escape') {
47
- setIsVisible(false);
48
- // Return focus to toggle button after closing
49
- toggleBtnRef.current?.focus();
50
- }
51
-
52
- if (e.key === 'Tab') {
53
- const focusableEls = getFocusable();
54
-
55
- if (focusableEls.length === 0) {
56
- return;
57
- }
58
-
59
- const first = focusableEls[0];
60
- const last = focusableEls[focusableEls.length - 1];
61
-
62
- if (!e.shiftKey && document.activeElement === last) {
63
- e.preventDefault();
64
- first.focus();
65
- } else if (e.shiftKey && document.activeElement === first) {
66
- e.preventDefault();
67
- last.focus();
68
- }
69
- }
70
- };
71
-
72
- panel.addEventListener('keydown', handleKeyDown);
73
- // Focus the first focusable element in the panel
74
- const focusableEls = getFocusable();
28
+ const triggerRef = useRef<HTMLElement | null>(null); // store previously focused element
75
29
 
76
- if (focusableEls.length) {
77
- focusableEls[0].focus();
78
- }
30
+ const openPanel = () => {
31
+ triggerRef.current = document.activeElement as HTMLElement;
32
+ setIsVisible(true);
33
+ };
79
34
 
80
- return () => {
81
- panel.removeEventListener('keydown', handleKeyDown);
82
- };
83
- }, [isVisible]);
35
+ const closePanel = useCallback(() => {
36
+ setIsVisible(false);
37
+ }, []);
84
38
 
85
39
  // Measure panel dimensions when visible
86
40
  useEffect(() => {
@@ -112,44 +66,81 @@ export const SlidingPanel = ({
112
66
  }
113
67
  }, [isVisible, panelDimensions.height, panelDimensions.width]);
114
68
 
69
+ // move focus into panel when opened, restore focus to correct element when closed
70
+ useEffect(() => {
71
+ if (isVisible && panelRef.current) {
72
+ const els = Array.from(panelRef.current.querySelectorAll<HTMLElement>(TRAP_SELECTORS));
73
+
74
+ if (els.length > 0) {
75
+ els[0].focus();
76
+ } else {
77
+ panelRef.current.focus();
78
+ }
79
+ } else if (!isVisible && triggerRef.current) {
80
+ triggerRef.current.focus();
81
+ }
82
+ }, [isVisible]);
83
+
84
+ // trap focus while open
115
85
  useEffect(() => {
116
- if (!panelRef.current) {
86
+ if (!isVisible || !panelRef.current) {
117
87
  return;
118
88
  }
119
89
 
120
- const focusableSelectors = [
121
- 'a[href]',
122
- 'button:not([disabled])',
123
- 'textarea:not([disabled])',
124
- 'input:not([disabled])',
125
- 'select:not([disabled])',
126
- '[tabindex]:not([tabindex="-1"])',
127
- ];
128
- const focusableEls = Array.from(
129
- panelRef.current.querySelectorAll<HTMLElement>(focusableSelectors.join(',')),
130
- );
131
-
132
- if (!isVisible) {
133
- // Remove from tab order
134
- focusableEls.forEach((el) => {
135
- el.dataset.prevTabIndex = el.getAttribute('tabindex') ?? '';
136
- el.setAttribute('tabindex', '-1');
137
- });
138
- } else {
139
- // Restore previous tabIndex
140
- focusableEls.forEach((el) => {
141
- if (el.dataset.prevTabIndex !== undefined) {
142
- if (el.dataset.prevTabIndex === '') {
143
- el.removeAttribute('tabindex');
144
- } else {
145
- el.setAttribute('tabindex', el.dataset.prevTabIndex);
146
- }
90
+ const panel = panelRef.current;
147
91
 
148
- delete el.dataset.prevTabIndex;
149
- }
150
- });
151
- }
152
- }, [isVisible]);
92
+ const moveFocus = (direction: 'next' | 'prev') => {
93
+ const els = Array.from(panel.querySelectorAll<HTMLElement>(TRAP_SELECTORS));
94
+
95
+ if (!els.length) {
96
+ panel.focus();
97
+
98
+ return true;
99
+ }
100
+
101
+ const first = els[0];
102
+ const last = els[els.length - 1];
103
+ const active = document.activeElement;
104
+
105
+ if (direction === 'next' && active === last) {
106
+ first.focus();
107
+
108
+ return true;
109
+ }
110
+
111
+ if (direction === 'prev' && active === first) {
112
+ last.focus();
113
+
114
+ return true;
115
+ }
116
+
117
+ return false;
118
+ };
119
+
120
+ const handleKey = (event: KeyboardEvent) => {
121
+ if (event.key === KeyboardKeys.Escape) {
122
+ event.preventDefault();
123
+ closePanel();
124
+
125
+ return;
126
+ }
127
+
128
+ if (event.key !== KeyboardKeys.Tab) {
129
+ return;
130
+ }
131
+
132
+ // only if focus was moved do we prevent the default behaviour
133
+ if (moveFocus(event.shiftKey ? 'prev' : 'next')) {
134
+ event.preventDefault();
135
+ }
136
+ };
137
+
138
+ panel.addEventListener('keydown', handleKey);
139
+
140
+ return () => {
141
+ panel.removeEventListener('keydown', handleKey);
142
+ };
143
+ }, [closePanel, isVisible]);
153
144
 
154
145
  const panelBase =
155
146
  'absolute bg-white shadow-lg p-4 flex flex-col transition-transform duration-300 ease-in-out overflow-auto z-30';
@@ -211,12 +202,11 @@ export const SlidingPanel = ({
211
202
  }, [isVisible, panelDimensions.height, panelDimensions.width, position]);
212
203
 
213
204
  return (
214
- <div className="absolute inset-0 overflow-hidden pointer-events-none z-30">
205
+ <div className="absolute inset-0 z-30 overflow-hidden pointer-events-none sliding-panel">
215
206
  <button
216
- ref={toggleBtnRef}
217
207
  className={`pointer-events-auto ${buttonPosition} bg-gray-700 text-white z-40 focus-yellow`}
218
208
  style={getButtonStyle}
219
- onClick={() => setIsVisible((prev) => !prev)}
209
+ onClick={() => (isVisible ? closePanel() : openPanel())}
220
210
  aria-expanded={isVisible}
221
211
  aria-controls="sliding-panel"
222
212
  >
@@ -225,9 +215,11 @@ export const SlidingPanel = ({
225
215
 
226
216
  <div
227
217
  ref={panelRef}
218
+ tabIndex={-1}
228
219
  id="sliding-panel"
229
220
  className={`${panelBase} ${panelLayout} pointer-events-auto`}
230
221
  aria-hidden={!isVisible}
222
+ inert={!isVisible}
231
223
  >
232
224
  <div className="mt-4">{children}</div>
233
225
  </div>
@@ -4,6 +4,8 @@ import { type KeyboardEvent, useCallback, useEffect, useState } from 'react';
4
4
 
5
5
  import { LuArrowUp } from 'react-icons/lu';
6
6
 
7
+ import { KeyboardKeys } from '../../utils';
8
+
7
9
  export type BackToTopProps = {
8
10
  /** Scroll threshold in pixels before button appears */
9
11
  threshold?: number;
@@ -87,7 +89,7 @@ export const BackToTop = ({
87
89
  // Handle keyboard interaction
88
90
  const handleKeyDown = useCallback(
89
91
  (event: KeyboardEvent<HTMLButtonElement>) => {
90
- if (event.key === 'Enter' || event.key === ' ') {
92
+ if (event.key === KeyboardKeys.Enter || event.key === KeyboardKeys.Space) {
91
93
  event.preventDefault();
92
94
  scrollToTop();
93
95
  }
@@ -11,6 +11,7 @@ import {
11
11
  } from 'react';
12
12
 
13
13
  import type { ExtendProps } from '../../types/utils';
14
+ import { KeyboardKeys } from '../../utils';
14
15
 
15
16
  /**
16
17
  * The current state of the menu
@@ -117,7 +118,11 @@ export const useDropdownMenu = (itemCount: number): DropdownMenuHook => {
117
118
  useEffect(() => {
118
119
  // handles exiting the menu when the `Escape` key is pressed
119
120
  const escapeExitMenuHandler = (event: KeyboardEvent) => {
120
- if (event.key === 'Escape' && document.activeElement && isMenuItem(document.activeElement)) {
121
+ if (
122
+ event.key === KeyboardKeys.Escape &&
123
+ document.activeElement &&
124
+ isMenuItem(document.activeElement)
125
+ ) {
121
126
  event.preventDefault();
122
127
 
123
128
  setIsOpen(false);
@@ -166,14 +171,14 @@ export const useDropdownMenu = (itemCount: number): DropdownMenuHook => {
166
171
  onPointerUp: () => setIsOpen(!isOpen),
167
172
  onKeyDown: (event) => {
168
173
  // space and enter act more like clicking and can toggle the menu open/closed
169
- if (event.key === ' ' || event.key === 'Enter') {
174
+ if (event.key === KeyboardKeys.Space || event.key === KeyboardKeys.Enter) {
170
175
  event.preventDefault();
171
176
 
172
177
  setIsOpen(!isOpen);
173
178
  }
174
179
 
175
180
  // the down arrow should open the menu
176
- if (event.key === 'ArrowDown') {
181
+ if (event.key === KeyboardKeys.ArrowDown) {
177
182
  event.preventDefault();
178
183
 
179
184
  if (!isOpen) {
@@ -208,7 +213,7 @@ export const useDropdownMenu = (itemCount: number): DropdownMenuHook => {
208
213
  let focusIndex = currentFocusIndex.current;
209
214
 
210
215
  // close the menu if you "select" an item
211
- if (event.key === ' ' || event.key === 'Enter') {
216
+ if (event.key === KeyboardKeys.Space || event.key === KeyboardKeys.Enter) {
212
217
  event.preventDefault();
213
218
 
214
219
  // we just called `preventDefault` so we need to manually click it otherwise nothing will happen
@@ -220,11 +225,11 @@ export const useDropdownMenu = (itemCount: number): DropdownMenuHook => {
220
225
 
221
226
  if (focusIndex !== null) {
222
227
  // arrow keys should cycle through each item in the menu
223
- if (event.key === 'ArrowDown') {
228
+ if (event.key === KeyboardKeys.ArrowDown) {
224
229
  event.preventDefault();
225
230
 
226
231
  focusIndex += 1;
227
- } else if (event.key === 'ArrowUp') {
232
+ } else if (event.key === KeyboardKeys.ArrowUp) {
228
233
  event.preventDefault();
229
234
 
230
235
  focusIndex -= 1;
@@ -1,5 +1,6 @@
1
1
  'use client';
2
2
 
3
+ import { KeyboardKeys } from '../../utils';
3
4
  import { Link } from '../link/Link';
4
5
 
5
6
  export type SkipLinkProps = {
@@ -21,12 +22,12 @@ export const SkipLink = ({ mainContentId = 'main-content' }: SkipLinkProps) => {
21
22
  return (
22
23
  <nav aria-label="Skip navigation">
23
24
  <Link
24
- className="bg-focus focus:relative focus:top-0 w-full absolute -top-full text-black
25
- visited:text-black hover:text-black p-3 skip-link"
25
+ className="absolute w-full p-3 text-black bg-focus focus:relative focus:top-0 -top-full
26
+ visited:text-black hover:text-black skip-link"
26
27
  href={`#${mainContentId}`}
27
28
  onClick={handleActivate}
28
29
  onKeyDown={(event) => {
29
- if (event.key === 'Enter' || event.key === ' ') {
30
+ if (event.key === KeyboardKeys.Enter || event.key === KeyboardKeys.Space) {
30
31
  event.preventDefault(); // Prevent default scroll/jump
31
32
  handleActivate();
32
33
  }
@@ -1,33 +1,95 @@
1
1
  /* eslint-disable no-restricted-syntax */
2
2
 
3
- import { Http } from '../utils/http';
3
+ import z from 'zod/v4';
4
4
 
5
- export class ApiError extends Error {
5
+ import { HttpStatus, HttpStatusText } from '../http';
6
+
7
+ /**
8
+ * Schema defining the JSON shape of error responses returned by a server. (The proxy is where
9
+ * the error is used the most, though it isn't unique to the proxy and may be used elsewhere).
10
+ *
11
+ * Note: This schema intentionally excludes the HTTP `status` field,
12
+ * as that value is carried in the HTTP response itself (via the status code)
13
+ * and in the in-memory {@link ApiError} class for internal use.
14
+ */
15
+ export const ApiErrorSchema = z.object({
16
+ message: z.string(),
17
+ details: z.string().nullable(),
18
+ });
19
+
20
+ export type ApiErrorSchemaOutput = z.output<typeof ApiErrorSchema>;
21
+
22
+ /**
23
+ * Represents an error that can be returned by an API or proxy endpoint.
24
+ *
25
+ * Use this class to consistently capture errors along with an HTTP status
26
+ * code and optional detailed message. This allows:
27
+ * - Returning structured errors from API handlers or internal proxies.
28
+ * - Setting the status code for the response when an error has occurred.
29
+ * - Including additional context in `details` for debugging.
30
+ *
31
+ * Note:
32
+ * - Although it contains the same members, this class is separate from {@link ApiErrorSchema}.
33
+ * The schema defines the serialized JSON payload returned to clients, while this class
34
+ * *also* includes meta information of the response, such as the HTTP status code.
35
+ * The status code is typically only used internally inside the proxy (to forward status codes),
36
+ * but it could be accessed if a request 1 made with `throw` set to `false.`
37
+ */
38
+ export class ApiError extends Error implements z.output<typeof ApiErrorSchema> {
39
+ public readonly details: string | null;
40
+ public readonly digest: string;
6
41
  public readonly status: number;
7
- public readonly code?: string;
8
- public readonly details?: unknown;
42
+ // `true` if this class was reconstructed on the client from an response from the proxy
43
+ private readonly rehydrated: boolean;
9
44
 
10
- constructor(message: string, status: number, code?: string, details?: unknown) {
45
+ constructor(
46
+ message: string,
47
+ status: number,
48
+ details?: string | null,
49
+ options?: { rehydrated?: boolean },
50
+ ) {
11
51
  super(message);
52
+
12
53
  this.name = 'ApiError';
13
54
  this.status = status;
14
- this.code = code;
15
- this.details = details;
55
+ this.details = details ?? null;
56
+ this.digest = crypto.randomUUID();
57
+ this.rehydrated = options?.rehydrated ?? false;
16
58
 
17
59
  // Maintains proper stack trace for where our error was thrown (only available on V8)
18
60
  if (Error.captureStackTrace) {
19
61
  Error.captureStackTrace(this, ApiError);
20
62
  }
63
+
64
+ this.logError();
65
+ }
66
+
67
+ private logError() {
68
+ const trimmedStack = this.stack
69
+ ?.split('\n')
70
+ // Filter out noisy frames but keep traces from toolkit for debugging
71
+ .filter((line) => !line.includes('node_modules') || line.includes('next-toolkit'))
72
+ .join('\n');
73
+
74
+ const prefix =
75
+ !this.rehydrated && typeof window !== 'undefined' ? 'Client API Error' : 'Server API Error';
76
+
77
+ console.error(
78
+ // eslint-disable-next-line sonarjs/no-nested-template-literals
79
+ `[${prefix}]: ${this.message}${this.details ? ` (${this.details})` : ''}. Digest: ${
80
+ this.digest
81
+ }\n${!this.rehydrated ? (trimmedStack ?? 'aa') : 'bb'}`,
82
+ );
21
83
  }
22
84
 
23
85
  // Helper method to check if it's a client error (4xx)
24
86
  get isClientError(): boolean {
25
- return this.status >= Http.BadRequest && this.status < Http.InternalServerError;
87
+ return this.status >= HttpStatus.BadRequest && this.status < HttpStatus.InternalServerError;
26
88
  }
27
89
 
28
90
  // Helper method to check if it's a server error (5xx)
29
91
  get isServerError(): boolean {
30
- return this.status >= Http.InternalServerError;
92
+ return this.status >= HttpStatus.InternalServerError;
31
93
  }
32
94
 
33
95
  // Convert to a plain object for JSON serialization
@@ -36,36 +98,48 @@ export class ApiError extends Error {
36
98
  name: this.name,
37
99
  message: this.message,
38
100
  status: this.status,
39
- code: this.code,
40
101
  details: this.details,
41
102
  };
42
103
  }
43
104
 
44
105
  // Static factory methods for common error types
45
- static badRequest(message: string, details?: unknown): ApiError {
46
- return new ApiError(message, Http.BadRequest, 'BAD_REQUEST', details);
106
+ static badRequest(details?: string): ApiError {
107
+ return new ApiError(HttpStatusText.BadRequest, HttpStatus.BadRequest, details);
108
+ }
109
+
110
+ static notFound(details?: string): ApiError {
111
+ return new ApiError(HttpStatusText.NotFound, HttpStatus.NotFound, details);
47
112
  }
48
113
 
49
- static notFound(message = 'Resource not found'): ApiError {
50
- return new ApiError(message, Http.NotFound, 'NOT_FOUND');
114
+ static unauthorized(details?: string): ApiError {
115
+ return new ApiError(HttpStatusText.Unauthorized, HttpStatus.Unauthorized, details);
51
116
  }
52
117
 
53
- static unauthorized(message = 'Unauthorized'): ApiError {
54
- return new ApiError(message, Http.Unauthorized, 'UNAUTHORIZED');
118
+ static notAllowed(details?: string): ApiError {
119
+ return new ApiError(HttpStatusText.NotAllowed, HttpStatus.NotAllowed, details);
55
120
  }
56
121
 
57
- static forbidden(message = 'Forbidden'): ApiError {
58
- return new ApiError(message, Http.Forbidden, 'FORBIDDEN');
122
+ static notImplemented(details?: string): ApiError {
123
+ return new ApiError(HttpStatusText.NotImplemented, HttpStatus.NotImplemented, details);
59
124
  }
60
125
 
61
- static internalServerError(message = 'Internal server error'): ApiError {
62
- return new ApiError(message, Http.InternalServerError, 'INTERNAL_SERVER_ERROR');
126
+ static forbidden(details?: string): ApiError {
127
+ return new ApiError(HttpStatusText.Forbidden, HttpStatus.Forbidden, details);
128
+ }
129
+
130
+ static unprocessableContent(details?: string): ApiError {
131
+ return new ApiError(
132
+ HttpStatusText.UnprocessableContent,
133
+ HttpStatus.UnprocessableContent,
134
+ details,
135
+ );
63
136
  }
64
137
 
65
- static fromResponse(response: Response, message?: string): ApiError {
138
+ static internalServerError(details?: string): ApiError {
66
139
  return new ApiError(
67
- message ?? `HTTP ${response.status}: ${response.statusText}`,
68
- response.status,
140
+ HttpStatusText.InternalServerError,
141
+ HttpStatus.InternalServerError,
142
+ details,
69
143
  );
70
144
  }
71
145
  }
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Use the {@link MimeTypes} type for a union type containing these mime types.
3
+ */
4
+ export const MimeType = {
5
+ Json: 'application/json',
6
+ JsonLd: 'application/ld+json',
7
+ GeoJson: 'application/geo+json',
8
+ Png: 'image/png',
9
+ XJsonLines: 'application/x-jsonlines',
10
+ Csv: 'text/csv',
11
+ Form: 'application/x-www-form-urlencoded',
12
+ Text: 'text/plain',
13
+ } as const;
14
+
15
+ /**
16
+ * Use the {@link MimeType} object for named constants of each mime type.
17
+ */
18
+ export type MimeTypes = (typeof MimeType)[keyof typeof MimeType];
19
+
20
+ /**
21
+ * Returns `true` if the mime type is a non-streamed JSON mime type (I.E. not JSON X-Lines).
22
+ */
23
+ export const isJsonMimeType = (mime: MimeTypes | string | null): boolean => {
24
+ return ([MimeType.Json, MimeType.GeoJson, MimeType.JsonLd] as string[]).includes(mime ?? '');
25
+ };
26
+
27
+ /**
28
+ * Returns `true` if the mime type is a plain-text mime type.
29
+ */
30
+ export const isTextMimeType = (mime: MimeTypes | string | null): boolean => {
31
+ return ([MimeType.Text, MimeType.Csv] as string[]).includes(mime ?? '');
32
+ };
33
+
34
+ /**
35
+ * Use the {@link HttpStatuses} type for a union type containing these status codes.
36
+ *
37
+ * Use the {@link HttpStatusText} object for the status text represented by each code.
38
+ */
39
+ export const HttpStatus = {
40
+ Ok: 200,
41
+ Created: 201,
42
+ NoContent: 204,
43
+ BadRequest: 400,
44
+ Unauthorized: 401,
45
+ Forbidden: 403,
46
+ NotFound: 404,
47
+ NotAllowed: 405,
48
+ UnprocessableContent: 422,
49
+ InternalServerError: 500,
50
+ NotImplemented: 501,
51
+ } as const;
52
+
53
+ /**
54
+ * Use the {@link HttpStatus} object for named constants of each status code.
55
+ */
56
+ export type HttpStatuses = (typeof HttpStatus)[keyof typeof HttpStatus];
57
+
58
+ /**
59
+ * Status texts for each status code.
60
+ */
61
+ export const HttpStatusText = {
62
+ Ok: 'OK',
63
+ Created: 'Created',
64
+ NoContent: 'No Content',
65
+ BadRequest: 'Bad Request',
66
+ Unauthorized: 'Unauthorized',
67
+ Forbidden: 'Forbidden',
68
+ NotFound: 'Not Found',
69
+ NotAllowed: 'Method Not Allowed',
70
+ UnprocessableContent: 'Unprocessable Content',
71
+ InternalServerError: 'Internal Server Error',
72
+ NotImplemented: 'Not Implemented',
73
+ } as const;
74
+
75
+ /**
76
+ * Use the {@link HttpMethods} type for a union type containing these method verbs.
77
+ */
78
+ export const HttpMethod = {
79
+ Get: 'get',
80
+ Post: 'post',
81
+ Put: 'put',
82
+ Patch: 'patch',
83
+ Delete: 'delete',
84
+ } as const;
85
+
86
+ /**
87
+ * Use the {@link HttpMethod} object for named constants of each method verb.
88
+ */
89
+ export type HttpMethods = (typeof HttpMethod)[keyof typeof HttpMethod];
90
+
91
+ /**
92
+ * Use the {@link Headers} type for a union type containing these headers.
93
+ */
94
+ export const Header = {
95
+ ContentType: 'Content-Type',
96
+ ContentDisposition: 'Content-Disposition',
97
+ Accept: 'Accept',
98
+ AcceptCrs: 'Accept-Crs',
99
+ CsvHeader: 'CSV-Header',
100
+ XTotalItems: 'X-Total-Items',
101
+ XRequestId: 'X-Request-ID',
102
+ } as const;
103
+
104
+ /**
105
+ * Use the {@link Header} object for named constants of each header.
106
+ */
107
+ export type Headers = (typeof Header)[keyof typeof Header];
108
+
109
+ // Special types used for downloading files via a `POST` request
110
+ export const SPECIAL_FORM_DATA_TYPE = '_type';
111
+ export const SPECIAL_FORM_DOWNLOAD_POST = 'file-download';