@shohojdhara/atomix 0.3.7 → 0.3.8

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 (53) hide show
  1. package/dist/atomix.css +77 -0
  2. package/dist/atomix.css.map +1 -1
  3. package/dist/atomix.min.css +77 -0
  4. package/dist/atomix.min.css.map +1 -1
  5. package/dist/charts.js.map +1 -1
  6. package/dist/core.d.ts +2 -2
  7. package/dist/core.js.map +1 -1
  8. package/dist/forms.js.map +1 -1
  9. package/dist/heavy.js.map +1 -1
  10. package/dist/index.d.ts +578 -515
  11. package/dist/index.esm.js +3157 -2626
  12. package/dist/index.esm.js.map +1 -1
  13. package/dist/index.js +10496 -9973
  14. package/dist/index.js.map +1 -1
  15. package/dist/index.min.js +1 -1
  16. package/dist/index.min.js.map +1 -1
  17. package/dist/theme.d.ts +237 -420
  18. package/dist/theme.js +1629 -1701
  19. package/dist/theme.js.map +1 -1
  20. package/package.json +1 -1
  21. package/src/components/DataTable/DataTable.stories.tsx +238 -0
  22. package/src/components/DataTable/DataTable.test.tsx +450 -0
  23. package/src/components/DataTable/DataTable.tsx +384 -61
  24. package/src/components/DatePicker/DatePicker.tsx +29 -38
  25. package/src/components/Upload/Upload.tsx +539 -40
  26. package/src/lib/composables/useDataTable.ts +355 -15
  27. package/src/lib/composables/useDatePicker.ts +19 -0
  28. package/src/lib/constants/components.ts +10 -0
  29. package/src/lib/theme/adapters/cssVariableMapper.ts +29 -14
  30. package/src/lib/theme/adapters/index.ts +1 -4
  31. package/src/lib/theme/config/configLoader.ts +53 -35
  32. package/src/lib/theme/core/composeTheme.ts +22 -30
  33. package/src/lib/theme/core/createTheme.ts +49 -26
  34. package/src/lib/theme/core/index.ts +0 -1
  35. package/src/lib/theme/generators/generateCSSNested.ts +4 -3
  36. package/src/lib/theme/generators/generateCSSVariables.ts +24 -16
  37. package/src/lib/theme/index.ts +10 -17
  38. package/src/lib/theme/runtime/ThemeApplicator.ts +6 -109
  39. package/src/lib/theme/runtime/ThemeErrorBoundary.tsx +3 -3
  40. package/src/lib/theme/runtime/ThemeProvider.tsx +186 -44
  41. package/src/lib/theme/runtime/useTheme.ts +1 -1
  42. package/src/lib/theme/runtime/useThemeTokens.ts +7 -16
  43. package/src/lib/theme/test/testTheme.ts +2 -1
  44. package/src/lib/theme/types.ts +14 -14
  45. package/src/lib/theme/utils/componentTheming.ts +35 -27
  46. package/src/lib/theme/utils/domUtils.ts +57 -15
  47. package/src/lib/theme/utils/injectCSS.ts +0 -1
  48. package/src/lib/theme/utils/themeHelpers.ts +1 -39
  49. package/src/lib/theme/utils/themeUtils.ts +1 -170
  50. package/src/lib/types/components.ts +145 -0
  51. package/src/lib/utils/dataTableExport.ts +143 -0
  52. package/src/styles/06-components/_components.data-table.scss +95 -0
  53. package/src/lib/hooks/useThemeTokens.ts +0 -105
@@ -7,6 +7,7 @@
7
7
 
8
8
  import type { ThemeMetadata, ThemeValidationResult } from '../types';
9
9
  import { THEME_LINK_ID_PREFIX } from '../constants/constants';
10
+ import { ThemeError, ThemeErrorCode } from '../errors/errors';
10
11
 
