@expresscsv/react 0.1.4 → 0.1.6

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ExpressCSV
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,6 +1,9 @@
1
- # ExpressCSV React SDK
1
+ # @expresscsv/react
2
2
 
3
- A React component wrapper for the ExpressCSV SDK that provides a simple, declarative way to integrate CSV import functionality into your React applications.
3
+ [![npm version](https://img.shields.io/npm/v/@expresscsv/react.svg)](https://www.npmjs.com/package/@expresscsv/react)
4
+ ![license](https://img.shields.io/npm/l/@expresscsv/react.svg)
5
+
6
+ React hook for embedding the [ExpressCSV](https://expresscsv.com) CSV import widget. Wraps [`@expresscsv/sdk`](https://www.npmjs.com/package/@expresscsv/sdk) with automatic lifecycle management and reactive state.
4
7
 
5
8
  ## Installation
6
9
 
@@ -15,47 +18,45 @@ npm install @expresscsv/react
15
18
  yarn add @expresscsv/react
16
19
  ```
17
20
 
18
- ## Usage
19
-
20
- The React SDK provides a `useExpressCSV` hook for easy integration into your React components.
21
+ > **Peer dependency:** React 18+
21
22
 
22
- ### Basic Usage
23
-
24
- The React SDK provides a `useExpressCSV` hook for easy integration:
23
+ ## Quick Start
25
24
 
26
25
  ```tsx
27
- import { useExpressCSV, x, type Infer } from '@expresscsv/react';
26
+ import { useExpressCSV, x } from "@expresscsv/react";
28
27
 
29
28
  const schema = x.row({
30
- name: x.string().label('Full Name'),
31
- email: x.string().email().label('Email Address'),
32
- age: x.number().label('Age').min(18),
29
+ name: x.string().label("Full Name"),
30
+ email: x.string().email().label("Email Address"),
31
+ age: x.number().label("Age").min(18),
33
32
  });
34
33
 
35
34
  function App() {
36
- const { open, isOpen, widgetState } = useExpressCSV({
35
+ const { open, isOpen } = useExpressCSV({
37
36
  schema,
38
- title: "Import Users",
37
+ publishableKey: "your-publishable-key",
39
38
  importIdentifier: "user-import",
39
+ title: "Import Users",
40
40
  });
41
41
 
42
42
  const handleImport = () => {
43
43
  open({
44
- chunkSize: 100,
44
+ chunkSize: 500,
45
45
  onData: async (chunk, next) => {
46
- console.log(`Processing chunk ${chunk.currentChunkIndex + 1}/${chunk.totalChunks}`);
47
- console.log('CSV data:', chunk.records);
48
- // Process your validated data here
46
+ console.log(
47
+ `Chunk ${chunk.currentChunkIndex + 1}/${chunk.totalChunks}`
48
+ );
49
+ console.log("Records:", chunk.records);
49
50
  next();
50
51
  },
51
52
  onComplete: () => {
52
- console.log('All chunks processed');
53
+ console.log("All chunks processed");
53
54
  },
54
55
  onCancel: () => {
55
- console.log('User cancelled import');
56
+ console.log("User cancelled");
56
57
  },
57
58
  onError: (error) => {
58
- console.error('Import error:', error);
59
+ console.error("Import error:", error);
59
60
  },
60
61
  });
61
62
  };
@@ -68,226 +69,299 @@ function App() {
68
69
  }
69
70
  ```
70
71
 
71
- ### Preload for Instant Display
72
+ Your `publishableKey` is available from the [ExpressCSV dashboard](https://expresscsv.com). Two key types are available: **production** keys for live usage, and **dev/testing** keys that provide unlimited test imports.
73
+
74
+ ## Preloading
72
75
 
73
- By default, the widget preloads in the background for instant display. This provides the best user experience with minimal perceived loading time.
76
+ By default the widget preloads in a hidden iframe so it appears instantly when `open()` is called:
74
77
 
75
78
  ```tsx
76
- import { useExpressCSV, x } from '@expresscsv/react';
79
+ const { open } = useExpressCSV({
80
+ schema,
81
+ publishableKey: "your-publishable-key",
82
+ importIdentifier: "user-import",
83
+ });
77
84
 
78
- const schema = x.row({
79
- name: x.string().label('Full Name'),
80
- email: x.string().email().label('Email Address'),
85
+ // Widget displays instantly
86
+ const handleImport = () => {
87
+ open({
88
+ onData: (chunk, next) => {
89
+ console.log("Records:", chunk.records);
90
+ next();
91
+ },
92
+ });
93
+ };
94
+ ```
95
+
96
+ To disable preloading (there will be a brief loading screen instead):
97
+
98
+ ```tsx
99
+ const { open } = useExpressCSV({
100
+ schema,
101
+ publishableKey: "your-publishable-key",
102
+ importIdentifier: "user-import",
103
+ preload: false,
81
104
  });
105
+ ```
106
+
107
+ ## Theming and Styling
108
+
109
+ Customize the widget's appearance with the `theme`, `colorMode`, `customCSS`, and `fonts` options.
110
+
111
+ ### Theme
112
+
113
+ Use the `theme` option to override CSS variables (colors, radius, typography). Pass either a single theme (applies to both light and dark) or a dual-mode theme with separate light/dark values:
114
+
115
+ ```tsx
116
+ import { useExpressCSV, x, type ECSVTheme } from "@expresscsv/react";
117
+
118
+ // Single theme (both modes)
119
+ const theme: ECSVTheme = {
120
+ primary: "#4F46E5",
121
+ "primary-foreground": "#ffffff",
122
+ background: "#ffffff",
123
+ foreground: "#0f172a",
124
+ border: "#e5e7eb",
125
+ ring: "#A5B4FC",
126
+ radius: "0.5rem",
127
+ };
128
+
129
+ // Dual-mode (light and dark)
130
+ const dualTheme: ECSVTheme = {
131
+ modes: {
132
+ light: {
133
+ primary: "#4F46E5",
134
+ background: "#ffffff",
135
+ foreground: "#0f172a",
136
+ },
137
+ dark: {
138
+ primary: "#a5b4fc",
139
+ background: "#09090b",
140
+ foreground: "#fafafa",
141
+ },
142
+ },
143
+ };
82
144
 
83
145
  function App() {
84
- // Preload is enabled by default
85
146
  const { open } = useExpressCSV({
86
147
  schema,
87
- title: "Import Users",
148
+ publishableKey: "your-publishable-key",
88
149
  importIdentifier: "user-import",
150
+ theme,
89
151
  });
152
+ // ...
153
+ }
154
+ ```
90
155
 
91
- const handleImport = () => {
92
- open({
93
- onData: async (chunk, next) => {
94
- console.log('CSV data:', chunk.records);
95
- next();
96
- },
97
- });
98
- };
156
+ Theme variables (light mode defaults):
157
+
158
+ | Variable | Default |
159
+ |---|---|
160
+ | `radius` | `0.625rem` |
161
+ | `background` | `oklch(1 0 0)` |
162
+ | `foreground` | `oklch(0.145 0 0)` |
163
+ | `card` | `oklch(1 0 0)` |
164
+ | `card-foreground` | `oklch(0.145 0 0)` |
165
+ | `popover` | `oklch(1 0 0)` |
166
+ | `popover-foreground` | `oklch(0.145 0 0)` |
167
+ | `primary` | `oklch(0.205 0 0)` |
168
+ | `primary-foreground` | `oklch(0.985 0 0)` |
169
+ | `secondary` | `oklch(0.97 0 0)` |
170
+ | `secondary-foreground` | `oklch(0.205 0 0)` |
171
+ | `muted` | `oklch(0.97 0 0)` |
172
+ | `muted-foreground` | `oklch(0.556 0 0)` |
173
+ | `accent` | `oklch(0.7 0.2 145)` |
174
+ | `accent-foreground` | `oklch(0.985 0 0)` |
175
+ | `destructive` | `oklch(0.577 0.245 27.325)` |
176
+ | `destructive-foreground` | `oklch(0.985 0 0)` |
177
+ | `success` | `oklch(0.7 0.2 145)` |
178
+ | `success-foreground` | `oklch(0.985 0 0)` |
179
+ | `warning` | `oklch(0.769 0.188 70)` |
180
+ | `warning-foreground` | `oklch(0.985 0 0)` |
181
+ | `border` | `oklch(0.922 0 0)` |
182
+ | `input` | `oklch(0.922 0 0)` |
183
+ | `ring` | `oklch(0.708 0 0)` |
184
+ | `font-title` | `inherit` |
185
+ | `font-body` | `inherit` |
186
+
187
+ ### Color Mode
188
+
189
+ Control light/dark mode with `colorMode`:
99
190
 
100
- return (
101
- <button onClick={handleImport}>
102
- Import CSV
103
- </button>
104
- );
105
- }
191
+ ```tsx
192
+ const { open } = useExpressCSV({
193
+ schema,
194
+ publishableKey: "your-publishable-key",
195
+ importIdentifier: "user-import",
196
+ colorMode: "system", // 'light' | 'dark' | 'system'
197
+ });
198
+ ```
199
+
200
+ ### Custom CSS
201
+
202
+ Inject custom CSS for fine-grained styling overrides.
203
+
204
+ ```tsx
205
+ const { open } = useExpressCSV({
206
+ schema,
207
+ publishableKey: "your-publishable-key",
208
+ importIdentifier: "user-import",
209
+ customCSS: `
210
+ .ecsv [data-step="upload"] {
211
+ border-radius: 1rem;
212
+ }
213
+ .ecsv button {
214
+ font-weight: 600;
215
+ }
216
+ `,
217
+ });
106
218
  ```
107
219
 
108
- To disable preload for specific use cases (not recommended for most scenarios):
220
+ ### Custom Fonts
221
+
222
+ Load custom fonts via the `fonts` option:
109
223
 
110
224
  ```tsx
111
225
  const { open } = useExpressCSV({
112
226
  schema,
113
- title: "Import Users",
227
+ publishableKey: "your-publishable-key",
114
228
  importIdentifier: "user-import",
115
- preload: false, // Disable preload
229
+ fonts: {
230
+ title: { source: "google", name: "Space Grotesk", weights: [400, 600, 700] },
231
+ body: { source: "custom", url: "https://example.com/font.woff2", format: "woff2" },
232
+ },
233
+ theme: {
234
+ "font-title": "'Space Grotesk', sans-serif",
235
+ "font-body": "'Custom Font', sans-serif",
236
+ },
116
237
  });
117
238
  ```
118
239
 
119
- ### Webhook Delivery
240
+ ## Webhook Delivery
120
241
 
121
- You can deliver CSV data to a webhook endpoint instead of (or in addition to) using local callbacks. The backend will handle retries, exponential backoff, and deliver data in chunks:
242
+ Deliver data to a server endpoint instead of (or in addition to) processing locally:
122
243
 
123
244
  ```tsx
124
- import { useExpressCSV, x, type Infer, type WebhookConfig } from '@expresscsv/react';
245
+ import { useExpressCSV, x } from "@expresscsv/react";
125
246
 
126
247
  const schema = x.row({
127
- name: x.string().label('Full Name'),
128
- email: x.string().email().label('Email Address'),
248
+ name: x.string().label("Full Name"),
249
+ email: x.string().email().label("Email Address"),
129
250
  });
130
251
 
131
252
  function App() {
132
253
  const { open } = useExpressCSV({
133
254
  schema,
134
- title: "Import Users",
255
+ publishableKey: "your-publishable-key",
135
256
  importIdentifier: "user-import",
257
+ title: "Import Users",
136
258
  });
137
259
 
138
260
  const handleImport = () => {
139
261
  open({
140
- chunkSize: 500, // Optional: chunk size for webhook delivery (default: 1000)
141
262
  webhook: {
142
- url: 'https://api.example.com/webhooks/csv-import',
143
- method: 'POST',
263
+ url: "https://api.example.com/webhooks/csv-import",
264
+ method: "POST",
144
265
  headers: {
145
- 'Authorization': 'Bearer your-api-token',
146
- 'X-Custom-Header': 'custom-value',
266
+ Authorization: "Bearer your-api-token",
147
267
  },
148
268
  metadata: {
149
- // Optional: arbitrary metadata to include in webhook payload
150
- source: 'react-app',
151
- userId: 'user-123',
269
+ source: "react-app",
270
+ userId: "user-123",
152
271
  },
153
272
  },
154
273
  onComplete: () => {
155
- console.log('Webhook delivery initiated');
274
+ console.log("Webhook delivery initiated");
156
275
  },
157
276
  onError: (error) => {
158
- console.error('Webhook delivery error:', error);
277
+ console.error("Delivery error:", error);
159
278
  },
160
279
  });
161
280
  };
162
281
 
163
- return (
164
- <button onClick={handleImport}>
165
- Import CSV via Webhook
166
- </button>
167
- );
282
+ return <button onClick={handleImport}>Import CSV via Webhook</button>;
168
283
  }
169
284
  ```
170
285
 
171
286
  ### Combined Local Callback and Webhook
172
287
 
173
- You can use both `onData` callback and webhook delivery simultaneously:
174
-
175
288
  ```tsx
176
- import { useExpressCSV, x, type Infer } from '@expresscsv/react';
177
-
178
- const schema = x.row({
179
- name: x.string().label('Full Name'),
180
- email: x.string().email().label('Email Address'),
289
+ open({
290
+ chunkSize: 500,
291
+ onData: async (chunk, next) => {
292
+ await saveToLocalDatabase(chunk.records);
293
+ next();
294
+ },
295
+ webhook: {
296
+ url: "https://api.example.com/webhooks/csv-import",
297
+ headers: { Authorization: "Bearer your-api-token" },
298
+ },
299
+ onComplete: () => {
300
+ console.log("Local processing and webhook delivery complete");
301
+ },
181
302
  });
182
-
183
- function App() {
184
- const { open } = useExpressCSV({
185
- schema,
186
- title: "Import Users",
187
- importIdentifier: "user-import",
188
- });
189
-
190
- const handleImport = () => {
191
- open({
192
- chunkSize: 100,
193
- // Process locally
194
- onData: async (chunk, next) => {
195
- console.log(`Processing chunk ${chunk.currentChunkIndex + 1}/${chunk.totalChunks}`);
196
- // Your local processing logic
197
- await processChunkLocally(chunk.records);
198
- next();
199
- },
200
- // Also deliver to webhook
201
- webhook: {
202
- url: 'https://api.example.com/webhooks/csv-import',
203
- headers: {
204
- 'Authorization': 'Bearer your-api-token',
205
- },
206
- },
207
- onComplete: () => {
208
- console.log('All chunks processed and webhook delivery initiated');
209
- },
210
- });
211
- };
212
-
213
- return (
214
- <button onClick={handleImport}>
215
- Import CSV
216
- </button>
217
- );
218
- }
219
303
  ```
220
304
 
221
- ### Advanced Example with Error Handling
305
+ ## Advanced Example
222
306
 
223
307
  ```tsx
224
- import { useExpressCSV, x, type Infer } from '@expresscsv/react';
225
- import { useState } from 'react';
308
+ import { useExpressCSV, x, type Infer } from "@expresscsv/react";
309
+ import { useState } from "react";
226
310
 
227
311
  const candidateSchema = x.row({
228
- firstName: x.string().label('First Name'),
229
- lastName: x.string().label('Last Name'),
230
- email: x.string().email().label('Email'),
231
- experienceLevel: x.select([
232
- { label: 'Entry Level', value: 'entry' },
233
- { label: 'Mid Level', value: 'mid' },
234
- { label: 'Senior Level', value: 'senior' },
235
- ]).label('Experience Level'),
236
- salary: x.number().currency('USD').min(30000).label('Expected Salary'),
312
+ firstName: x.string().label("First Name"),
313
+ lastName: x.string().label("Last Name"),
314
+ email: x.string().email().label("Email"),
315
+ level: x
316
+ .select([
317
+ { label: "Entry Level", value: "entry" },
318
+ { label: "Mid Level", value: "mid" },
319
+ { label: "Senior Level", value: "senior" },
320
+ ])
321
+ .label("Experience Level"),
322
+ salary: x.number().currency("USD").min(30000).label("Expected Salary"),
237
323
  });
238
324
 
239
325
  function CandidateImporter() {
240
- const [isLoading, setIsLoading] = useState(false);
241
- const [error, setError] = useState<string | null>(null);
326
+ const [status, setStatus] = useState<string | null>(null);
242
327
 
243
- const { open } = useExpressCSV({
328
+ const { open, isOpen } = useExpressCSV({
244
329
  schema: candidateSchema,
245
- title: "Import Candidates",
330
+ publishableKey: "your-publishable-key",
246
331
  importIdentifier: "candidate-import",
247
- developerMode: process.env.NODE_ENV === 'development',
248
- debug: process.env.NODE_ENV === 'development',
332
+ title: "Import Candidates",
333
+ developerMode: process.env.NODE_ENV === "development",
249
334
  });
250
335
 
251
336
  const handleImport = () => {
252
- setIsLoading(true);
253
- setError(null);
337
+ setStatus(null);
254
338
 
255
339
  open({
256
340
  onData: async (chunk, next) => {
257
- try {
258
- // Process the candidates chunk
259
- await importCandidates(chunk.records);
260
- next();
261
- } catch (err) {
262
- setError('Failed to import candidates. Please try again.');
263
- throw err;
264
- }
341
+ setStatus(
342
+ `Processing chunk ${chunk.currentChunkIndex + 1}/${chunk.totalChunks}...`
343
+ );
344
+ await importCandidates(chunk.records);
345
+ next();
265
346
  },
266
347
  onComplete: () => {
267
- setIsLoading(false);
268
- alert('Successfully imported all candidates!');
348
+ setStatus("Import complete!");
269
349
  },
270
350
  onError: (error) => {
271
- setIsLoading(false);
272
- setError(`Import failed: ${error.message}`);
351
+ setStatus(`Error: ${error.message}`);
273
352
  },
274
353
  onCancel: () => {
275
- setIsLoading(false);
354
+ setStatus(null);
276
355
  },
277
356
  });
278
357
  };
279
358
 
280
359
  return (
281
360
  <div>
282
- {error && <div className="error">{error}</div>}
283
-
284
- <button
285
- onClick={handleImport}
286
- disabled={isLoading}
287
- className="import-button"
288
- >
289
- {isLoading ? 'Processing...' : 'Import Candidates'}
361
+ <button onClick={handleImport} disabled={isOpen}>
362
+ {isOpen ? "Importing..." : "Import Candidates"}
290
363
  </button>
364
+ {status && <p>{status}</p>}
291
365
  </div>
292
366
  );
293
367
  }
@@ -295,126 +369,196 @@ function CandidateImporter() {
295
369
 
296
370
  ## API Reference
297
371
 
298
- ### useExpressCSV Hook
299
-
300
- A custom hook that provides access to the CSV importer functionality.
301
-
302
- #### Parameters
303
-
304
- ```tsx
305
- interface UseExpressCSVOptions<TSchema> {
306
- schema: TSchema;
307
- title?: string;
308
- importIdentifier: string;
309
- publishableKey: string;
310
- debug?: boolean;
311
- developerMode?: boolean;
312
- preload?: boolean; // Defaults to true
313
- theme?: ECSVTheme;
314
- colorMode?: ColorModeConfig;
315
- customCSS?: string;
316
- fonts?: Record<string, ECSVFontSource>;
317
- stepDisplay?: 'progressBar' | 'segmented' | 'numbered';
318
- }
319
- ```
320
-
321
- #### Returns
372
+ ### `useExpressCSV(options)`
322
373
 
323
- ```tsx
324
- {
374
+ ```typescript
375
+ function useExpressCSV<TSchema>(
376
+ options: UseExpressCSVOptions<TSchema>
377
+ ): {
325
378
  open: (options: OpenOptions<Infer<TSchema>>) => void;
326
379
  widgetState: WidgetState;
327
380
  isInitialising: boolean;
328
381
  isOpen: boolean;
329
- }
382
+ };
330
383
  ```
331
384
 
332
- ### OpenOptions
333
-
334
- Options passed to the `open` method:
335
-
336
- ```tsx
337
- interface OpenOptions<T> {
338
- // At least one of onData or webhook must be provided
339
- onData?: (chunk: RecordsChunk<T>, next: () => void) => void | Promise<void>;
340
- webhook?: WebhookConfig;
341
-
342
- // Optional configuration
343
- chunkSize?: number; // Default: 1000
344
- onComplete?: () => void;
345
- onCancel?: () => void;
346
- onError?: (error: Error) => void;
385
+ #### Options
386
+
387
+ | Option | Type | Required | Default | Description |
388
+ |---|---|---|---|---|
389
+ | `schema` | Schema | Yes | - | Schema definition created with `x.row()` |
390
+ | `publishableKey` | `string` | Yes | - | Your publishable key from the [dashboard](https://expresscsv.com) |
391
+ | `importIdentifier` | `string` | Yes | - | Unique identifier for this import type |
392
+ | `title` | `string` | No | - | Title shown in the widget header |
393
+ | `preload` | `boolean` | No | `true` | Preload widget for instant display |
394
+ | `debug` | `boolean` | No | `false` | Enable debug logging |
395
+ | `developerMode` | `boolean` | No | `false` | Enable developer mode features |
396
+ | `theme` | `ECSVTheme` | No | - | Custom theme configuration |
397
+ | `colorMode` | `ColorModePref` | No | - | Light/dark mode (`'light'`, `'dark'`, or `'system'`) |
398
+ | `customCSS` | `string` | No | - | Custom CSS to inject into the widget |
399
+ | `fonts` | `Record<string, ECSVFontSource>` | No | - | Custom font sources |
400
+ | `stepDisplay` | `'progressBar' \| 'segmented' \| 'numbered'` | No | `'progressBar'` | Step indicator style |
401
+ | `previewSchemaBeforeUpload` | `boolean` | No | `true` | Show schema preview before upload |
402
+ | `templateDownload` | `TemplateDownloadConfig` | No | - | Template download configuration |
403
+ | `saveSession` | `boolean` | No | - | Persist session state |
404
+ | `locale` | `DeepPartial<ExpressCSVLocaleInput>` | No | - | Localization overrides |
405
+
406
+ #### Return Value
407
+
408
+ | Property | Type | Description |
409
+ |---|---|---|
410
+ | `open` | `(options: OpenOptions) => void` | Opens the widget. Requires at least one of `onData` or `webhook`. |
411
+ | `widgetState` | `WidgetState` | Current widget state (reactive) |
412
+ | `isInitialising` | `boolean` | `true` while the widget is initializing or opening |
413
+ | `isOpen` | `boolean` | `true` while the widget is open |
414
+
415
+ ### `OpenOptions<T>`
416
+
417
+ Options passed to `open()`. At least one of `onData` or `webhook` must be provided.
418
+
419
+ | Option | Type | Required | Description |
420
+ |---|---|---|---|
421
+ | `onData` | `(chunk: RecordsChunk<T>, next: () => void) => void` | * | Callback for each chunk. Call `next()` to continue. |
422
+ | `webhook` | `WebhookConfig` | * | Webhook endpoint for server-side delivery |
423
+ | `chunkSize` | `number` | No | Records per chunk (default: 1000) |
424
+ | `onComplete` | `() => void` | No | Called when all chunks have been processed |
425
+ | `onCancel` | `() => void` | No | Called when the user cancels the import |
426
+ | `onError` | `(error: Error) => void` | No | Called when an error occurs |
427
+ | `onWidgetOpen` | `() => void` | No | Called when the widget opens |
428
+ | `onWidgetClose` | `(reason: string) => void` | No | Called when the widget closes |
429
+ | `onStepChange` | `(stepId, previousStepId?) => void` | No | Called when the wizard step changes |
430
+
431
+ \* At least one of `onData` or `webhook` is required.
432
+
433
+ ### `RecordsChunk<T>`
434
+
435
+ ```typescript
436
+ interface RecordsChunk<T> {
437
+ records: T[]; // Automatically typed to your schema
438
+ totalChunks: number;
439
+ currentChunkIndex: number;
440
+ totalRecords: number;
347
441
  }
348
442
  ```
349
443
 
350
- ### WebhookConfig
444
+ ### `WebhookConfig`
351
445
 
352
- Configuration for webhook delivery:
353
-
354
- ```tsx
446
+ ```typescript
355
447
  interface WebhookConfig {
356
- url: string; // Required: Webhook endpoint URL
357
- headers?: Record<string, string>; // Optional HTTP headers
358
- method?: 'POST' | 'PUT' | 'PATCH'; // Default: 'POST'
359
- timeout?: number; // Request timeout in milliseconds (default: 30000)
360
- retries?: number; // Number of retry attempts (default: 0)
361
- metadata?: Record<string, unknown>; // Optional metadata to include in webhook payload
448
+ url: string;
449
+ headers?: Record<string, string>;
450
+ method?: "POST" | "PUT" | "PATCH";
451
+ timeout?: number;
452
+ retries?: number;
453
+ metadata?: Record<string, unknown>;
362
454
  }
363
455
  ```
364
456
 
365
- **Note:** The `chunkSize` option in `OpenOptions` controls the chunk size for both `onData` callbacks and webhook delivery. The backend will deliver webhooks in chunks of this size (or default 1000 if not specified).
366
-
367
457
  ### Schema Builder
368
458
 
369
- The schema builder (`x`) provides a fluent API for defining field validation:
459
+ The `x` schema builder provides a type-safe, fluent API for defining your CSV structure.
460
+
461
+ #### Field Types
370
462
 
371
463
  ```tsx
464
+ import { x } from "@expresscsv/react";
465
+
372
466
  const schema = x.row({
373
- // String fields
374
- name: x.string().label('Name').minLength(2).maxLength(50),
375
-
376
- // Email validation
377
- email: x.string().email().label('Email Address'),
378
-
379
- // Number fields with constraints
380
- age: x.number().label('Age').min(18).max(100),
381
-
382
- // Currency fields
383
- salary: x.number().currency('USD').min(30000),
384
-
385
- // Select dropdowns
467
+ // Strings with validation
468
+ name: x.string().label("Full Name").min(2).max(100),
469
+ email: x.string().email().label("Email"),
470
+ website: x.string().url().label("Website").optional(),
471
+
472
+ // Numbers with constraints
473
+ age: x.number().label("Age").min(0).max(150).integer(),
474
+ salary: x.number().currency("USD").label("Salary").min(0),
475
+
476
+ // Boolean
477
+ isActive: x.boolean().label("Active"),
478
+
479
+ // Dates and times
480
+ startDate: x.date().label("Start Date"),
481
+ createdAt: x.datetime().label("Created At"),
482
+
483
+ // Single selection (requires { label, value } objects)
386
484
  role: x.select([
387
- { label: 'Admin', value: 'admin' },
388
- { label: 'User', value: 'user' },
389
- ]).label('Role'),
390
-
391
- // Date fields
392
- startDate: x.date().label('Start Date'),
393
-
394
- // Boolean fields
395
- isActive: x.boolean().label('Active Status'),
485
+ { label: "Admin", value: "admin" },
486
+ { label: "Editor", value: "editor" },
487
+ { label: "Viewer", value: "viewer" },
488
+ ]).label("Role"),
489
+
490
+ // Multi-selection
491
+ tags: x.multiselect([
492
+ { label: "Engineering", value: "eng" },
493
+ { label: "Design", value: "design" },
494
+ { label: "Marketing", value: "mkt" },
495
+ ]).label("Tags"),
396
496
  });
397
497
  ```
398
498
 
399
- ## TypeScript Support
400
-
401
- The component provides full TypeScript support with automatic type inference:
499
+ #### Common Modifiers
500
+
501
+ All field types support:
502
+
503
+ | Modifier | Description |
504
+ |---|---|
505
+ | `.label(text)` | User-facing label shown in the widget |
506
+ | `.description(text)` | Help text for the field |
507
+ | `.example(text)` | Example value shown as placeholder |
508
+ | `.optional()` | Makes the field optional (default is required) |
509
+ | `.refine(fn)` | Custom validation function |
510
+
511
+ #### String Modifiers
512
+
513
+ | Modifier | Description |
514
+ |---|---|
515
+ | `.email()` | Validates email format |
516
+ | `.url()` | Validates URL format |
517
+ | `.uuid()` | Validates UUID format |
518
+ | `.ip()` | Validates IP address |
519
+ | `.phone()` | Validates phone number |
520
+ | `.regex(pattern)` | Matches a regular expression |
521
+ | `.min(n)` | Minimum string length |
522
+ | `.max(n)` | Maximum string length |
523
+ | `.length(n)` | Exact string length |
524
+ | `.includes(str)` | Must contain substring |
525
+ | `.startsWith(str)` | Must start with prefix |
526
+ | `.endsWith(str)` | Must end with suffix |
527
+
528
+ #### Number Modifiers
529
+
530
+ | Modifier | Description |
531
+ |---|---|
532
+ | `.min(n)` | Minimum value |
533
+ | `.max(n)` | Maximum value |
534
+ | `.integer()` | Must be a whole number |
535
+ | `.multipleOf(n)` | Must be a multiple of n |
536
+ | `.currency(code)` | Formats as currency (e.g. `"USD"`) |
537
+ | `.percentage()` | Formats as percentage |
538
+
539
+ ## TypeScript
540
+
541
+ Full type inference from your schema is built in:
402
542
 
403
543
  ```tsx
544
+ import { x, type Infer } from "@expresscsv/react";
545
+
404
546
  const schema = x.row({
405
547
  name: x.string(),
406
548
  age: x.number(),
407
549
  });
408
550
 
409
- // results is automatically typed as { name: string; age: number }[]
410
- const handleResults = (results: Infer<typeof schema>[]) => {
411
- results.forEach(row => {
412
- console.log(row.name); // string
413
- console.log(row.age); // number
414
- });
415
- };
551
+ type Row = Infer<typeof schema>;
552
+ // { name: string; age: number }
416
553
  ```
417
554
 
555
+ The `onData` callback receives `RecordsChunk<Row>` automatically -- no manual type annotations needed.
556
+
557
+ ## Resources
558
+
559
+ - [ExpressCSV Dashboard](https://expresscsv.com) -- manage your imports and API keys
560
+ - [`@expresscsv/sdk`](https://www.npmjs.com/package/@expresscsv/sdk) -- vanilla JS SDK (no React dependency)
561
+
418
562
  ## License
419
563
 
420
- ISC
564
+ [MIT](./LICENSE)
package/dist/index.d.mts CHANGED
@@ -4,15 +4,6 @@ declare interface BICOptions {
4
4
 
5
5
  declare type BooleanControlType = 'toggle' | 'checkbox' | 'dropdown';
6
6
 
7
- /**
8
- * Color mode configuration
9
- */
10
- export declare interface ColorModeConfig {
11
- default?: ColorModePref;
12
- persist?: boolean;
13
- onChange?: (mode: ColorModePref) => void;
14
- }
15
-
16
7
  /**
17
8
  * Color mode preference
18
9
  */
@@ -3744,7 +3735,7 @@ export declare interface UseExpressCSVOptions<TSchema extends ExType<unknown, Ex
3744
3735
  developerMode?: boolean;
3745
3736
  preload?: boolean;
3746
3737
  theme?: ECSVTheme;
3747
- colorMode?: ColorModeConfig;
3738
+ colorMode?: ColorModePref;
3748
3739
  customCSS?: string;
3749
3740
  fonts?: Record<string, ECSVFontSource>;
3750
3741
  stepDisplay?: 'progressBar' | 'segmented' | 'numbered';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@expresscsv/react",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "React component wrapper for ExpressCSV SDK",
5
5
  "module": "dist/index.mjs",
6
6
  "types": "dist/index.d.mts",
@@ -25,6 +25,6 @@
25
25
  "access": "public"
26
26
  },
27
27
  "peerDependencies": {
28
- "react": "^18.0.0"
28
+ "react": ">=16.8.0"
29
29
  }
30
30
  }
@@ -1,27 +0,0 @@
1
- import type { DeepPartial, ExpressCSVLocaleInput, TemplateDownloadConfig } from '@expresscsv/core';
2
- import { type ColorModeConfig, type ECSVFontSource, type ECSVTheme, type ExBaseDef, type ExType, type Infer, type OpenOptions, WidgetState } from '@expresscsv/sdk';
3
- export type { ColorModeConfig, ColorModePref, ECSVTheme, TailwindThemeVars, } from '@expresscsv/sdk';
4
- export interface UseExpressCSVOptions<TSchema extends ExType<unknown, ExBaseDef, unknown>> {
5
- schema: TSchema;
6
- title?: string;
7
- importIdentifier: string;
8
- publishableKey: string;
9
- debug?: boolean;
10
- developerMode?: boolean;
11
- preload?: boolean;
12
- theme?: ECSVTheme;
13
- colorMode?: ColorModeConfig;
14
- customCSS?: string;
15
- fonts?: Record<string, ECSVFontSource>;
16
- stepDisplay?: 'progressBar' | 'segmented' | 'numbered';
17
- previewSchemaBeforeUpload?: boolean;
18
- templateDownload?: TemplateDownloadConfig;
19
- saveSession?: boolean;
20
- locale?: DeepPartial<ExpressCSVLocaleInput>;
21
- }
22
- export declare function useExpressCSV<TSchema extends ExType<unknown, ExBaseDef, unknown>>({ schema, title, importIdentifier, publishableKey, debug, developerMode, preload, theme, colorMode, customCSS, fonts, stepDisplay, previewSchemaBeforeUpload, templateDownload, saveSession, locale, }: UseExpressCSVOptions<TSchema>): {
23
- open: (options: OpenOptions<Infer<TSchema>>) => void;
24
- widgetState: WidgetState;
25
- isInitialising: boolean;
26
- isOpen: boolean;
27
- };
@@ -1,4 +0,0 @@
1
- export { useExpressCSV, type UseExpressCSVOptions, } from './CSVImporter';
2
- export { x, WidgetState, WidgetMode, ImportCancelledError, } from '@expresscsv/sdk';
3
- export type { Infer, ECSVFontSource, ECSVTheme, TailwindThemeVars, ColorModeConfig, ColorModePref, ExpressCSVStep, RecordsChunk, OpenOptions, WebhookConfig, DeliveryOptions, } from '@expresscsv/sdk';
4
- export type { TemplateDownloadConfig, TemplateDownloadFormat, ExpressCSVLocaleInput, DeepPartial, } from '@expresscsv/core';