@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.
- package/dist/atomix.css +77 -0
- package/dist/atomix.css.map +1 -1
- package/dist/atomix.min.css +77 -0
- package/dist/atomix.min.css.map +1 -1
- package/dist/charts.js.map +1 -1
- package/dist/core.d.ts +2 -2
- package/dist/core.js.map +1 -1
- package/dist/forms.js.map +1 -1
- package/dist/heavy.js.map +1 -1
- package/dist/index.d.ts +578 -515
- package/dist/index.esm.js +3157 -2626
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +10496 -9973
- package/dist/index.js.map +1 -1
- package/dist/index.min.js +1 -1
- package/dist/index.min.js.map +1 -1
- package/dist/theme.d.ts +237 -420
- package/dist/theme.js +1629 -1701
- package/dist/theme.js.map +1 -1
- package/package.json +1 -1
- package/src/components/DataTable/DataTable.stories.tsx +238 -0
- package/src/components/DataTable/DataTable.test.tsx +450 -0
- package/src/components/DataTable/DataTable.tsx +384 -61
- package/src/components/DatePicker/DatePicker.tsx +29 -38
- package/src/components/Upload/Upload.tsx +539 -40
- package/src/lib/composables/useDataTable.ts +355 -15
- package/src/lib/composables/useDatePicker.ts +19 -0
- package/src/lib/constants/components.ts +10 -0
- package/src/lib/theme/adapters/cssVariableMapper.ts +29 -14
- package/src/lib/theme/adapters/index.ts +1 -4
- package/src/lib/theme/config/configLoader.ts +53 -35
- package/src/lib/theme/core/composeTheme.ts +22 -30
- package/src/lib/theme/core/createTheme.ts +49 -26
- package/src/lib/theme/core/index.ts +0 -1
- package/src/lib/theme/generators/generateCSSNested.ts +4 -3
- package/src/lib/theme/generators/generateCSSVariables.ts +24 -16
- package/src/lib/theme/index.ts +10 -17
- package/src/lib/theme/runtime/ThemeApplicator.ts +6 -109
- package/src/lib/theme/runtime/ThemeErrorBoundary.tsx +3 -3
- package/src/lib/theme/runtime/ThemeProvider.tsx +186 -44
- package/src/lib/theme/runtime/useTheme.ts +1 -1
- package/src/lib/theme/runtime/useThemeTokens.ts +7 -16
- package/src/lib/theme/test/testTheme.ts +2 -1
- package/src/lib/theme/types.ts +14 -14
- package/src/lib/theme/utils/componentTheming.ts +35 -27
- package/src/lib/theme/utils/domUtils.ts +57 -15
- package/src/lib/theme/utils/injectCSS.ts +0 -1
- package/src/lib/theme/utils/themeHelpers.ts +1 -39
- package/src/lib/theme/utils/themeUtils.ts +1 -170
- package/src/lib/types/components.ts +145 -0
- package/src/lib/utils/dataTableExport.ts +143 -0
- package/src/styles/06-components/_components.data-table.scss +95 -0
- 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
|
|
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
|
-
//
|
|
57
|
-
const cleanCdnPath = cdnPath
|
|
76
|
+
// Sanitize CDN path to prevent path injection
|
|
77
|
+
const cleanCdnPath = sanitizePath(cdnPath);
|
|
58
78
|
return `${cleanCdnPath}/${fileName}`;
|
|
59
79
|
}
|
|
60
80
|
|
|
61
|
-
//
|
|
62
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
206
|
-
|
|
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
|
-
|
|
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
|
};
|
|
@@ -1,34 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Theme Helper Functions
|
|
3
3
|
*
|
|
4
|
-
* Utility functions for working with
|
|
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 {
|
|
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
|
+
|