@camtomlabs/malix-design-system 0.1.2

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 (56) hide show
  1. package/LICENSE +7 -0
  2. package/README.md +38 -0
  3. package/package.json +49 -0
  4. package/src/components/Accordion.tsx +52 -0
  5. package/src/components/Avatar.tsx +18 -0
  6. package/src/components/Badge.tsx +27 -0
  7. package/src/components/Banner.tsx +75 -0
  8. package/src/components/Breadcrumb.tsx +58 -0
  9. package/src/components/Button.tsx +47 -0
  10. package/src/components/Card.tsx +34 -0
  11. package/src/components/ChatInput.tsx +53 -0
  12. package/src/components/Checkbox.tsx +85 -0
  13. package/src/components/CreditsIndicator.tsx +41 -0
  14. package/src/components/DataTable.tsx +75 -0
  15. package/src/components/DateInput.tsx +57 -0
  16. package/src/components/Divider.tsx +12 -0
  17. package/src/components/Dropzone.tsx +94 -0
  18. package/src/components/EmptyState.tsx +65 -0
  19. package/src/components/FileCard.tsx +78 -0
  20. package/src/components/FilterTabs.tsx +49 -0
  21. package/src/components/FlyoutMenu.tsx +36 -0
  22. package/src/components/GlassPopover.tsx +38 -0
  23. package/src/components/Header.tsx +22 -0
  24. package/src/components/Input.tsx +18 -0
  25. package/src/components/InputGroup.tsx +37 -0
  26. package/src/components/LanguageSelector.tsx +81 -0
  27. package/src/components/Modal.tsx +104 -0
  28. package/src/components/OnboardingPopover.tsx +61 -0
  29. package/src/components/OperationStatus.tsx +73 -0
  30. package/src/components/Overlay.tsx +66 -0
  31. package/src/components/Pagination.tsx +89 -0
  32. package/src/components/Pill.tsx +19 -0
  33. package/src/components/PricingCard.tsx +74 -0
  34. package/src/components/ProgressBar.tsx +47 -0
  35. package/src/components/Radio.tsx +56 -0
  36. package/src/components/SectionHeader.tsx +32 -0
  37. package/src/components/SegmentedControl.tsx +42 -0
  38. package/src/components/Select.tsx +62 -0
  39. package/src/components/SelectGroup.tsx +32 -0
  40. package/src/components/SelectionCard.tsx +47 -0
  41. package/src/components/SidebarItem.tsx +27 -0
  42. package/src/components/SidebarPanel.tsx +84 -0
  43. package/src/components/SplitPane.tsx +85 -0
  44. package/src/components/StatCard.tsx +64 -0
  45. package/src/components/StatusDot.tsx +26 -0
  46. package/src/components/Stepper.tsx +40 -0
  47. package/src/components/TabBar.tsx +45 -0
  48. package/src/components/Textarea.tsx +43 -0
  49. package/src/components/Toggle.tsx +50 -0
  50. package/src/components/Tooltip.tsx +33 -0
  51. package/src/components/UserProfilePopover.tsx +100 -0
  52. package/src/components/ValidationAlert.tsx +72 -0
  53. package/src/index.ts +177 -0
  54. package/src/styles.css +3237 -0
  55. package/src/tokens.css +165 -0
  56. package/src/tokens.registry.json +75 -0
