@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
@@ -0,0 +1,57 @@
1
+ import React from 'react';
2
+
3
+ export type DateInputProps = Omit<React.InputHTMLAttributes<HTMLInputElement>, 'type' | 'onChange'> & {
4
+ value?: string;
5
+ placeholder?: string;
6
+ onChange?: (value: string) => void;
7
+ disabled?: boolean;
8
+ };
9
+
10
+ export function DateInput({
11
+ value,
12
+ placeholder = 'dd/mm/yyyy',
13
+ onChange,
14
+ disabled,
15
+ className,
16
+ id,
17
+ ...props
18
+ }: DateInputProps) {
19
+ const inputId = id || `date-input-${React.useId()}`;
20
+
21
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
22
+ onChange?.(e.target.value);
23
+ };
24
+
25
+ return (
26
+ <div
27
+ className={`malix-date-input${className ? ` ${className}` : ''}`}
28
+ data-filled={value ? true : undefined}
29
+ data-disabled={disabled || undefined}
30
+ >
31
+ <input
32
+ id={inputId}
33
+ type="date"
34
+ className="malix-date-input__native"
35
+ value={value}
36
+ onChange={handleChange}
37
+ disabled={disabled}
38
+ aria-label={placeholder}
39
+ {...props}
40
+ />
41
+ <span className="malix-date-input__value">
42
+ {value || placeholder}
43
+ </span>
44
+ <span className="malix-date-input__icon" aria-hidden="true">
45
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
46
+ <path
47
+ d="M12.667 2.667H3.333C2.597 2.667 2 3.264 2 4v9.333c0 .737.597 1.334 1.333 1.334h9.334c.736 0 1.333-.597 1.333-1.334V4c0-.736-.597-1.333-1.333-1.333ZM10.667 1.333V4M5.333 1.333V4M2 6.667h12"
48
+ stroke="currentColor"
49
+ strokeWidth="1.33"
50
+ strokeLinecap="round"
51
+ strokeLinejoin="round"
52
+ />
53
+ </svg>
54
+ </span>
55
+ </div>
56
+ );
57
+ }
@@ -0,0 +1,12 @@
1
+ import React from 'react';
2
+
3
+ export type DividerProps = React.HTMLAttributes<HTMLHRElement>;
4
+
5
+ export function Divider({ className, ...props }: DividerProps) {
6
+ return (
7
+ <hr
8
+ className={`malix-divider${className ? ` ${className}` : ''}`}
9
+ {...props}
10
+ />
11
+ );
12
+ }
@@ -0,0 +1,94 @@
1
+ import React, { useRef, useState } from 'react';
2
+
3
+ export type DropzoneProps = React.HTMLAttributes<HTMLDivElement> & {
4
+ onDrop?: (files: File[]) => void;
5
+ accept?: string;
6
+ title?: string;
7
+ hint?: string;
8
+ disabled?: boolean;
9
+ };
10
+
11
+ export function Dropzone({
12
+ onDrop,
13
+ accept,
14
+ title = 'Drop files here',
15
+ hint = 'or click to browse',
16
+ disabled = false,
17
+ className,
18
+ ...props
19
+ }: DropzoneProps) {
20
+ const [dragging, setDragging] = useState(false);
21
+ const inputRef = useRef<HTMLInputElement | null>(null);
22
+
23
+ function handleDragOver(event: React.DragEvent<HTMLDivElement>) {
24
+ event.preventDefault();
25
+ if (!disabled) setDragging(true);
26
+ }
27
+
28
+ function handleDragLeave(event: React.DragEvent<HTMLDivElement>) {
29
+ event.preventDefault();
30
+ setDragging(false);
31
+ }
32
+
33
+ function handleDrop(event: React.DragEvent<HTMLDivElement>) {
34
+ event.preventDefault();
35
+ setDragging(false);
36
+ if (disabled) return;
37
+ const files = Array.from(event.dataTransfer.files);
38
+ onDrop?.(files);
39
+ }
40
+
41
+ function handleInputChange(event: React.ChangeEvent<HTMLInputElement>) {
42
+ const files = event.target.files ? Array.from(event.target.files) : [];
43
+ if (files.length > 0) onDrop?.(files);
44
+ event.target.value = '';
45
+ }
46
+
47
+ function handleClick() {
48
+ if (!disabled) inputRef.current?.click();
49
+ }
50
+
51
+ return (
52
+ <div
53
+ className={`malix-dropzone${className ? ` ${className}` : ''}`}
54
+ data-dragging={dragging}
55
+ data-disabled={disabled}
56
+ onDragOver={handleDragOver}
57
+ onDragLeave={handleDragLeave}
58
+ onDrop={handleDrop}
59
+ onClick={handleClick}
60
+ role="button"
61
+ tabIndex={disabled ? -1 : 0}
62
+ aria-disabled={disabled || undefined}
63
+ {...props}
64
+ >
65
+ <input
66
+ ref={inputRef}
67
+ type="file"
68
+ accept={accept}
69
+ onChange={handleInputChange}
70
+ hidden
71
+ aria-hidden="true"
72
+ />
73
+ <svg
74
+ className="malix-dropzone__icon"
75
+ width="32"
76
+ height="32"
77
+ viewBox="0 0 24 24"
78
+ fill="none"
79
+ stroke="currentColor"
80
+ strokeWidth="1.5"
81
+ strokeLinecap="round"
82
+ strokeLinejoin="round"
83
+ aria-hidden="true"
84
+ >
85
+ <path d="M4 14.899A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.5 8.242" />
86
+ <path d="M12 12v9" />
87
+ <path d="m16 16-4-4-4 4" />
88
+ </svg>
89
+ <span className="malix-dropzone__title">{title}</span>
90
+ <span className="malix-dropzone__hint">{hint}</span>
91
+ <span className="malix-dropzone__browse-btn">Browse files</span>
92
+ </div>
93
+ );
94
+ }
@@ -0,0 +1,65 @@
1
+ import React from 'react';
2
+
3
+ export type EmptyStateProps = React.HTMLAttributes<HTMLDivElement> & {
4
+ icon?: React.ReactNode;
5
+ title: string;
6
+ description?: string;
7
+ action?: React.ReactNode;
8
+ };
9
+
10
+ const DefaultIcon = () => (
11
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
12
+ <path
13
+ d="M3 7V5C3 3.895 3.895 3 5 3H9L11 5H19C20.105 5 21 5.895 21 7V9"
14
+ stroke="currentColor"
15
+ strokeWidth="1.5"
16
+ strokeLinecap="round"
17
+ strokeLinejoin="round"
18
+ />
19
+ <rect
20
+ x="2"
21
+ y="9"
22
+ width="20"
23
+ height="12"
24
+ rx="2"
25
+ stroke="currentColor"
26
+ strokeWidth="1.5"
27
+ strokeLinecap="round"
28
+ strokeLinejoin="round"
29
+ />
30
+ <path
31
+ d="M9 15H15"
32
+ stroke="currentColor"
33
+ strokeWidth="1.5"
34
+ strokeLinecap="round"
35
+ strokeLinejoin="round"
36
+ />
37
+ </svg>
38
+ );
39
+
40
+ export function EmptyState({
41
+ icon,
42
+ title,
43
+ description,
44
+ action,
45
+ className,
46
+ ...props
47
+ }: EmptyStateProps) {
48
+ return (
49
+ <div
50
+ className={`malix-empty-state${className ? ` ${className}` : ''}`}
51
+ {...props}
52
+ >
53
+ <div className="malix-empty-state__icon-wrap">
54
+ {icon ?? <DefaultIcon />}
55
+ </div>
56
+ <h3 className="malix-empty-state__title">{title}</h3>
57
+ {description ? (
58
+ <p className="malix-empty-state__description">{description}</p>
59
+ ) : null}
60
+ {action ? (
61
+ <div className="malix-empty-state__action">{action}</div>
62
+ ) : null}
63
+ </div>
64
+ );
65
+ }
@@ -0,0 +1,78 @@
1
+ import React from 'react';
2
+
3
+ export type FileCardProps = React.HTMLAttributes<HTMLDivElement> & {
4
+ fileName: string;
5
+ meta?: string;
6
+ icon?: React.ReactNode;
7
+ onAction?: React.MouseEventHandler<HTMLButtonElement>;
8
+ };
9
+
10
+ const DefaultFileIcon = () => (
11
+ <svg
12
+ width="20"
13
+ height="20"
14
+ viewBox="0 0 24 24"
15
+ fill="none"
16
+ stroke="currentColor"
17
+ strokeWidth="2"
18
+ strokeLinecap="round"
19
+ strokeLinejoin="round"
20
+ >
21
+ <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
22
+ <polyline points="14 2 14 8 20 8" />
23
+ <line x1="16" y1="13" x2="8" y2="13" />
24
+ <line x1="16" y1="17" x2="8" y2="17" />
25
+ <polyline points="10 9 9 9 8 9" />
26
+ </svg>
27
+ );
28
+
29
+ const MoreVerticalIcon = () => (
30
+ <svg
31
+ width="16"
32
+ height="16"
33
+ viewBox="0 0 24 24"
34
+ fill="currentColor"
35
+ >
36
+ <circle cx="12" cy="5" r="1.5" />
37
+ <circle cx="12" cy="12" r="1.5" />
38
+ <circle cx="12" cy="19" r="1.5" />
39
+ </svg>
40
+ );
41
+
42
+ export function FileCard({
43
+ fileName,
44
+ meta,
45
+ icon,
46
+ onAction,
47
+ className,
48
+ ...props
49
+ }: FileCardProps) {
50
+ return (
51
+ <div
52
+ className={`malix-file-card${className ? ` ${className}` : ''}`}
53
+ {...props}
54
+ >
55
+ <div className="malix-file-card__icon-wrap">
56
+ <span className="malix-file-card__icon">
57
+ {icon ?? <DefaultFileIcon />}
58
+ </span>
59
+ </div>
60
+ <div className="malix-file-card__info">
61
+ <span className="malix-file-card__name">{fileName}</span>
62
+ {meta ? (
63
+ <span className="malix-file-card__meta">{meta}</span>
64
+ ) : null}
65
+ </div>
66
+ {onAction ? (
67
+ <button
68
+ type="button"
69
+ className="malix-file-card__action"
70
+ onClick={onAction}
71
+ aria-label="More actions"
72
+ >
73
+ <MoreVerticalIcon />
74
+ </button>
75
+ ) : null}
76
+ </div>
77
+ );
78
+ }
@@ -0,0 +1,49 @@
1
+ import React from 'react';
2
+
3
+ export type FilterTabItem = {
4
+ label: string;
5
+ count?: number;
6
+ value: string;
7
+ };
8
+
9
+ export type FilterTabsProps = Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange'> & {
10
+ items: FilterTabItem[];
11
+ value: string;
12
+ onChange: (value: string) => void;
13
+ };
14
+
15
+ export function FilterTabs({
16
+ items,
17
+ value,
18
+ onChange,
19
+ className,
20
+ ...props
21
+ }: FilterTabsProps) {
22
+ return (
23
+ <div
24
+ className={`malix-filter-tabs${className ? ` ${className}` : ''}`}
25
+ role="tablist"
26
+ {...props}
27
+ >
28
+ {items.map((item) => {
29
+ const isActive = item.value === value;
30
+ return (
31
+ <button
32
+ key={item.value}
33
+ type="button"
34
+ role="tab"
35
+ className="malix-filter-tabs__tab"
36
+ data-active={isActive || undefined}
37
+ aria-selected={isActive}
38
+ onClick={() => onChange(item.value)}
39
+ >
40
+ <span className="malix-filter-tabs__tab-label">{item.label}</span>
41
+ {item.count != null ? (
42
+ <span className="malix-filter-tabs__tab-count">{item.count}</span>
43
+ ) : null}
44
+ </button>
45
+ );
46
+ })}
47
+ </div>
48
+ );
49
+ }
@@ -0,0 +1,36 @@
1
+ import React from 'react';
2
+
3
+ export type FlyoutMenuItem = {
4
+ icon?: React.ReactNode;
5
+ label: string;
6
+ active?: boolean;
7
+ onClick?: () => void;
8
+ };
9
+
10
+ export type FlyoutMenuProps = React.HTMLAttributes<HTMLDivElement> & {
11
+ items: FlyoutMenuItem[];
12
+ };
13
+
14
+ export function FlyoutMenu({ items, className, ...props }: FlyoutMenuProps) {
15
+ return (
16
+ <nav
17
+ className={`malix-flyout-menu${className ? ` ${className}` : ''}`}
18
+ role="menu"
19
+ {...props}
20
+ >
21
+ {items.map((item, i) => (
22
+ <button
23
+ key={i}
24
+ type="button"
25
+ className="malix-flyout-menu__item"
26
+ data-active={item.active || undefined}
27
+ role="menuitem"
28
+ onClick={item.onClick}
29
+ >
30
+ {item.icon ? <span className="malix-flyout-menu__icon">{item.icon}</span> : null}
31
+ <span className="malix-flyout-menu__label">{item.label}</span>
32
+ </button>
33
+ ))}
34
+ </nav>
35
+ );
36
+ }
@@ -0,0 +1,38 @@
1
+ import React from 'react';
2
+
3
+ export type GlassPopoverProps = React.HTMLAttributes<HTMLDivElement> & {
4
+ title?: string;
5
+ onClose?: () => void;
6
+ };
7
+
8
+ export function GlassPopover({
9
+ title,
10
+ onClose,
11
+ children,
12
+ className,
13
+ ...props
14
+ }: GlassPopoverProps) {
15
+ return (
16
+ <div
17
+ className={`malix-glass-popover${className ? ` ${className}` : ''}`}
18
+ {...props}
19
+ >
20
+ {(title || onClose) ? (
21
+ <div className="malix-glass-popover__header">
22
+ {title ? <span className="malix-glass-popover__title">{title}</span> : null}
23
+ {onClose ? (
24
+ <button
25
+ type="button"
26
+ className="malix-glass-popover__close"
27
+ onClick={onClose}
28
+ aria-label="Close"
29
+ >
30
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" /></svg>
31
+ </button>
32
+ ) : null}
33
+ </div>
34
+ ) : null}
35
+ <div className="malix-glass-popover__body">{children}</div>
36
+ </div>
37
+ );
38
+ }
@@ -0,0 +1,22 @@
1
+ import React from 'react';
2
+
3
+ export type HeaderProps = React.HTMLAttributes<HTMLElement> & {
4
+ pageTitle: string;
5
+ actions?: React.ReactNode;
6
+ };
7
+
8
+ export function Header({ pageTitle, actions, className, ...props }: HeaderProps) {
9
+ return (
10
+ <header
11
+ className={`malix-header${className ? ` ${className}` : ''}`}
12
+ {...props}
13
+ >
14
+ <div className="malix-header__left">
15
+ <h1 className="malix-header__title">{pageTitle}</h1>
16
+ </div>
17
+ {actions ? (
18
+ <div className="malix-header__right">{actions}</div>
19
+ ) : null}
20
+ </header>
21
+ );
22
+ }
@@ -0,0 +1,18 @@
1
+ import React from 'react';
2
+
3
+ export type InputProps = React.InputHTMLAttributes<HTMLInputElement> & {
4
+ leadingIcon?: React.ReactNode;
5
+ };
6
+
7
+ export function Input({ leadingIcon, ...props }: InputProps) {
8
+ return (
9
+ <label className="malix-input-wrap">
10
+ {leadingIcon ? <span className="malix-button__icon">{leadingIcon}</span> : null}
11
+ <input className="malix-input" {...props} />
12
+ </label>
13
+ );
14
+ }
15
+
16
+ export function SearchInput(props: Omit<InputProps, 'type'>) {
17
+ return <Input type="search" placeholder="Search" {...props} />;
18
+ }
@@ -0,0 +1,37 @@
1
+ import React from 'react';
2
+
3
+ export type InputGroupProps = React.InputHTMLAttributes<HTMLInputElement> & {
4
+ label?: string;
5
+ helperText?: string;
6
+ leadingIcon?: React.ReactNode;
7
+ error?: boolean;
8
+ };
9
+
10
+ export function InputGroup({
11
+ label,
12
+ helperText,
13
+ leadingIcon,
14
+ error,
15
+ id,
16
+ className,
17
+ ...props
18
+ }: InputGroupProps) {
19
+ const inputId = id || `input-${React.useId()}`;
20
+
21
+ return (
22
+ <div className={`malix-input-group${className ? ` ${className}` : ''}`} data-error={error || undefined}>
23
+ {label ? (
24
+ <label htmlFor={inputId} className="malix-input-group__label">
25
+ {label}
26
+ </label>
27
+ ) : null}
28
+ <div className="malix-input-group__field">
29
+ {leadingIcon ? <span className="malix-input-group__icon">{leadingIcon}</span> : null}
30
+ <input id={inputId} className="malix-input" {...props} />
31
+ </div>
32
+ {helperText ? (
33
+ <span className="malix-input-group__helper">{helperText}</span>
34
+ ) : null}
35
+ </div>
36
+ );
37
+ }
@@ -0,0 +1,81 @@
1
+ import React from 'react';
2
+
3
+ export type LanguageSelectorOption = {
4
+ value: string;
5
+ label: string;
6
+ };
7
+
8
+ export type LanguageSelectorProps = Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange'> & {
9
+ value: string;
10
+ options?: LanguageSelectorOption[];
11
+ onChange?: (value: string) => void;
12
+ };
13
+
14
+ export function LanguageSelector({
15
+ value,
16
+ options,
17
+ onChange,
18
+ className,
19
+ ...props
20
+ }: LanguageSelectorProps) {
21
+ const selectedOption = options?.find((opt) => opt.value === value);
22
+ const displayLabel = selectedOption?.label ?? value;
23
+
24
+ return (
25
+ <div
26
+ className={`malix-language-selector${className ? ` ${className}` : ''}`}
27
+ {...props}
28
+ >
29
+ <span className="malix-language-selector__icon" aria-hidden="true">
30
+ <svg
31
+ width="16"
32
+ height="16"
33
+ viewBox="0 0 24 24"
34
+ fill="none"
35
+ stroke="currentColor"
36
+ strokeWidth="2"
37
+ strokeLinecap="round"
38
+ strokeLinejoin="round"
39
+ >
40
+ <circle cx="12" cy="12" r="10" />
41
+ <line x1="2" y1="12" x2="22" y2="12" />
42
+ <path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10A15.3 15.3 0 0 1 12 2z" />
43
+ </svg>
44
+ </span>
45
+
46
+ <select
47
+ className="malix-language-selector__select"
48
+ value={value}
49
+ onChange={(e) => onChange?.(e.target.value)}
50
+ aria-label="Select language"
51
+ >
52
+ {options ? (
53
+ options.map((opt) => (
54
+ <option key={opt.value} value={opt.value}>
55
+ {opt.label}
56
+ </option>
57
+ ))
58
+ ) : (
59
+ <option value={value}>{displayLabel}</option>
60
+ )}
61
+ </select>
62
+
63
+ <span className="malix-language-selector__label">{displayLabel}</span>
64
+
65
+ <span className="malix-language-selector__chevron" aria-hidden="true">
66
+ <svg
67
+ width="14"
68
+ height="14"
69
+ viewBox="0 0 24 24"
70
+ fill="none"
71
+ stroke="currentColor"
72
+ strokeWidth="2"
73
+ strokeLinecap="round"
74
+ strokeLinejoin="round"
75
+ >
76
+ <polyline points="6 9 12 15 18 9" />
77
+ </svg>
78
+ </span>
79
+ </div>
80
+ );
81
+ }