11
12
  /**
12
13
  * Check if code is running in a browser environment
@@ -29,6 +30,20 @@ export const getThemeLinkId = (themeName: string): string => {
29
30
  return `${THEME_LINK_ID_PREFIX}${themeName}`;
30
31
  };
31
32
 
33
+ /**
34
+ * Sanitize path to prevent path injection attacks
35
+ *
36
+ * @param path - Path to sanitize
37
+ * @returns Sanitized path
38
+ */
39
+ const sanitizePath = (path: string): string => {
40
+ return path
41
+ .replace(/[<>"']/g, '') // Remove dangerous characters
42
+ .replace(/\.\./g, '') // Remove path traversal attempts
43
+ .replace(/\/+/g, '/') // Normalize multiple slashes
44
+ .replace(/^\/+|\/+$/g, ''); // Trim leading/trailing slashes
45
+ };
46
+
32
47
  /**
33
48
  * Build the CSS file path for a theme
34
49
  *
@@ -37,6 +52,7 @@ export const getThemeLinkId = (themeName: string): string => {
37
52
  * @param useMinified - Whether to use minified CSS
38
53
  * @param cdnPath - Optional CDN path
39
54
  * @returns Full path to the theme CSS file
55
+ * @throws Error if theme name is invalid
40
56
  */
41
57
  export const buildThemePath = (
42
58
  themeName: string,
@@ -46,21 +62,24 @@ export const buildThemePath = (
46
62
  ): string => {
47
63
  // Validate theme name to prevent path injection
48
64
  if (!isValidThemeName(themeName)) {
49
- throw new Error(`Invalid theme name: "${themeName}". Theme names must be lowercase alphanumeric with hyphens.`);
65
+ throw new ThemeError(
66
+ `Invalid theme name: "${themeName}". Theme names must be lowercase alphanumeric with hyphens (e.g., "my-theme").`,
67
+ ThemeErrorCode.INVALID_THEME_NAME,
68
+ { themeName, pattern: /^[a-z0-9]+(-[a-z0-9]+)*$/ }
69
+ );
50
70
  }
51
71
 
52
72
  const extension = useMinified ? '.min.css' : '.css';
53
73
  const fileName = `${themeName}${extension}`;
54
74
 
55
75
  if (cdnPath) {
56
- // Validate CDN path doesn't contain dangerous characters
57
- const cleanCdnPath = cdnPath.replace(/[<>"']/g, '');
76
+ // Sanitize CDN path to prevent path injection
77
+ const cleanCdnPath = sanitizePath(cdnPath);
58
78
  return `${cleanCdnPath}/${fileName}`;
59
79
  }
60
80
 
61
- // Ensure basePath doesn't end with slash and fileName doesn't start with slash
62
- // Also sanitize basePath to prevent path injection
63
- const cleanBasePath = basePath.replace(/\/$/, '').replace(/[<>"']/g, '');
81
+ // Sanitize basePath to prevent path injection
82
+ const cleanBasePath = sanitizePath(basePath);
64
83
  const cleanFileName = fileName.replace(/^\//, '');
65
84
 
66
85
  return `${cleanBasePath}/${cleanFileName}`;
@@ -110,7 +129,11 @@ export const loadThemeCSS = (
110
129
  link.onerror = () => {
111
130
  // Remove failed link element
112
131
  link.remove();
113
- reject(new Error(`Failed to load theme CSS: ${fullPath}`));
132
+ reject(new ThemeError(
133
+ `Failed to load theme CSS from: ${fullPath}. Please check that the file exists and is accessible.`,
134
+ ThemeErrorCode.THEME_LOAD_FAILED,
135
+ { fullPath, linkId }
136
+ ));
114
137
  };
115
138
 
116
139
  // Append to head
@@ -166,8 +189,10 @@ export const applyThemeAttributes = (
166
189
  return;
167
190
  }
168
191
 
169
- // Set data attribute on body
170
- document.body.setAttribute(dataAttribute, themeName);
192
+ // Set data attribute on body (with null check)
193
+ if (document.body) {
194
+ document.body.setAttribute(dataAttribute, themeName);
195
+ }
171
196
 
172
197
  // Also set on documentElement for broader compatibility
173
198
  document.documentElement.setAttribute(dataAttribute, themeName);
@@ -185,7 +210,12 @@ export const removeThemeAttributes = (
185
210
  return;
186
211
  }
187
212
 
188
- document.body.removeAttribute(dataAttribute);
213
+ // Remove from body (with null check)
214
+ if (document.body) {
215
+ document.body.removeAttribute(dataAttribute);
216
+ }
217
+
218
+ // Remove from documentElement
189
219
  document.documentElement.removeAttribute(dataAttribute);
190
220
  };
191
221
 
@@ -202,8 +232,10 @@ export const getCurrentThemeFromDOM = (
202
232
  return null;
203
233
  }
204
234
 
205
- return document.body.getAttribute(dataAttribute) ||
206
- document.documentElement.getAttribute(dataAttribute);
235
+ // Add null checks for SSR safety
236
+ const bodyTheme = document.body?.getAttribute(dataAttribute);
237
+ const htmlTheme = document.documentElement?.getAttribute(dataAttribute);
238
+ return bodyTheme || htmlTheme || null;
207
239
  };
208
240
 
209
241
  /**
@@ -377,15 +409,15 @@ export const createLocalStorageAdapter = () => {
377
409
  *
378
410
  * @param func - Function to debounce
379
411
  * @param wait - Wait time in milliseconds
380
- * @returns Debounced function
412
+ * @returns Debounced function with cancel method
381
413
  */
382
414
  export const debounce = <T extends (...args: any[]) => any>(
383
415
  func: T,
384
416
  wait: number
385
- ): ((...args: Parameters<T>) => void) => {
417
+ ): ((...args: Parameters<T>) => void) & { cancel: () => void } => {
386
418
  let timeout: ReturnType<typeof setTimeout> | null = null;
387
419
 
388
- return function executedFunction(...args: Parameters<T>) {
420
+ const debounced = function executedFunction(...args: Parameters<T>) {
389
421
  const later = () => {
390
422
  timeout = null;
391
423
  func(...args);
@@ -396,4 +428,14 @@ export const debounce = <T extends (...args: any[]) => any>(
396
428
  }
397
429
  timeout = setTimeout(later, wait);
398
430
  };
431
+
432
+ // Add cancel method for cleanup
433
+ debounced.cancel = () => {
434
+ if (timeout !== null) {
435
+ clearTimeout(timeout);
436
+ timeout = null;
437
+ }
438
+ };
439
+
440
+ return debounced;
399
441
  };
@@ -34,7 +34,6 @@ export function injectCSS(
34
34
  id: string = 'atomix-theme'
35
35
  ): void {
36
36
  if (!isBrowser()) {
37
- console.warn('injectCSS: Not in browser environment, CSS not injected');
38
37
  return;
39
38
  }
40
39
 
@@ -1,34 +1,10 @@
1
1
  /**
2
2
  * Theme Helper Functions
3
3
  *
4
- * Utility functions for working with themes and DesignTokens
4
+ * Utility functions for working with DesignTokens
5
5
  */
6
6
 
7
- import type { Theme } from '../types';
8
7
  import type { DesignTokens } from '../tokens/tokens';
9
- import { createDesignTokensFromTheme } from '../adapters/themeAdapter';
10
-
11
- /**
12
- * Get DesignTokens from current theme
13
- *
14
- * Converts a Theme object to DesignTokens. Useful when you need to
15
- * work with DesignTokens but have a Theme object.
16
- *
17
- * @param theme - Theme object to convert
18
- * @returns DesignTokens object
19
- *
20
- * @example
21
- * ```typescript
22
- * // If you have a Theme object, convert it to DesignTokens
23
- * const tokens = getDesignTokensFromTheme(theme);
24
- * // Now you can use tokens with unified theme system
25
- * const css = createTheme(tokens);
26
- * ```
27
- */
28
- export function getDesignTokensFromTheme(theme: Theme | null): DesignTokens | null {
29
- if (!theme) return null;
30
- return createDesignTokensFromTheme(theme);
31
- }
32
8
 
33
9
  /**
34
10
  * Check if a value is DesignTokens
@@ -62,17 +38,3 @@ export function isDesignTokens(value: unknown): value is DesignTokens {
62
38
  return hasDesignTokenKeys;
63
39
  }
64
40
 
65
- /**
66
- * Check if a value is a Theme object
67
- *
68
- * Type guard to check if an object is a Theme.
69
- *
70
- * @param value - Value to check
71
- * @returns True if value is Theme
72
- */
73
- export function isThemeObject(value: unknown): value is Theme {
74
- if (!value || typeof value !== 'object') return false;
75
- const obj = value as Record<string, unknown>;
76
- return '__isJSTheme' in obj || ('palette' in obj && 'typography' in obj);
77
- }
78
-
@@ -5,7 +5,7 @@
5
5
  * spacing helpers, and theme value accessors.
6
6
  */
7
7
 
8
- import type { Theme, SpacingFunction, SpacingOptions } from '../types';
8
+ import type { SpacingFunction, SpacingOptions } from '../types';
9
9
 
10
10
  // ============================================================================
11
11
  // Color Manipulation Utilities
@@ -184,172 +184,3 @@ export function createSpacing(spacingInput: SpacingOptions = 4): SpacingFunction
184
184
  };
185
185
  }
186
186
 
187
- /**
188
- * Get spacing value from theme
189
- *
190
- * @param theme - Theme object
191
- * @param values - Spacing multipliers
192
- * @returns Spacing string
193
- */
194
- export function spacing(theme: Theme, ...values: number[]): string {
195
- return theme.spacing(...values);
196
- }
197
-
198
- // ============================================================================
199
- // Theme Value Accessors
200
- // ============================================================================
201
-
202
- /**
203
- * Safely get a nested value from theme using dot notation
204
- *
205
- * @param theme - Theme object
206
- * @param path - Dot-notation path (e.g., 'palette.primary.main')
207
- * @param fallback - Fallback value if path not found
208
- * @returns Theme value or fallback
209
- */
210
- export function getThemeValue<T = unknown>(theme: Theme, path: string, fallback?: T): T {
211
- const keys = path.split('.');
212
- let value: unknown = theme;
213
-
214
- for (const key of keys) {
215
- if (value && typeof value === 'object' && key in value) {
216
- value = (value as Record<string, unknown>)[key];
217
- } else {
218
- return fallback as T;
219
- }
220
- }
221
-
222
- return value as T;
223
- }
224
-
225
- /**
226
- * Check if a theme is a JS theme (created with createTheme)
227
- */
228
- export function isJSTheme(theme: unknown): theme is Theme {
229
- return typeof theme === 'object' && theme !== null && '__isJSTheme' in theme && theme.__isJSTheme === true;
230
- }
231
-
232
- // ============================================================================
233
- // Responsive Utilities
234
- // ============================================================================
235
-
236
- /**
237
- * Get media query for breakpoint up
238
- */
239
- export function breakpointUp(theme: Theme, key: keyof Theme['breakpoints']['values'] | number): string {
240
- return theme.breakpoints.up(key);
241
- }
242
-
243
- /**
244
- * Get media query for breakpoint down
245
- */
246
- export function breakpointDown(theme: Theme, key: keyof Theme['breakpoints']['values'] | number): string {
247
- return theme.breakpoints.down(key);
248
- }
249
-
250
- /**
251
- * Get media query for breakpoint between
252
- */
253
- export function breakpointBetween(
254
- theme: Theme,
255
- start: keyof Theme['breakpoints']['values'] | number,
256
- end: keyof Theme['breakpoints']['values'] | number
257
- ): string {
258
- return theme.breakpoints.between(start, end);
259
- }
260
-
261
- // ============================================================================
262
- // Typography Utilities
263
- // ============================================================================
264
-
265
- /**
266
- * Get typography variant styles
267
- */
268
- export function getTypography(theme: Theme, variant: keyof Theme['typography']): Theme['typography'][keyof Theme['typography']] {
269
- return theme.typography[variant] ?? {};
270
- }
271
-
272
- /**
273
- * Convert rem to px based on theme font size
274
- */
275
- export function remToPx(theme: Theme, rem: number): number {
276
- return rem * theme.typography.fontSize;
277
- }
278
-
279
- /**
280
- * Convert px to rem based on theme font size
281
- */
282
- export function pxToRem(theme: Theme, px: number): string {
283
- return `${px / theme.typography.fontSize}rem`;
284
- }
285
-
286
- // ============================================================================
287
- // Shadow Utilities
288
- // ============================================================================
289
-
290
- /**
291
- * Get shadow value from theme
292
- */
293
- export function getShadow(theme: Theme, level: keyof Theme['shadows']): string {
294
- return theme.shadows[level] || 'none';
295
- }
296
-
297
- // ============================================================================
298
- // Transition Utilities
299
- // ============================================================================
300
-
301
- /**
302
- * Create a transition string
303
- */
304
- export function createTransition(
305
- theme: Theme,
306
- props: string | string[],
307
- options?: {
308
- duration?: keyof Theme['transitions']['duration'] | number;
309
- easing?: keyof Theme['transitions']['easing'] | string;
310
- delay?: number;
311
- }
312
- ): string {
313
- const properties = Array.isArray(props) ? props : [props];
314
- const duration =
315
- typeof options?.duration === 'number'
316
- ? options.duration
317
- : theme.transitions.duration[options?.duration || 'standard'];
318
- const easing =
319
- typeof options?.easing === 'string' && !options.easing.includes('(')
320
- ? theme.transitions.easing[options.easing as keyof Theme['transitions']['easing']]
321
- : options?.easing || theme.transitions.easing.easeInOut;
322
- const delay = options?.delay || 0;
323
-
324
- return properties
325
- .map((prop) => `${prop} ${duration}ms ${easing}${delay ? ` ${delay}ms` : ''}`)
326
- .join(', ');
327
- }
328
-
329
- /**
330
- * Get transition duration
331
- */
332
- export function getTransitionDuration(
333
- theme: Theme,
334
- key: keyof Theme['transitions']['duration']
335
- ): number {
336
- return theme.transitions.duration[key] ?? 300;
337
- }
338
-
339
- /**
340
- * Get transition easing
341
- */
342
- export function getTransitionEasing(theme: Theme, key: keyof Theme['transitions']['easing']): string {
343
- return theme.transitions.easing[key] ?? 'cubic-bezier(0.4, 0, 0.2, 1)';
344
- }
345
-
346
- // ============================================================================
347
- // Z-Index Utilities
348
- // ============================================================================
349
-
350
- /**
351
- * Get z-index value from theme
352
- */
353
- export function getZIndex(theme: Theme, key: keyof Theme['zIndex']): number {
354
- return theme.zIndex[key] ?? 0;
355
- }
@@ -1644,6 +1644,46 @@ export interface DataTableColumn {
1644
1644
  * Width of the column (CSS value)
1645
1645
  */
1646
1646
  width?: string;
1647
+
1648
+ /**
1649
+ * Minimum width for resizable columns (CSS value)
1650
+ */
1651
+ minWidth?: string;
1652
+
1653
+ /**
1654
+ * Maximum width for resizable columns (CSS value)
1655
+ */
1656
+ maxWidth?: string;
1657
+
1658
+ /**
1659
+ * Whether the column is resizable
1660
+ */
1661
+ resizable?: boolean;
1662
+
1663
+ /**
1664
+ * Whether the column is visible by default
1665
+ */
1666
+ visible?: boolean;
1667
+
1668
+ /**
1669
+ * Whether the column can be reordered
1670
+ */
1671
+ reorderable?: boolean;
1672
+
1673
+ /**
1674
+ * Custom filter function for column-specific filtering
1675
+ */
1676
+ filterFunction?: (value: any, filterValue: string) => boolean;
1677
+
1678
+ /**
1679
+ * Filter type for column-specific filtering
1680
+ */
1681
+ filterType?: 'text' | 'select' | 'date' | 'number' | 'custom';
1682
+
1683
+ /**
1684
+ * Options for select-type filters
1685
+ */
1686
+ filterOptions?: Array<{ label: string; value: any }>;
1647
1687
  }
1648
1688
 
1649
1689
  /**
@@ -1661,6 +1701,16 @@ export interface SortConfig {
1661
1701
  direction: 'asc' | 'desc';
1662
1702
  }
1663
1703
 
1704
+ /**
1705
+ * Row selection mode
1706
+ */
1707
+ export type SelectionMode = 'single' | 'multiple' | 'none';
1708
+
1709
+ /**
1710
+ * Export format
1711
+ */
1712
+ export type ExportFormat = 'csv' | 'excel' | 'json';
1713
+
1664
1714
  /**
1665
1715
  * DataTable component properties
1666
1716
  */
@@ -1735,6 +1785,101 @@ export interface DataTableProps extends BaseComponentProps {
1735
1785
  * Can be a boolean to enable with default settings, or an object with AtomixGlassProps to customize the effect
1736
1786
  */
1737
1787
  glass?: AtomixGlassProps | boolean;
1788
+
1789
+ /**
1790
+ * Row selection mode ('single', 'multiple', or 'none')
1791
+ */
1792
+ selectionMode?: SelectionMode;
1793
+
1794
+ /**
1795
+ * Selected row IDs (for controlled selection)
1796
+ */
1797
+ selectedRowIds?: (string | number)[];
1798
+
1799
+ /**
1800
+ * Callback when selection changes
1801
+ */
1802
+ onSelectionChange?: (selectedRows: any[], selectedIds: (string | number)[]) => void;
1803
+
1804
+ /**
1805
+ * Key to use as unique identifier for rows (defaults to 'id')
1806
+ */
1807
+ rowKey?: string | ((row: any) => string | number);
1808
+
1809
+ /**
1810
+ * Whether columns are resizable
1811
+ */
1812
+ resizable?: boolean;
1813
+
1814
+ /**
1815
+ * Whether columns can be reordered
1816
+ */
1817
+ reorderable?: boolean;
1818
+
1819
+ /**
1820
+ * Callback when column order changes
1821
+ */
1822
+ onColumnReorder?: (columnKeys: string[]) => void;
1823
+
1824
+ /**
1825
+ * Whether to show column visibility toggle
1826
+ */
1827
+ showColumnVisibility?: boolean;
1828
+
1829
+ /**
1830
+ * Callback when column visibility changes
1831
+ */
1832
+ onColumnVisibilityChange?: (visibleColumns: string[]) => void;
1833
+
1834
+ /**
1835
+ * Whether to enable sticky headers
1836
+ */
1837
+ stickyHeader?: boolean;
1838
+
1839
+ /**
1840
+ * Offset from top for sticky headers (CSS value)
1841
+ */
1842
+ stickyHeaderOffset?: string;
1843
+
1844
+ /**
1845
+ * Whether to enable virtual scrolling for large datasets
1846
+ */
1847
+ virtualScrolling?: boolean;
1848
+
1849
+ /**
1850
+ * Estimated row height for virtual scrolling (in pixels)
1851
+ */
1852
+ estimatedRowHeight?: number;
1853
+
1854
+ /**
1855
+ * Number of rows to render outside visible area (overscan)
1856
+ */
1857
+ overscan?: number;
1858
+
1859
+ /**
1860
+ * Whether to enable export functionality
1861
+ */
1862
+ exportable?: boolean;
1863
+
1864
+ /**
1865
+ * Export formats available
1866
+ */
1867
+ exportFormats?: ExportFormat[];
1868
+
1869
+ /**
1870
+ * Custom export filename
1871
+ */
1872
+ exportFilename?: string;
1873
+
1874
+ /**
1875
+ * Callback for custom export logic
1876
+ */
1877
+ onExport?: (format: ExportFormat, data: any[]) => void;
1878
+
1879
+ /**
1880
+ * Whether to show column-specific filters
1881
+ */
1882
+ columnFilters?: boolean;
1738
1883
  }
1739
1884
 
1740
1885
  /**
@@ -0,0 +1,143 @@
1
+ import { DataTableColumn, ExportFormat } from '../types/components';
2
+
3
+ /**
4
+ * Sanitize cell content to prevent CSV injection
5
+ */
6
+ function sanitizeCSVCell(cell: any): string {
7
+ const sanitized = String(cell ?? '').replace(/[\r\n\t]/g, ' ').replace(/"/g, '""');
8
+ // Prevent formula injection by prefixing dangerous characters
9
+ const dangerous = /^[=+\-@]/;
10
+ return dangerous.test(sanitized) ? `'${sanitized}` : sanitized;
11
+ }
12
+
13
+ /**
14
+ * Export data as CSV
15
+ */
16
+ export function exportToCSV(
17
+ data: any[],
18
+ columns: DataTableColumn[],
19
+ filename: string = 'data-table.csv'
20
+ ): void {
21
+ if (!data.length || !columns.length) return;
22
+
23
+ // Create headers
24
+ const headers = columns.map(col => col.title || col.key);
25
+
26
+ // Create rows
27
+ const rows = data.map(row => {
28
+ return columns.map(col => {
29
+ const value = row[col.key];
30
+ if (col.render) {
31
+ // For rendered cells, try to extract text content
32
+ // This is a simplified approach - in production you might want to handle React elements differently
33
+ return value ?? '';
34
+ }
35
+ return value ?? '';
36
+ });
37
+ });
38
+
39
+ // Convert to CSV string
40
+ const csvContent = [
41
+ headers.map(h => `"${sanitizeCSVCell(h)}"`).join(','),
42
+ ...rows.map(row => row.map(cell => `"${sanitizeCSVCell(cell)}"`).join(',')),
43
+ ].join('\n');
44
+
45
+ // Download
46
+ const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
47
+ const url = URL.createObjectURL(blob);
48
+ const link = document.createElement('a');
49
+ link.download = filename.endsWith('.csv') ? filename : `${filename}.csv`;
50
+ link.href = url;
51
+ link.click();
52
+ URL.revokeObjectURL(url);
53
+ }
54
+
55
+ /**
56
+ * Export data as JSON
57
+ */
58
+ export function exportToJSON(
59
+ data: any[],
60
+ filename: string = 'data-table.json'
61
+ ): void {
62
+ if (!data.length) return;
63
+
64
+ const jsonContent = JSON.stringify(data, null, 2);
65
+ const blob = new Blob([jsonContent], { type: 'application/json;charset=utf-8;' });
66
+ const url = URL.createObjectURL(blob);
67
+ const link = document.createElement('a');
68
+ link.download = filename.endsWith('.json') ? filename : `${filename}.json`;
69
+ link.href = url;
70
+ link.click();
71
+ URL.revokeObjectURL(url);
72
+ }
73
+
74
+ /**
75
+ * Export data as Excel (XLSX) - simplified version using CSV with .xlsx extension
76
+ * Note: For true Excel format, you would need a library like xlsx or exceljs
77
+ */
78
+ export function exportToExcel(
79
+ data: any[],
80
+ columns: DataTableColumn[],
81
+ filename: string = 'data-table.xlsx'
82
+ ): void {
83
+ // For now, we'll export as CSV but with .xlsx extension
84
+ // In a production environment, you'd want to use a library like 'xlsx' or 'exceljs'
85
+ // to create a proper Excel file
86
+ if (!data.length || !columns.length) return;
87
+
88
+ // Create headers
89
+ const headers = columns.map(col => col.title || col.key);
90
+
91
+ // Create rows
92
+ const rows = data.map(row => {
93
+ return columns.map(col => {
94
+ const value = row[col.key];
95
+ return value ?? '';
96
+ });
97
+ });
98
+
99
+ // Convert to CSV format (Excel can open CSV files)
100
+ const csvContent = [
101
+ headers.map(h => `"${sanitizeCSVCell(h)}"`).join(','),
102
+ ...rows.map(row => row.map(cell => `"${sanitizeCSVCell(cell)}"`).join(',')),
103
+ ].join('\n');
104
+
105
+ // Download with .xlsx extension (though it's actually CSV)
106
+ // In production, use a proper Excel library
107
+ const blob = new Blob([csvContent], {
108
+ type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
109
+ });
110
+ const url = URL.createObjectURL(blob);
111
+ const link = document.createElement('a');
112
+ link.download = filename.endsWith('.xlsx') ? filename : `${filename}.xlsx`;
113
+ link.href = url;
114
+ link.click();
115
+ URL.revokeObjectURL(url);
116
+ }
117
+
118
+ /**
119
+ * Export data in the specified format
120
+ */
121
+ export function exportData(
122
+ format: ExportFormat,
123
+ data: any[],
124
+ columns: DataTableColumn[],
125
+ filename?: string
126
+ ): void {
127
+ const defaultFilename = filename || 'data-table';
128
+
129
+ switch (format) {
130
+ case 'csv':
131
+ exportToCSV(data, columns, defaultFilename);
132
+ break;
133
+ case 'excel':
134
+ exportToExcel(data, columns, defaultFilename);
135
+ break;
136
+ case 'json':
137
+ exportToJSON(data, defaultFilename);
138
+ break;
139
+ default:
140
+ console.warn(`Unsupported export format: ${format}`);
141
+ }
142
+ }
143
+