package/LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ Copyright (c) 2026 Carlos Garcia
2
+
3
+ All rights reserved.
4
+
5
+ This package is provided without a public-use license.
6
+ You may not copy, modify, distribute, sublicense, or use this work except with explicit written
7
+ permission from the copyright holder.
package/README.md ADDED
@@ -0,0 +1,38 @@
1
+ # @camtomlabs/malix-design-system
2
+
3
+ Malix Design System combined package.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @camtomlabs/malix-design-system
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ Import the bundled styles once:
14
+
15
+ ```ts
16
+ import '@camtomlabs/malix-design-system/styles.css';
17
+ ```
18
+
19
+ Import components from the combined entrypoint:
20
+
21
+ ```tsx
22
+ import { Button, Input, Card } from '@camtomlabs/malix-design-system';
23
+ ```
24
+
25
+ You can also read the token registry:
26
+
27
+ ```ts
28
+ import { tokenRegistry } from '@camtomlabs/malix-design-system';
29
+ ```
30
+
31
+ ## Note
32
+
33
+ This package publishes raw TypeScript source. Configure your bundler to transpile the package in
34
+ frameworks like Next.js.
35
+
36
+ ## License
37
+
38
+ `UNLICENSED`
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@camtomlabs/malix-design-system",
3
+ "version": "0.1.2",
4
+ "description": "Malix Design System combined package with components, tokens, and bundled styles.",
5
+ "type": "module",
6
+ "main": "src/index.ts",
7
+ "types": "src/index.ts",
8
+ "sideEffects": [
9
+ "./src/styles.css",
10
+ "./src/tokens.css"
11
+ ],
12
+ "files": [
13
+ "src"
14
+ ],
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/carloscamtom/malix-design-system.git",
18
+ "directory": "packages/malix"
19
+ },
20
+ "homepage": "https://github.com/carloscamtom/malix-design-system#readme",
21
+ "bugs": {
22
+ "url": "https://github.com/carloscamtom/malix-design-system/issues"
23
+ },
24
+ "keywords": [
25
+ "malix",
26
+ "design-system",
27
+ "react",
28
+ "components",
29
+ "tokens"
30
+ ],
31
+ "license": "UNLICENSED",
32
+ "exports": {
33
+ ".": "./src/index.ts",
34
+ "./styles.css": "./src/styles.css",
35
+ "./tokens.css": "./src/tokens.css",
36
+ "./tokens.registry.json": "./src/tokens.registry.json"
37
+ },
38
+ "peerDependencies": {
39
+ "react": "^18.0.0 || ^19.0.0",
40
+ "react-dom": "^18.0.0 || ^19.0.0"
41
+ },
42
+ "publishConfig": {
43
+ "access": "public"
44
+ },
45
+ "scripts": {
46
+ "lint": "eslint src",
47
+ "build": "node -e \"console.log('malix build: source package ready')\""
48
+ }
49
+ }
@@ -0,0 +1,52 @@
1
+ import React, { useState } from 'react';
2
+
3
+ export type AccordionProps = React.HTMLAttributes<HTMLDivElement> & {
4
+ title: string;
5
+ children: React.ReactNode;
6
+ defaultOpen?: boolean;
7
+ icon?: React.ReactNode;
8
+ };
9
+
10
+ export function Accordion({
11
+ title,
12
+ children,
13
+ defaultOpen = false,
14
+ icon,
15
+ className,
16
+ ...props
17
+ }: AccordionProps) {
18
+ const [open, setOpen] = useState(defaultOpen);
19
+
20
+ return (
21
+ <div
22
+ className={`malix-accordion${className ? ` ${className}` : ''}`}
23
+ data-open={open}
24
+ {...props}
25
+ >
26
+ <button
27
+ type="button"
28
+ className="malix-accordion__header"
29
+ onClick={() => setOpen((prev) => !prev)}
30
+ aria-expanded={open}
31
+ >
32
+ {icon ? <span className="malix-accordion__icon">{icon}</span> : null}
33
+ <span className="malix-accordion__title">{title}</span>
34
+ <svg
35
+ className="malix-accordion__chevron"
36
+ width="16"
37
+ height="16"
38
+ viewBox="0 0 24 24"
39
+ fill="none"
40
+ stroke="currentColor"
41
+ strokeWidth="2"
42
+ strokeLinecap="round"
43
+ strokeLinejoin="round"
44
+ aria-hidden="true"
45
+ >
46
+ <polyline points="6 9 12 15 18 9" />
47
+ </svg>
48
+ </button>
49
+ <div className="malix-accordion__body">{children}</div>
50
+ </div>
51
+ );
52
+ }
@@ -0,0 +1,18 @@
1
+ import React from 'react';
2
+
3
+ export type AvatarProps = React.HTMLAttributes<HTMLSpanElement> & {
4
+ initials: string;
5
+ size?: number;
6
+ };
7
+
8
+ export function Avatar({ initials, size = 40, className, style, ...props }: AvatarProps) {
9
+ return (
10
+ <span
11
+ className={`malix-avatar${className ? ` ${className}` : ''}`}
12
+ style={{ width: size, height: size, borderRadius: size * 0.35, ...style }}
13
+ {...props}
14
+ >
15
+ <span className="malix-avatar__text">{initials}</span>
16
+ </span>
17
+ );
18
+ }
@@ -0,0 +1,27 @@
1
+ import React from 'react';
2
+
3
+ export type BadgeVariant = 'default' | 'primary' | 'success' | 'warning' | 'error';
4
+
5
+ export type BadgeProps = React.HTMLAttributes<HTMLSpanElement> & {
6
+ variant?: BadgeVariant;
7
+ dot?: boolean;
8
+ };
9
+
10
+ export function Badge({
11
+ variant = 'default',
12
+ dot = false,
13
+ children,
14
+ className,
15
+ ...props
16
+ }: BadgeProps) {
17
+ return (
18
+ <span
19
+ className={`malix-badge${className ? ` ${className}` : ''}`}
20
+ data-variant={variant}
21
+ {...props}
22
+ >
23
+ {dot ? <span className="malix-badge__dot" /> : null}
24
+ <span className="malix-badge__label">{children}</span>
25
+ </span>
26
+ );
27
+ }
@@ -0,0 +1,75 @@
1
+ import React from 'react';
2
+
3
+ export type BannerVariant = 'info' | 'success' | 'warning' | 'error';
4
+
5
+ export type BannerProps = React.HTMLAttributes<HTMLDivElement> & {
6
+ variant?: BannerVariant;
7
+ onClose?: () => void;
8
+ icon?: React.ReactNode;
9
+ };
10
+
11
+ const defaultIcons: Record<BannerVariant, React.ReactNode> = {
12
+ info: (
13
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
14
+ <circle cx="12" cy="12" r="10" />
15
+ <line x1="12" y1="16" x2="12" y2="12" />
16
+ <line x1="12" y1="8" x2="12.01" y2="8" />
17
+ </svg>
18
+ ),
19
+ success: (
20
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
21
+ <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" />
22
+ <polyline points="22 4 12 14.01 9 11.01" />
23
+ </svg>
24
+ ),
25
+ warning: (
26
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
27
+ <path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" />
28
+ <line x1="12" y1="9" x2="12" y2="13" />
29
+ <line x1="12" y1="17" x2="12.01" y2="17" />
30
+ </svg>
31
+ ),
32
+ error: (
33
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
34
+ <circle cx="12" cy="12" r="10" />
35
+ <line x1="15" y1="9" x2="9" y2="15" />
36
+ <line x1="9" y1="9" x2="15" y2="15" />
37
+ </svg>
38
+ ),
39
+ };
40
+
41
+ export function Banner({
42
+ variant = 'info',
43
+ onClose,
44
+ icon,
45
+ children,
46
+ className,
47
+ ...props
48
+ }: BannerProps) {
49
+ return (
50
+ <div
51
+ className={`malix-banner${className ? ` ${className}` : ''}`}
52
+ data-variant={variant}
53
+ role="alert"
54
+ {...props}
55
+ >
56
+ <span className="malix-banner__icon">
57
+ {icon ?? defaultIcons[variant]}
58
+ </span>
59
+ <span className="malix-banner__content">{children}</span>
60
+ {onClose ? (
61
+ <button
62
+ type="button"
63
+ className="malix-banner__close"
64
+ onClick={onClose}
65
+ aria-label="Dismiss"
66
+ >
67
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
68
+ <line x1="18" y1="6" x2="6" y2="18" />
69
+ <line x1="6" y1="6" x2="18" y2="18" />
70
+ </svg>
71
+ </button>
72
+ ) : null}
73
+ </div>
74
+ );
75
+ }
@@ -0,0 +1,58 @@
1
+ import React from 'react';
2
+
3
+ export type BreadcrumbItem = {
4
+ label: string;
5
+ href?: string;
6
+ icon?: React.ReactNode;
7
+ active?: boolean;
8
+ };
9
+
10
+ export type BreadcrumbProps = React.HTMLAttributes<HTMLElement> & {
11
+ items: BreadcrumbItem[];
12
+ separator?: React.ReactNode;
13
+ };
14
+
15
+ const DefaultSeparator = () => (
16
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
17
+ <path d="M6 3L11 8L6 13" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
18
+ </svg>
19
+ );
20
+
21
+ export function Breadcrumb({ items, separator, className, ...props }: BreadcrumbProps) {
22
+ return (
23
+ <nav
24
+ className={`malix-breadcrumb${className ? ` ${className}` : ''}`}
25
+ aria-label="Breadcrumb"
26
+ {...props}
27
+ >
28
+ {items.map((item, index) => {
29
+ const isLast = index === items.length - 1;
30
+ const isActive = item.active ?? isLast;
31
+
32
+ return (
33
+ <React.Fragment key={index}>
34
+ <span className="malix-breadcrumb__item" data-active={isActive || undefined}>
35
+ {item.icon ? (
36
+ <span className="malix-breadcrumb__icon">{item.icon}</span>
37
+ ) : null}
38
+ {isActive || !item.href ? (
39
+ <span className="malix-breadcrumb__label" aria-current={isActive ? 'page' : undefined}>
40
+ {item.label}
41
+ </span>
42
+ ) : (
43
+ <a className="malix-breadcrumb__label" href={item.href}>
44
+ {item.label}
45
+ </a>
46
+ )}
47
+ </span>
48
+ {!isLast ? (
49
+ <span className="malix-breadcrumb__separator" aria-hidden="true">
50
+ {separator ?? <DefaultSeparator />}
51
+ </span>
52
+ ) : null}
53
+ </React.Fragment>
54
+ );
55
+ })}
56
+ </nav>
57
+ );
58
+ }
@@ -0,0 +1,47 @@
1
+ import React from 'react';
2
+
3
+ export type ButtonHierarchy = 'primary' | 'secondary' | 'tertiary' | 'ghost';
4
+ export type ButtonVariant = 'text' | 'leading-icon-text' | 'icon-only' | 'icon-badge';
5
+
6
+ export type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
7
+ hierarchy?: ButtonHierarchy;
8
+ variant?: ButtonVariant;
9
+ icon?: React.ReactNode;
10
+ badge?: React.ReactNode;
11
+ loading?: boolean;
12
+ label?: string;
13
+ };
14
+
15
+ export function Button({
16
+ hierarchy = 'primary',
17
+ variant = 'text',
18
+ icon,
19
+ badge,
20
+ loading = false,
21
+ disabled,
22
+ children,
23
+ label,
24
+ ...props
25
+ }: ButtonProps) {
26
+ const isDisabled = disabled || loading;
27
+ const isIconOnly = variant === 'icon-only';
28
+
29
+ return (
30
+ <button
31
+ type="button"
32
+ className="malix-button"
33
+ data-hierarchy={hierarchy}
34
+ data-variant={variant}
35
+ data-loading={loading}
36
+ disabled={isDisabled}
37
+ aria-busy={loading || undefined}
38
+ aria-label={isIconOnly ? label : props['aria-label']}
39
+ {...props}
40
+ >
41
+ {icon ? <span className="malix-button__icon">{icon}</span> : null}
42
+ {!isIconOnly ? <span>{loading ? 'Loading...' : children}</span> : null}
43
+ {variant === 'icon-badge' && badge ? <span className="malix-button__badge">{badge}</span> : null}
44
+ {isIconOnly && !icon ? <span>{loading ? '...' : label}</span> : null}
45
+ </button>
46
+ );
47
+ }
@@ -0,0 +1,34 @@
1
+ import React from 'react';
2
+
3
+ export type CardLevel = 1 | 2 | 3;
4
+
5
+ export type CardProps = React.HTMLAttributes<HTMLDivElement> & {
6
+ level?: CardLevel;
7
+ title?: string;
8
+ description?: string;
9
+ };
10
+
11
+ export function Card({
12
+ level = 1,
13
+ title,
14
+ description,
15
+ children,
16
+ className,
17
+ ...props
18
+ }: CardProps) {
19
+ return (
20
+ <div
21
+ className={`malix-card${className ? ` ${className}` : ''}`}
22
+ data-level={level}
23
+ {...props}
24
+ >
25
+ {(title || description) ? (
26
+ <div className="malix-card__header">
27
+ {title ? <h3 className="malix-card__title">{title}</h3> : null}
28
+ {description ? <p className="malix-card__desc">{description}</p> : null}
29
+ </div>
30
+ ) : null}
31
+ {children ? <div className="malix-card__body">{children}</div> : null}
32
+ </div>
33
+ );
34
+ }
@@ -0,0 +1,53 @@
1
+ import React from 'react';
2
+
3
+ export type ChatInputProps = {
4
+ value: string;
5
+ onChange: (value: string) => void;
6
+ onSend: () => void;
7
+ placeholder?: string;
8
+ disabled?: boolean;
9
+ className?: string;
10
+ };
11
+
12
+ export function ChatInput({
13
+ value,
14
+ onChange,
15
+ onSend,
16
+ placeholder = 'Type a message...',
17
+ disabled,
18
+ className,
19
+ }: ChatInputProps) {
20
+ const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
21
+ if (e.key === 'Enter' && !e.shiftKey && value.trim()) {
22
+ e.preventDefault();
23
+ onSend();
24
+ }
25
+ };
26
+
27
+ return (
28
+ <div className={`malix-chat-input${className ? ` ${className}` : ''}`}>
29
+ <input
30
+ type="text"
31
+ className="malix-chat-input__field"
32
+ value={value}
33
+ onChange={(e) => onChange(e.target.value)}
34
+ onKeyDown={handleKeyDown}
35
+ placeholder={placeholder}
36
+ disabled={disabled}
37
+ aria-label={placeholder}
38
+ />
39
+ <button
40
+ type="button"
41
+ className="malix-chat-input__send-btn"
42
+ onClick={onSend}
43
+ disabled={disabled || !value.trim()}
44
+ aria-label="Send message"
45
+ >
46
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
47
+ <path d="M12 19V5" />
48
+ <path d="m5 12 7-7 7 7" />
49
+ </svg>
50
+ </button>
51
+ </div>
52
+ );
53
+ }
@@ -0,0 +1,85 @@
1
+ import React from 'react';
2
+
3
+ export type CheckboxProps = Omit<React.HTMLAttributes<HTMLButtonElement>, 'onChange'> & {
4
+ checked?: boolean;
5
+ onChange?: (checked: boolean) => void;
6
+ disabled?: boolean;
7
+ label?: string;
8
+ indeterminate?: boolean;
9
+ };
10
+
11
+ export function Checkbox({
12
+ checked = false,
13
+ onChange,
14
+ disabled = false,
15
+ label,
16
+ indeterminate = false,
17
+ className,
18
+ ...props
19
+ }: CheckboxProps) {
20
+ const handleClick = () => {
21
+ if (!disabled && onChange) {
22
+ onChange(!checked);
23
+ }
24
+ };
25
+
26
+ const checkbox = (
27
+ <button
28
+ type="button"
29
+ role="checkbox"
30
+ className={`malix-checkbox${className ? ` ${className}` : ''}`}
31
+ data-checked={checked}
32
+ data-disabled={disabled}
33
+ data-indeterminate={indeterminate}
34
+ aria-checked={indeterminate ? 'mixed' : checked}
35
+ disabled={disabled}
36
+ onClick={handleClick}
37
+ {...props}
38
+ >
39
+ {checked && !indeterminate ? (
40
+ <svg
41
+ className="malix-checkbox__icon"
42
+ viewBox="0 0 12 12"
43
+ fill="none"
44
+ xmlns="http://www.w3.org/2000/svg"
45
+ aria-hidden="true"
46
+ >
47
+ <path
48
+ d="M2 6l3 3 5-5"
49
+ stroke="currentColor"
50
+ strokeWidth="2"
51
+ strokeLinecap="round"
52
+ strokeLinejoin="round"
53
+ />
54
+ </svg>
55
+ ) : null}
56
+ {indeterminate ? (
57
+ <svg
58
+ className="malix-checkbox__icon"
59
+ viewBox="0 0 12 12"
60
+ fill="none"
61
+ xmlns="http://www.w3.org/2000/svg"
62
+ aria-hidden="true"
63
+ >
64
+ <path
65
+ d="M2 6h8"
66
+ stroke="currentColor"
67
+ strokeWidth="2"
68
+ strokeLinecap="round"
69
+ />
70
+ </svg>
71
+ ) : null}
72
+ </button>
73
+ );
74
+
75
+ if (label) {
76
+ return (
77
+ <label className="malix-checkbox-row">
78
+ {checkbox}
79
+ <span className="malix-checkbox-row__label">{label}</span>
80
+ </label>
81
+ );
82
+ }
83
+
84
+ return checkbox;
85
+ }
@@ -0,0 +1,41 @@
1
+ import React from 'react';
2
+
3
+ export type CreditsIndicatorProps = React.HTMLAttributes<HTMLDivElement> & {
4
+ remaining: number;
5
+ label?: string;
6
+ };
7
+
8
+ export function CreditsIndicator({
9
+ remaining,
10
+ label = 'Credits Remaining',
11
+ className,
12
+ ...props
13
+ }: CreditsIndicatorProps) {
14
+ return (
15
+ <div
16
+ className={`malix-credits-indicator${className ? ` ${className}` : ''}`}
17
+ {...props}
18
+ >
19
+ <span className="malix-credits-indicator__icon" aria-hidden="true">
20
+ <svg
21
+ width="20"
22
+ height="20"
23
+ viewBox="0 0 24 24"
24
+ fill="none"
25
+ stroke="currentColor"
26
+ strokeWidth="2"
27
+ strokeLinecap="round"
28
+ strokeLinejoin="round"
29
+ >
30
+ <ellipse cx="12" cy="6" rx="8" ry="3" />
31
+ <path d="M4 6v6c0 1.66 3.58 3 8 3s8-1.34 8-3V6" />
32
+ <path d="M4 12v6c0 1.66 3.58 3 8 3s8-1.34 8-3v-6" />
33
+ </svg>
34
+ </span>
35
+ <div className="malix-credits-indicator__info">
36
+ <span className="malix-credits-indicator__label">{label}</span>
37
+ <span className="malix-credits-indicator__value">{remaining}</span>
38
+ </div>
39
+ </div>
40
+ );
41
+ }
@@ -0,0 +1,75 @@
1
+ import React from 'react';
2
+
3
+ export type TableColumn = {
4
+ key: string;
5
+ header: string;
6
+ width?: string;
7
+ render?: (value: any, row: TableRow) => React.ReactNode;
8
+ };
9
+
10
+ export type TableRow = Record<string, any>;
11
+
12
+ export type DataTableProps = React.HTMLAttributes<HTMLTableElement> & {
13
+ columns: TableColumn[];
14
+ data: TableRow[];
15
+ onRowClick?: (row: TableRow) => void;
16
+ emptyMessage?: string;
17
+ className?: string;
18
+ };
19
+
20
+ export function DataTable({
21
+ columns,
22
+ data,
23
+ onRowClick,
24
+ emptyMessage,
25
+ className,
26
+ ...props
27
+ }: DataTableProps) {
28
+ return (
29
+ <table
30
+ className={`malix-data-table${className ? ` ${className}` : ''}`}
31
+ {...props}
32
+ >
33
+ <thead>
34
+ <tr className="malix-data-table__header-row">
35
+ {columns.map((col) => (
36
+ <th
37
+ key={col.key}
38
+ className="malix-data-table__header-cell"
39
+ style={col.width ? { width: col.width } : undefined}
40
+ >
41
+ {col.header}
42
+ </th>
43
+ ))}
44
+ </tr>
45
+ </thead>
46
+ <tbody className="malix-data-table__body">
47
+ {data.length > 0 ? (
48
+ data.map((row, rowIndex) => (
49
+ <tr
50
+ key={rowIndex}
51
+ className="malix-data-table__data-row"
52
+ data-clickable={onRowClick ? true : undefined}
53
+ onClick={onRowClick ? () => onRowClick(row) : undefined}
54
+ >
55
+ {columns.map((col) => (
56
+ <td key={col.key} className="malix-data-table__cell">
57
+ {col.render ? col.render(row[col.key], row) : row[col.key]}
58
+ </td>
59
+ ))}
60
+ </tr>
61
+ ))
62
+ ) : (
63
+ <tr className="malix-data-table__data-row">
64
+ <td
65
+ className="malix-data-table__cell"
66
+ colSpan={columns.length}
67
+ >
68
+ {emptyMessage || 'No data available'}
69
+ </td>
70
+ </tr>
71
+ )}
72
+ </tbody>
73
+ </table>
74
+ );
75
+ }