@eztrak/shared 0.3.0 → 0.4.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.
- package/README.md +186 -73
- package/dist/chunk-5L6SERQP.mjs +11 -0
- package/dist/chunk-5L6SERQP.mjs.map +1 -0
- package/dist/{chunk-POG3P25K.mjs → chunk-AR7LUJY3.mjs} +2 -10
- package/dist/chunk-AR7LUJY3.mjs.map +1 -0
- package/dist/chunk-SNADLOOV.mjs +213 -0
- package/dist/chunk-SNADLOOV.mjs.map +1 -0
- package/dist/components/index.d.mts +40 -0
- package/dist/components/index.d.ts +40 -0
- package/dist/components/index.js +248 -0
- package/dist/components/index.js.map +1 -0
- package/dist/components/index.mjs +8 -0
- package/dist/components/index.mjs.map +1 -0
- package/dist/components/tabs/styles.css +205 -0
- package/dist/index.d.mts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +204 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +8 -2
- package/dist/utils/index.js +3 -3
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/index.mjs +4 -2
- package/package.json +71 -65
- package/dist/chunk-POG3P25K.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -1,73 +1,186 @@
|
|
|
1
|
-
# @eztrak/shared
|
|
2
|
-
|
|
3
|
-
Shared utilities, hooks, and UI components for Eztrak applications.
|
|
4
|
-
|
|
5
|
-
## Install
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
npm install @eztrak/shared
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
Peer dependencies (hooks + `handleApiError`):
|
|
12
|
-
|
|
13
|
-
```bash
|
|
14
|
-
npm install react react-dom react-router-dom react-
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
Your app must
|
|
18
|
-
|
|
19
|
-
## Usage
|
|
20
|
-
|
|
21
|
-
### Utils
|
|
22
|
-
|
|
23
|
-
```js
|
|
24
|
-
import { cn, formatDate, getApiError, isEmpty } from "@eztrak/shared/utils";
|
|
25
|
-
|
|
26
|
-
<div className={cn("px-4 py-2", isActive && "bg-blue-500", className)} />
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
`cn` combines [`clsx`](https://github.com/lukeed/clsx) with [`tailwind-merge`](https://github.com/dcastil/tailwind-merge).
|
|
30
|
-
|
|
31
|
-
RTK Query errors (toast or SweetAlert — no extra wiring):
|
|
32
|
-
|
|
33
|
-
```js
|
|
34
|
-
import { handleApiError } from "@eztrak/shared/utils";
|
|
35
|
-
|
|
36
|
-
// Toasts each validation message (or a single generic error)
|
|
37
|
-
handleApiError(error, { fallbackMessage: "Save failed" });
|
|
38
|
-
|
|
39
|
-
// SweetAlert list for validation errors
|
|
40
|
-
handleApiError(error, { showAlert: true });
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
### Hooks
|
|
44
|
-
|
|
45
|
-
```js
|
|
46
|
-
import { usePaginationUrlSync, useGridHeight } from "@eztrak/shared/hooks";
|
|
47
|
-
|
|
48
|
-
const gridHeight = useGridHeight({ offset: 390 });
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
###
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
1
|
+
# @eztrak/shared
|
|
2
|
+
|
|
3
|
+
Shared utilities, hooks, and UI components for Eztrak applications.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @eztrak/shared
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Peer dependencies (hooks + `handleApiError` + components):
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install react react-dom react-router-dom react-hot-toast sweetalert2
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Your app must render `<Toaster />` from react-hot-toast (e.g. in your root layout).
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
### Utils
|
|
22
|
+
|
|
23
|
+
```js
|
|
24
|
+
import { cn, formatDate, getApiError, isEmpty } from "@eztrak/shared/utils";
|
|
25
|
+
|
|
26
|
+
<div className={cn("px-4 py-2", isActive && "bg-blue-500", className)} />
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
`cn` combines [`clsx`](https://github.com/lukeed/clsx) with [`tailwind-merge`](https://github.com/dcastil/tailwind-merge).
|
|
30
|
+
|
|
31
|
+
RTK Query errors (toast or SweetAlert — no extra wiring):
|
|
32
|
+
|
|
33
|
+
```js
|
|
34
|
+
import { handleApiError } from "@eztrak/shared/utils";
|
|
35
|
+
|
|
36
|
+
// Toasts each validation message (or a single generic error)
|
|
37
|
+
handleApiError(error, { fallbackMessage: "Save failed" });
|
|
38
|
+
|
|
39
|
+
// SweetAlert list for validation errors
|
|
40
|
+
handleApiError(error, { showAlert: true });
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Hooks
|
|
44
|
+
|
|
45
|
+
```js
|
|
46
|
+
import { usePaginationUrlSync, useGridHeight } from "@eztrak/shared/hooks";
|
|
47
|
+
|
|
48
|
+
const gridHeight = useGridHeight({ offset: 390 });
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Components — EztrakTabs
|
|
52
|
+
|
|
53
|
+
Import styles once in your app entry or layout:
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
import "@eztrak/shared/components/tabs.css";
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
#### With panels
|
|
60
|
+
|
|
61
|
+
```tsx
|
|
62
|
+
import { EztrakTabs } from "@eztrak/shared/components";
|
|
63
|
+
import "@eztrak/shared/components/tabs.css";
|
|
64
|
+
|
|
65
|
+
const tabs = [
|
|
66
|
+
{ id: "daily", label: "Daily Report", content: <DailyReport /> },
|
|
67
|
+
{ id: "cumulative", label: "Cumulative", content: <CumulativeReport /> },
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
<EztrakTabs
|
|
71
|
+
tabs={tabs}
|
|
72
|
+
activeTab={activeView}
|
|
73
|
+
onTabChange={setActiveView}
|
|
74
|
+
/>
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
#### Nav only (content rendered elsewhere)
|
|
78
|
+
|
|
79
|
+
```tsx
|
|
80
|
+
<EztrakTabs
|
|
81
|
+
tabs={[
|
|
82
|
+
{ id: "grid", label: "Grid", icon: <GridIcon /> },
|
|
83
|
+
{ id: "table", label: "Table" },
|
|
84
|
+
]}
|
|
85
|
+
activeTab={view}
|
|
86
|
+
onTabChange={handleViewChange}
|
|
87
|
+
showPanels={false}
|
|
88
|
+
/>
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
#### Controlled with URL (react-router)
|
|
92
|
+
|
|
93
|
+
```tsx
|
|
94
|
+
const [searchParams, setSearchParams] = useSearchParams();
|
|
95
|
+
const activeTab = searchParams.get("view") ?? "dashboard";
|
|
96
|
+
|
|
97
|
+
<EztrakTabs
|
|
98
|
+
tabs={tabs}
|
|
99
|
+
activeTab={activeTab}
|
|
100
|
+
onTabChange={(id) => {
|
|
101
|
+
setSearchParams((prev) => {
|
|
102
|
+
prev.set("view", id);
|
|
103
|
+
return prev;
|
|
104
|
+
});
|
|
105
|
+
}}
|
|
106
|
+
showPanels={false}
|
|
107
|
+
/>
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
#### Keep heavy panels mounted
|
|
111
|
+
|
|
112
|
+
```tsx
|
|
113
|
+
<EztrakTabs tabs={tabs} activeTab={section} onTabChange={setSection} keepMounted />
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
#### Theming — CSS variables
|
|
117
|
+
|
|
118
|
+
```css
|
|
119
|
+
:root {
|
|
120
|
+
--eztrak-tabs-accent: #2563eb;
|
|
121
|
+
--eztrak-tabs-inactive-bg: #f3f4f6;
|
|
122
|
+
--eztrak-tabs-active-bg: #ffffff;
|
|
123
|
+
--eztrak-tabs-label-inactive: #9ca3af;
|
|
124
|
+
--eztrak-tabs-label-active: #1f2937;
|
|
125
|
+
--eztrak-tabs-radius: 8px;
|
|
126
|
+
--eztrak-tabs-gap: 12px;
|
|
127
|
+
--eztrak-tabs-padding: 12px 20px;
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
#### Theming — Tailwind `classNames`
|
|
132
|
+
|
|
133
|
+
```tsx
|
|
134
|
+
<EztrakTabs
|
|
135
|
+
tabs={tabs}
|
|
136
|
+
activeTab={id}
|
|
137
|
+
onTabChange={setId}
|
|
138
|
+
classNames={{
|
|
139
|
+
list: "gap-3",
|
|
140
|
+
button: "rounded-full px-6",
|
|
141
|
+
underline: "h-1 bg-blue-600",
|
|
142
|
+
panel: "mt-6",
|
|
143
|
+
}}
|
|
144
|
+
/>
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Root import
|
|
148
|
+
|
|
149
|
+
```js
|
|
150
|
+
import { cn, useGridHeight, EztrakTabs } from "@eztrak/shared";
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Exports
|
|
154
|
+
|
|
155
|
+
| Subpath | Description |
|
|
156
|
+
| --- | --- |
|
|
157
|
+
| `@eztrak/shared` | Main entry — re-exports utils, hooks, and components |
|
|
158
|
+
| `@eztrak/shared/utils` | `cn`, `handleApiError`, date/format helpers, API error helpers, form field helpers |
|
|
159
|
+
| `@eztrak/shared/hooks` | `usePaginationUrlSync`, `useGridHeight` |
|
|
160
|
+
| `@eztrak/shared/components` | `EztrakTabs` and related types |
|
|
161
|
+
| `@eztrak/shared/components/tabs.css` | Default tab styles (CSS variables) |
|
|
162
|
+
|
|
163
|
+
### EztrakTabs props
|
|
164
|
+
|
|
165
|
+
| Prop | Type | Default | Description |
|
|
166
|
+
| --- | --- | --- | --- |
|
|
167
|
+
| `tabs` | `EztrakTab[]` | required | Tab definitions (`id`, `label`, optional `content`, `icon`, `disabled`) |
|
|
168
|
+
| `activeTab` | `string` | — | Controlled active tab id |
|
|
169
|
+
| `defaultTabId` | `string` | first enabled tab | Initial tab when uncontrolled |
|
|
170
|
+
| `onTabChange` | `(id: string) => void` | — | Called when a tab is selected |
|
|
171
|
+
| `showPanels` | `boolean` | `true` | Render tab panels; set `false` for nav-only |
|
|
172
|
+
| `keepMounted` | `boolean` | `false` | Keep inactive panels in DOM (hidden) |
|
|
173
|
+
| `classNames` | `EztrakTabsClassNames` | — | Per-slot class overrides |
|
|
174
|
+
| `className` | `string` | — | Shorthand for `classNames.wrapper` |
|
|
175
|
+
|
|
176
|
+
Keyboard: Arrow keys move between tabs; Home/End jump to first/last enabled tab.
|
|
177
|
+
|
|
178
|
+
## Requirements
|
|
179
|
+
|
|
180
|
+
- Node.js 18+
|
|
181
|
+
- React 18+ (for hooks and components)
|
|
182
|
+
- Works with any bundler that supports the [Node.js `exports` field](https://nodejs.org/api/packages.html#exports)
|
|
183
|
+
|
|
184
|
+
## License
|
|
185
|
+
|
|
186
|
+
ISC
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/utils/cn.ts"],"sourcesContent":["import clsx from \"clsx\";\r\nimport { twMerge } from \"tailwind-merge\";\r\n\r\nexport function cn(...inputs: Parameters<typeof clsx>) {\r\n return twMerge(clsx(inputs));\r\n}\r\n"],"mappings":";AAAA,OAAO,UAAU;AACjB,SAAS,eAAe;AAEjB,SAAS,MAAM,QAAiC;AACrD,SAAO,QAAQ,KAAK,MAAM,CAAC;AAC7B;","names":[]}
|
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
// src/utils/cn.ts
|
|
2
|
-
import clsx from "clsx";
|
|
3
|
-
import { twMerge } from "tailwind-merge";
|
|
4
|
-
function cn(...inputs) {
|
|
5
|
-
return twMerge(clsx(inputs));
|
|
6
|
-
}
|
|
7
|
-
|
|
8
1
|
// src/utils/helpers.ts
|
|
9
2
|
var truncateText = (text, maxLength = 20) => {
|
|
10
3
|
if (!text) return "";
|
|
@@ -187,7 +180,7 @@ var getFieldsByInputType = (fields = [], key) => {
|
|
|
187
180
|
};
|
|
188
181
|
|
|
189
182
|
// src/utils/handleApiError.ts
|
|
190
|
-
import
|
|
183
|
+
import toast from "react-hot-toast";
|
|
191
184
|
import Swal from "sweetalert2";
|
|
192
185
|
function formatValidationErrorsHtml(errors) {
|
|
193
186
|
const items = Object.keys(errors).flatMap(
|
|
@@ -240,7 +233,6 @@ function handleApiError(error, { showAlert = false, fallbackMessage } = {}) {
|
|
|
240
233
|
}
|
|
241
234
|
|
|
242
235
|
export {
|
|
243
|
-
cn,
|
|
244
236
|
truncateText,
|
|
245
237
|
getApiError,
|
|
246
238
|
normalizeApiListItems,
|
|
@@ -264,4 +256,4 @@ export {
|
|
|
264
256
|
formatValidationErrorsHtml,
|
|
265
257
|
handleApiError
|
|
266
258
|
};
|
|
267
|
-
//# sourceMappingURL=chunk-
|
|
259
|
+
//# sourceMappingURL=chunk-AR7LUJY3.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/utils/helpers.ts","../src/utils/handleApiError.ts"],"sourcesContent":["export const truncateText = (text: string | null | undefined, maxLength = 20) => {\n if (!text) return \"\";\n if (text.length <= maxLength) {\n return text;\n }\n return text.slice(0, maxLength - 3) + \"...\";\n};\n\nexport function getApiError(apiError: unknown): string {\n if (!apiError) return \"An unknown error occurred\";\n\n if (typeof apiError === \"string\") {\n return apiError;\n }\n\n const error = apiError as {\n response?: { data?: Record<string, unknown> };\n message?: string;\n };\n\n if (error.response?.data) {\n const responseData = error.response.data;\n\n if (typeof responseData === \"string\") {\n return responseData;\n }\n\n const errors = responseData.errors;\n if (errors && typeof errors === \"object\") {\n for (const key in errors) {\n const messages = (errors as Record<string, unknown>)[key];\n if (Array.isArray(messages) && messages.length > 0) {\n return String(messages[0]);\n }\n }\n }\n\n if (responseData.message) {\n return String(responseData.message);\n }\n\n if (responseData.title) {\n return String(responseData.title);\n }\n }\n\n if (error.message) {\n return error.message;\n }\n\n return \"An unknown error occurred\";\n}\n\nexport const normalizeApiListItems = (responseData: unknown): unknown[] => {\n if (responseData == null) return [];\n if (Array.isArray(responseData)) return responseData;\n\n const record = responseData as Record<string, unknown>;\n const data = record.data;\n\n if (\n data &&\n typeof data === \"object\" &&\n !Array.isArray(data) &&\n Array.isArray((data as { items?: unknown[] }).items)\n ) {\n return (data as { items: unknown[] }).items;\n }\n\n const returnModel = record.returnModel as { items?: unknown[] } | undefined;\n if (Array.isArray(returnModel?.items)) return returnModel.items;\n if (Array.isArray(data)) return data;\n if (typeof data === \"object\" && data !== null) {\n return [data];\n }\n return [];\n};\n\nexport function capitalizeText(name: string | null | undefined): string {\n if (!name) return \"\";\n return name\n .split(\" \")\n .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())\n .join(\" \");\n}\n\nexport function toLowerCaseText(text: string | null | undefined): string {\n if (!text) return \"\";\n return text.toLowerCase();\n}\n\nexport const formatDate = (dateString: string | null | undefined): string => {\n if (dateString === null || dateString === undefined) return \"-\";\n const date = new Date(dateString);\n\n if (isNaN(date.getTime())) {\n return dateString;\n }\n\n const month = (date.getMonth() + 1).toString().padStart(2, \"0\");\n const day = date.getDate().toString().padStart(2, \"0\");\n const year = date.getFullYear().toString().slice(-2);\n return `${month}/${day}/${year}`;\n};\n\nexport const isEmpty = (obj: object): boolean => {\n return Object.keys(obj).length === 0;\n};\n\nexport const toDateInputValue = (date: string | Date | null | undefined): string => {\n if (!date) return \"\";\n\n let d: Date;\n\n if (typeof date === \"string\") {\n if (/^\\d{4}-\\d{2}-\\d{2}$/.test(date)) {\n return date;\n }\n d = new Date(date);\n } else {\n d = new Date(date);\n }\n\n if (isNaN(d.getTime())) return \"\";\n\n const localDate = new Date(d.getTime() - d.getTimezoneOffset() * 60000);\n const year = localDate.getFullYear();\n const month = (localDate.getMonth() + 1).toString().padStart(2, \"0\");\n const day = localDate.getDate().toString().padStart(2, \"0\");\n\n return `${year}-${month}-${day}`;\n};\n\nexport function convertCommaDataToArray(data: string | null | undefined): string[] {\n if (!data || data.trim() === \"\") {\n return [];\n }\n return data.split(\",\").map((item) => item.trim());\n}\n\nexport function checkIfExistsInArray<T>(array: T[], searchValue: T): boolean {\n return array.includes(searchValue);\n}\n\nexport const currencyFormatter = (value: number | null | undefined): string => {\n return value?.toLocaleString(\"en-US\", {\n style: \"currency\",\n currency: \"USD\",\n }) ?? \"\";\n};\n\nexport const parseCurrencyValueToNumber = (value: unknown): number => {\n if (value == null || value === \"\") return 0;\n\n const raw =\n typeof value === \"object\" && value !== null && \"value\" in value\n ? (value as { value: unknown }).value\n : value;\n\n const stringValue = raw == null || raw === \"\" ? \"\" : String(raw);\n const numericValue = stringValue.replace(/[$,]/g, \"\");\n return parseFloat(numericValue) || 0;\n};\n\nexport function flattenTableData<T extends Record<string, unknown>>(\n data: T[] | null | undefined,\n columns: { field?: string }[] | null | undefined\n): T[] {\n if (!Array.isArray(data)) return [];\n return data.map((row) => {\n const flatRow = { ...row };\n (columns ?? []).forEach((col) => {\n const key = col.field;\n if (!key) return;\n if (key.includes(\".\")) {\n const value = key.split(\".\").reduce<unknown>(\n (acc, part) => (acc as Record<string, unknown> | undefined)?.[part],\n row\n );\n (flatRow as Record<string, unknown>)[key] = value ?? \"\";\n }\n });\n return flatRow;\n });\n}\n\nexport const getErrorMessages = (error: {\n data?: {\n errors?: Record<string, string[]>;\n title?: string;\n error?: { name?: string[] };\n };\n} | null | undefined): string => {\n if (error?.data?.errors) {\n return Object.values(error.data.errors).flat().join(\"\\n\");\n }\n return (\n error?.data?.title ??\n error?.data?.error?.name?.[0] ??\n \"Failed to save\"\n );\n};\n\nexport const getFieldsByCategory = <T extends { category?: string }>(\n fields: T[] = [],\n key: string\n): T[] => {\n return fields?.filter(({ category = \"\" }) => category === key);\n};\n\nexport const getFieldsByName = <T extends { name?: string }>(\n fields: T[] = [],\n key: string\n): T[] => {\n return fields.filter(({ name = \"\" }) => name === key);\n};\n\nexport const deserializeJson = <T = unknown>(jsonString: string): T | null => {\n try {\n return JSON.parse(jsonString) as T;\n } catch (error) {\n console.error(\"Error parsing JSON:\", error);\n return null;\n }\n};\n\nexport const formatDateTime = (dateString: string | null | undefined): string => {\n if (!dateString) return \"Invalid Date\";\n const date = new Date(dateString);\n if (isNaN(date.getTime())) return \"Invalid Date\";\n const options: Intl.DateTimeFormatOptions = {\n year: \"numeric\",\n month: \"numeric\",\n day: \"numeric\",\n hour: \"numeric\",\n minute: \"numeric\",\n hour12: true,\n };\n return date.toLocaleString(\"en-US\", options);\n};\n\nexport const removeByPropertyValues = <T extends Record<string, unknown>>(\n items: T[] = [],\n property: keyof T & string,\n values: unknown[] = [],\n { caseInsensitive = false }: { caseInsensitive?: boolean } = {}\n): T[] => {\n if (!Array.isArray(items) || !property) return Array.isArray(items) ? items : [];\n const normalize = (v: unknown) =>\n caseInsensitive && typeof v === \"string\" ? v.toLowerCase() : v;\n const valuesArr = Array.isArray(values) ? values : [values];\n const toRemove = new Set(valuesArr.map(normalize));\n return items.filter((item) => !toRemove.has(normalize(item?.[property])));\n};\n\nexport const getFieldsByInputType = <T extends { inputType?: string }>(\n fields: T[] = [],\n key: string\n): T[] => {\n return fields.filter(({ inputType = \"\" }) => inputType === key);\n};\n","import toast from \"react-hot-toast\";\nimport Swal from \"sweetalert2\";\n\nexport type RtkQueryError = {\n status?: number | string;\n originalStatus?: number;\n data?: {\n errors?: Record<string, string[]>;\n message?: string;\n title?: string;\n };\n error?: string;\n};\n\nexport interface HandleApiErrorOptions {\n showAlert?: boolean;\n fallbackMessage?: string;\n}\n\nexport function formatValidationErrorsHtml(\n errors: Record<string, string[]>\n): string {\n const items = Object.keys(errors).flatMap((field) =>\n (errors[field] || []).map((msg) => `<li>${msg}</li>`)\n );\n return `\n <div style=\"text-align:left\">\n <p>Please review and correct the following:</p>\n <ul style=\"margin:8px 0 0 16px; list-style:disc;\">\n ${items.join(\"\")}\n </ul>\n </div>\n `;\n}\n\nfunction showValidationAlert(html: string): void {\n void Swal.fire({\n title: \"Validation errors\",\n html,\n icon: \"error\",\n confirmButtonText: \"OK\",\n showCancelButton: false,\n confirmButtonColor: \"#FF7335\",\n });\n}\n\n/** Handles RTK Query errors with built-in toast.error or SweetAlert (when showAlert is true). */\nexport function handleApiError(\n error: RtkQueryError | null | undefined,\n { showAlert = false, fallbackMessage }: HandleApiErrorOptions = {}\n): void {\n if (!error) return;\n\n const status = error.status ?? error.originalStatus;\n const data = error.data;\n\n if (status === 200) {\n return;\n }\n\n if (data?.errors && typeof data.errors === \"object\") {\n const errors = data.errors;\n\n if (showAlert) {\n showValidationAlert(formatValidationErrorsHtml(errors));\n } else {\n Object.keys(errors).forEach((field) => {\n (errors[field] || []).forEach((errorMessage) => {\n toast.error(errorMessage);\n });\n });\n }\n return;\n }\n\n if (status === 404) {\n return;\n }\n\n const message =\n fallbackMessage ||\n data?.message ||\n data?.title ||\n error.error ||\n \"An error occurred. Please try again.\";\n\n toast.error(message);\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import {
|
|
2
|
+
cn
|
|
3
|
+
} from "./chunk-5L6SERQP.mjs";
|
|
4
|
+
|
|
5
|
+
// src/components/tabs/EztrakTabs.tsx
|
|
6
|
+
import {
|
|
7
|
+
useCallback,
|
|
8
|
+
useEffect,
|
|
9
|
+
useId,
|
|
10
|
+
useRef,
|
|
11
|
+
useState
|
|
12
|
+
} from "react";
|
|
13
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
14
|
+
function getInitialTabId(tabs, defaultTabId) {
|
|
15
|
+
if (defaultTabId && tabs.some((t) => t.id === defaultTabId && !t.disabled)) {
|
|
16
|
+
return defaultTabId;
|
|
17
|
+
}
|
|
18
|
+
return tabs.find((t) => !t.disabled)?.id;
|
|
19
|
+
}
|
|
20
|
+
function getEnabledTabIds(tabs) {
|
|
21
|
+
return tabs.filter((t) => !t.disabled).map((t) => t.id);
|
|
22
|
+
}
|
|
23
|
+
function EztrakTabs({
|
|
24
|
+
tabs,
|
|
25
|
+
activeTab,
|
|
26
|
+
defaultTabId,
|
|
27
|
+
onTabChange,
|
|
28
|
+
showPanels = true,
|
|
29
|
+
keepMounted = false,
|
|
30
|
+
classNames,
|
|
31
|
+
className
|
|
32
|
+
}) {
|
|
33
|
+
const baseId = useId().replace(/:/g, "");
|
|
34
|
+
const isControlled = activeTab !== void 0 && activeTab !== null;
|
|
35
|
+
const [uncontrolledActive, setUncontrolledActive] = useState(
|
|
36
|
+
() => getInitialTabId(tabs, defaultTabId)
|
|
37
|
+
);
|
|
38
|
+
const tabRefs = useRef(/* @__PURE__ */ new Map());
|
|
39
|
+
const displayActive = isControlled ? activeTab : uncontrolledActive;
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (isControlled) return;
|
|
42
|
+
const enabledIds = getEnabledTabIds(tabs);
|
|
43
|
+
if (!displayActive || !enabledIds.includes(displayActive)) {
|
|
44
|
+
const next = getInitialTabId(tabs, defaultTabId);
|
|
45
|
+
if (next !== displayActive) {
|
|
46
|
+
setUncontrolledActive(next);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}, [tabs, defaultTabId, isControlled, displayActive]);
|
|
50
|
+
const handleTabClick = useCallback(
|
|
51
|
+
(tabId, disabled) => {
|
|
52
|
+
if (disabled) return;
|
|
53
|
+
if (!isControlled) {
|
|
54
|
+
setUncontrolledActive(tabId);
|
|
55
|
+
}
|
|
56
|
+
onTabChange?.(tabId);
|
|
57
|
+
},
|
|
58
|
+
[isControlled, onTabChange]
|
|
59
|
+
);
|
|
60
|
+
const focusTab = useCallback((tabId) => {
|
|
61
|
+
tabRefs.current.get(tabId)?.focus();
|
|
62
|
+
}, []);
|
|
63
|
+
const handleListKeyDown = useCallback(
|
|
64
|
+
(event) => {
|
|
65
|
+
const enabledIds = getEnabledTabIds(tabs);
|
|
66
|
+
if (enabledIds.length === 0) return;
|
|
67
|
+
const currentIndex = displayActive ? enabledIds.indexOf(displayActive) : -1;
|
|
68
|
+
let nextIndex = currentIndex;
|
|
69
|
+
switch (event.key) {
|
|
70
|
+
case "ArrowRight":
|
|
71
|
+
case "ArrowDown":
|
|
72
|
+
event.preventDefault();
|
|
73
|
+
nextIndex = currentIndex < 0 ? 0 : (currentIndex + 1) % enabledIds.length;
|
|
74
|
+
break;
|
|
75
|
+
case "ArrowLeft":
|
|
76
|
+
case "ArrowUp":
|
|
77
|
+
event.preventDefault();
|
|
78
|
+
nextIndex = currentIndex < 0 ? enabledIds.length - 1 : (currentIndex - 1 + enabledIds.length) % enabledIds.length;
|
|
79
|
+
break;
|
|
80
|
+
case "Home":
|
|
81
|
+
event.preventDefault();
|
|
82
|
+
nextIndex = 0;
|
|
83
|
+
break;
|
|
84
|
+
case "End":
|
|
85
|
+
event.preventDefault();
|
|
86
|
+
nextIndex = enabledIds.length - 1;
|
|
87
|
+
break;
|
|
88
|
+
default:
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const nextId = enabledIds[nextIndex];
|
|
92
|
+
if (nextId) {
|
|
93
|
+
handleTabClick(nextId);
|
|
94
|
+
focusTab(nextId);
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
[tabs, displayActive, handleTabClick, focusTab]
|
|
98
|
+
);
|
|
99
|
+
if (!tabs.length) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
const wrapperClass = cn(
|
|
103
|
+
"eztrak-tabs-wrapper",
|
|
104
|
+
classNames?.wrapper,
|
|
105
|
+
className
|
|
106
|
+
);
|
|
107
|
+
return /* @__PURE__ */ jsx("div", { className: wrapperClass, children: /* @__PURE__ */ jsxs("div", { className: "eztrak-tabs-container", children: [
|
|
108
|
+
/* @__PURE__ */ jsx("nav", { className: "eztrak-tabs-nav", "aria-label": "Tabs", children: /* @__PURE__ */ jsx(
|
|
109
|
+
"ul",
|
|
110
|
+
{
|
|
111
|
+
role: "tablist",
|
|
112
|
+
className: cn("eztrak-tabs-list", classNames?.list),
|
|
113
|
+
onKeyDown: handleListKeyDown,
|
|
114
|
+
children: tabs.map((tab) => {
|
|
115
|
+
const isActive = displayActive === tab.id;
|
|
116
|
+
const tabId = `${baseId}-tab-${tab.id}`;
|
|
117
|
+
const panelId = `${baseId}-panel-${tab.id}`;
|
|
118
|
+
return /* @__PURE__ */ jsx(
|
|
119
|
+
"li",
|
|
120
|
+
{
|
|
121
|
+
role: "presentation",
|
|
122
|
+
className: cn(
|
|
123
|
+
"eztrak-tabs-item",
|
|
124
|
+
isActive && "eztrak-tabs-item-active",
|
|
125
|
+
classNames?.item,
|
|
126
|
+
isActive && classNames?.itemActive
|
|
127
|
+
),
|
|
128
|
+
children: /* @__PURE__ */ jsxs(
|
|
129
|
+
"button",
|
|
130
|
+
{
|
|
131
|
+
ref: (el) => {
|
|
132
|
+
if (el) {
|
|
133
|
+
tabRefs.current.set(tab.id, el);
|
|
134
|
+
} else {
|
|
135
|
+
tabRefs.current.delete(tab.id);
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
type: "button",
|
|
139
|
+
role: "tab",
|
|
140
|
+
id: tabId,
|
|
141
|
+
"aria-selected": isActive,
|
|
142
|
+
"aria-controls": showPanels ? panelId : void 0,
|
|
143
|
+
tabIndex: isActive ? 0 : -1,
|
|
144
|
+
disabled: tab.disabled,
|
|
145
|
+
className: cn(
|
|
146
|
+
"eztrak-tabs-button",
|
|
147
|
+
isActive && "eztrak-tabs-button-active",
|
|
148
|
+
classNames?.button,
|
|
149
|
+
isActive && classNames?.buttonActive
|
|
150
|
+
),
|
|
151
|
+
onClick: () => handleTabClick(tab.id, tab.disabled),
|
|
152
|
+
children: [
|
|
153
|
+
tab.icon ? /* @__PURE__ */ jsx(
|
|
154
|
+
"span",
|
|
155
|
+
{
|
|
156
|
+
className: cn("eztrak-tabs-icon", classNames?.icon),
|
|
157
|
+
"aria-hidden": true,
|
|
158
|
+
children: tab.icon
|
|
159
|
+
}
|
|
160
|
+
) : null,
|
|
161
|
+
/* @__PURE__ */ jsx(
|
|
162
|
+
"span",
|
|
163
|
+
{
|
|
164
|
+
className: cn("eztrak-tabs-label", classNames?.label),
|
|
165
|
+
children: tab.label
|
|
166
|
+
}
|
|
167
|
+
),
|
|
168
|
+
isActive ? /* @__PURE__ */ jsx(
|
|
169
|
+
"span",
|
|
170
|
+
{
|
|
171
|
+
className: cn(
|
|
172
|
+
"eztrak-tabs-underline",
|
|
173
|
+
classNames?.underline
|
|
174
|
+
),
|
|
175
|
+
"aria-hidden": true
|
|
176
|
+
}
|
|
177
|
+
) : null
|
|
178
|
+
]
|
|
179
|
+
}
|
|
180
|
+
)
|
|
181
|
+
},
|
|
182
|
+
tab.id
|
|
183
|
+
);
|
|
184
|
+
})
|
|
185
|
+
}
|
|
186
|
+
) }),
|
|
187
|
+
showPanels ? /* @__PURE__ */ jsx("div", { className: cn("eztrak-tabs-content", classNames?.content), children: tabs.map((tab) => {
|
|
188
|
+
const isActive = displayActive === tab.id;
|
|
189
|
+
const panelId = `${baseId}-panel-${tab.id}`;
|
|
190
|
+
const tabId = `${baseId}-tab-${tab.id}`;
|
|
191
|
+
if (!keepMounted && !isActive) {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
return /* @__PURE__ */ jsx(
|
|
195
|
+
"div",
|
|
196
|
+
{
|
|
197
|
+
id: panelId,
|
|
198
|
+
role: "tabpanel",
|
|
199
|
+
"aria-labelledby": tabId,
|
|
200
|
+
hidden: !isActive,
|
|
201
|
+
className: cn("eztrak-tabs-panel", classNames?.panel),
|
|
202
|
+
children: tab.content
|
|
203
|
+
},
|
|
204
|
+
tab.id
|
|
205
|
+
);
|
|
206
|
+
}) }) : null
|
|
207
|
+
] }) });
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export {
|
|
211
|
+
EztrakTabs
|
|
212
|
+
};
|
|
213
|
+
//# sourceMappingURL=chunk-SNADLOOV.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/components/tabs/EztrakTabs.tsx"],"sourcesContent":["import {\r\n useCallback,\r\n useEffect,\r\n useId,\r\n useRef,\r\n useState,\r\n type KeyboardEvent,\r\n} from \"react\";\r\nimport { cn } from \"../../utils/cn\";\r\nimport type { EztrakTab, EztrakTabsProps } from \"./types\";\r\n\r\nfunction getInitialTabId(\r\n tabs: EztrakTab[],\r\n defaultTabId?: string\r\n): string | undefined {\r\n if (defaultTabId && tabs.some((t) => t.id === defaultTabId && !t.disabled)) {\r\n return defaultTabId;\r\n }\r\n return tabs.find((t) => !t.disabled)?.id;\r\n}\r\n\r\nfunction getEnabledTabIds(tabs: EztrakTab[]): string[] {\r\n return tabs.filter((t) => !t.disabled).map((t) => t.id);\r\n}\r\n\r\nexport function EztrakTabs({\r\n tabs,\r\n activeTab,\r\n defaultTabId,\r\n onTabChange,\r\n showPanels = true,\r\n keepMounted = false,\r\n classNames,\r\n className,\r\n}: EztrakTabsProps) {\r\n const baseId = useId().replace(/:/g, \"\");\r\n const isControlled = activeTab !== undefined && activeTab !== null;\r\n const [uncontrolledActive, setUncontrolledActive] = useState(() =>\r\n getInitialTabId(tabs, defaultTabId)\r\n );\r\n const tabRefs = useRef<Map<string, HTMLButtonElement>>(new Map());\r\n\r\n const displayActive = isControlled ? activeTab : uncontrolledActive;\r\n\r\n useEffect(() => {\r\n if (isControlled) return;\r\n const enabledIds = getEnabledTabIds(tabs);\r\n if (!displayActive || !enabledIds.includes(displayActive)) {\r\n const next = getInitialTabId(tabs, defaultTabId);\r\n if (next !== displayActive) {\r\n setUncontrolledActive(next);\r\n }\r\n }\r\n }, [tabs, defaultTabId, isControlled, displayActive]);\r\n\r\n const handleTabClick = useCallback(\r\n (tabId: string, disabled?: boolean) => {\r\n if (disabled) return;\r\n if (!isControlled) {\r\n setUncontrolledActive(tabId);\r\n }\r\n onTabChange?.(tabId);\r\n },\r\n [isControlled, onTabChange]\r\n );\r\n\r\n const focusTab = useCallback((tabId: string) => {\r\n tabRefs.current.get(tabId)?.focus();\r\n }, []);\r\n\r\n const handleListKeyDown = useCallback(\r\n (event: KeyboardEvent<HTMLUListElement>) => {\r\n const enabledIds = getEnabledTabIds(tabs);\r\n if (enabledIds.length === 0) return;\r\n\r\n const currentIndex = displayActive\r\n ? enabledIds.indexOf(displayActive)\r\n : -1;\r\n let nextIndex = currentIndex;\r\n\r\n switch (event.key) {\r\n case \"ArrowRight\":\r\n case \"ArrowDown\":\r\n event.preventDefault();\r\n nextIndex =\r\n currentIndex < 0\r\n ? 0\r\n : (currentIndex + 1) % enabledIds.length;\r\n break;\r\n case \"ArrowLeft\":\r\n case \"ArrowUp\":\r\n event.preventDefault();\r\n nextIndex =\r\n currentIndex < 0\r\n ? enabledIds.length - 1\r\n : (currentIndex - 1 + enabledIds.length) % enabledIds.length;\r\n break;\r\n case \"Home\":\r\n event.preventDefault();\r\n nextIndex = 0;\r\n break;\r\n case \"End\":\r\n event.preventDefault();\r\n nextIndex = enabledIds.length - 1;\r\n break;\r\n default:\r\n return;\r\n }\r\n\r\n const nextId = enabledIds[nextIndex];\r\n if (nextId) {\r\n handleTabClick(nextId);\r\n focusTab(nextId);\r\n }\r\n },\r\n [tabs, displayActive, handleTabClick, focusTab]\r\n );\r\n\r\n if (!tabs.length) {\r\n return null;\r\n }\r\n\r\n const wrapperClass = cn(\r\n \"eztrak-tabs-wrapper\",\r\n classNames?.wrapper,\r\n className\r\n );\r\n\r\n return (\r\n <div className={wrapperClass}>\r\n <div className=\"eztrak-tabs-container\">\r\n <nav className=\"eztrak-tabs-nav\" aria-label=\"Tabs\">\r\n <ul\r\n role=\"tablist\"\r\n className={cn(\"eztrak-tabs-list\", classNames?.list)}\r\n onKeyDown={handleListKeyDown}\r\n >\r\n {tabs.map((tab) => {\r\n const isActive = displayActive === tab.id;\r\n const tabId = `${baseId}-tab-${tab.id}`;\r\n const panelId = `${baseId}-panel-${tab.id}`;\r\n\r\n return (\r\n <li\r\n key={tab.id}\r\n role=\"presentation\"\r\n className={cn(\r\n \"eztrak-tabs-item\",\r\n isActive && \"eztrak-tabs-item-active\",\r\n classNames?.item,\r\n isActive && classNames?.itemActive\r\n )}\r\n >\r\n <button\r\n ref={(el) => {\r\n if (el) {\r\n tabRefs.current.set(tab.id, el);\r\n } else {\r\n tabRefs.current.delete(tab.id);\r\n }\r\n }}\r\n type=\"button\"\r\n role=\"tab\"\r\n id={tabId}\r\n aria-selected={isActive}\r\n aria-controls={showPanels ? panelId : undefined}\r\n tabIndex={isActive ? 0 : -1}\r\n disabled={tab.disabled}\r\n className={cn(\r\n \"eztrak-tabs-button\",\r\n isActive && \"eztrak-tabs-button-active\",\r\n classNames?.button,\r\n isActive && classNames?.buttonActive\r\n )}\r\n onClick={() => handleTabClick(tab.id, tab.disabled)}\r\n >\r\n {tab.icon ? (\r\n <span\r\n className={cn(\"eztrak-tabs-icon\", classNames?.icon)}\r\n aria-hidden\r\n >\r\n {tab.icon}\r\n </span>\r\n ) : null}\r\n <span\r\n className={cn(\"eztrak-tabs-label\", classNames?.label)}\r\n >\r\n {tab.label}\r\n </span>\r\n {isActive ? (\r\n <span\r\n className={cn(\r\n \"eztrak-tabs-underline\",\r\n classNames?.underline\r\n )}\r\n aria-hidden\r\n />\r\n ) : null}\r\n </button>\r\n </li>\r\n );\r\n })}\r\n </ul>\r\n </nav>\r\n\r\n {showPanels ? (\r\n <div className={cn(\"eztrak-tabs-content\", classNames?.content)}>\r\n {tabs.map((tab) => {\r\n const isActive = displayActive === tab.id;\r\n const panelId = `${baseId}-panel-${tab.id}`;\r\n const tabId = `${baseId}-tab-${tab.id}`;\r\n\r\n if (!keepMounted && !isActive) {\r\n return null;\r\n }\r\n\r\n return (\r\n <div\r\n key={tab.id}\r\n id={panelId}\r\n role=\"tabpanel\"\r\n aria-labelledby={tabId}\r\n hidden={!isActive}\r\n className={cn(\"eztrak-tabs-panel\", classNames?.panel)}\r\n >\r\n {tab.content}\r\n </div>\r\n );\r\n })}\r\n </div>\r\n ) : null}\r\n </div>\r\n </div>\r\n );\r\n}\r\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
type EztrakTab = {
|
|
5
|
+
id: string;
|
|
6
|
+
label: ReactNode;
|
|
7
|
+
content?: ReactNode;
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
icon?: ReactNode;
|
|
10
|
+
};
|
|
11
|
+
type EztrakTabsClassNames = {
|
|
12
|
+
wrapper?: string;
|
|
13
|
+
list?: string;
|
|
14
|
+
item?: string;
|
|
15
|
+
itemActive?: string;
|
|
16
|
+
button?: string;
|
|
17
|
+
buttonActive?: string;
|
|
18
|
+
label?: string;
|
|
19
|
+
icon?: string;
|
|
20
|
+
underline?: string;
|
|
21
|
+
panel?: string;
|
|
22
|
+
content?: string;
|
|
23
|
+
};
|
|
24
|
+
type EztrakTabsProps = {
|
|
25
|
+
tabs: EztrakTab[];
|
|
26
|
+
activeTab?: string;
|
|
27
|
+
defaultTabId?: string;
|
|
28
|
+
onTabChange?: (id: string) => void;
|
|
29
|
+
/** When false, only the tab list is rendered (nav-only). Default: true */
|
|
30
|
+
showPanels?: boolean;
|
|
31
|
+
/** Keep inactive panels in the DOM (hidden). Default: false */
|
|
32
|
+
keepMounted?: boolean;
|
|
33
|
+
classNames?: EztrakTabsClassNames;
|
|
34
|
+
/** Shorthand for classNames.wrapper */
|
|
35
|
+
className?: string;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
declare function EztrakTabs({ tabs, activeTab, defaultTabId, onTabChange, showPanels, keepMounted, classNames, className, }: EztrakTabsProps): react.JSX.Element | null;
|
|
39
|
+
|
|
40
|
+
export { type EztrakTab, EztrakTabs, type EztrakTabsClassNames, type EztrakTabsProps };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
type EztrakTab = {
|
|
5
|
+
id: string;
|
|
6
|
+
label: ReactNode;
|
|
7
|
+
content?: ReactNode;
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
icon?: ReactNode;
|
|
10
|
+
};
|
|
11
|
+
type EztrakTabsClassNames = {
|
|
12
|
+
wrapper?: string;
|
|
13
|
+
list?: string;
|
|
14
|
+
item?: string;
|
|
15
|
+
itemActive?: string;
|
|
16
|
+
button?: string;
|
|
17
|
+
buttonActive?: string;
|
|
18
|
+
label?: string;
|
|
19
|
+
icon?: string;
|
|
20
|
+
underline?: string;
|
|
21
|
+
panel?: string;
|
|
22
|
+
content?: string;
|
|
23
|
+
};
|
|
24
|
+
type EztrakTabsProps = {
|
|
25
|
+
tabs: EztrakTab[];
|
|
26
|
+
activeTab?: string;
|
|
27
|
+
defaultTabId?: string;
|
|
28
|
+
onTabChange?: (id: string) => void;
|
|
29
|
+
/** When false, only the tab list is rendered (nav-only). Default: true */
|
|
30
|
+
showPanels?: boolean;
|
|
31
|
+
/** Keep inactive panels in the DOM (hidden). Default: false */
|
|
32
|
+
keepMounted?: boolean;
|
|
33
|
+
classNames?: EztrakTabsClassNames;
|
|
34
|
+
/** Shorthand for classNames.wrapper */
|
|
35
|
+
className?: string;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
declare function EztrakTabs({ tabs, activeTab, defaultTabId, onTabChange, showPanels, keepMounted, classNames, className, }: EztrakTabsProps): react.JSX.Element | null;
|
|
39
|
+
|
|
40
|
+
export { type EztrakTab, EztrakTabs, type EztrakTabsClassNames, type EztrakTabsProps };
|