@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.
- package/LICENSE +7 -0
- package/README.md +38 -0
- package/package.json +49 -0
- package/src/components/Accordion.tsx +52 -0
- package/src/components/Avatar.tsx +18 -0
- package/src/components/Badge.tsx +27 -0
- package/src/components/Banner.tsx +75 -0
- package/src/components/Breadcrumb.tsx +58 -0
- package/src/components/Button.tsx +47 -0
- package/src/components/Card.tsx +34 -0
- package/src/components/ChatInput.tsx +53 -0
- package/src/components/Checkbox.tsx +85 -0
- package/src/components/CreditsIndicator.tsx +41 -0
- package/src/components/DataTable.tsx +75 -0
- package/src/components/DateInput.tsx +57 -0
- package/src/components/Divider.tsx +12 -0
- package/src/components/Dropzone.tsx +94 -0
- package/src/components/EmptyState.tsx +65 -0
- package/src/components/FileCard.tsx +78 -0
- package/src/components/FilterTabs.tsx +49 -0
- package/src/components/FlyoutMenu.tsx +36 -0
- package/src/components/GlassPopover.tsx +38 -0
- package/src/components/Header.tsx +22 -0
- package/src/components/Input.tsx +18 -0
- package/src/components/InputGroup.tsx +37 -0
- package/src/components/LanguageSelector.tsx +81 -0
- package/src/components/Modal.tsx +104 -0
- package/src/components/OnboardingPopover.tsx +61 -0
- package/src/components/OperationStatus.tsx +73 -0
- package/src/components/Overlay.tsx +66 -0
- package/src/components/Pagination.tsx +89 -0
- package/src/components/Pill.tsx +19 -0
- package/src/components/PricingCard.tsx +74 -0
- package/src/components/ProgressBar.tsx +47 -0
- package/src/components/Radio.tsx +56 -0
- package/src/components/SectionHeader.tsx +32 -0
- package/src/components/SegmentedControl.tsx +42 -0
- package/src/components/Select.tsx +62 -0
- package/src/components/SelectGroup.tsx +32 -0
- package/src/components/SelectionCard.tsx +47 -0
- package/src/components/SidebarItem.tsx +27 -0
- package/src/components/SidebarPanel.tsx +84 -0
- package/src/components/SplitPane.tsx +85 -0
- package/src/components/StatCard.tsx +64 -0
- package/src/components/StatusDot.tsx +26 -0
- package/src/components/Stepper.tsx +40 -0
- package/src/components/TabBar.tsx +45 -0
- package/src/components/Textarea.tsx +43 -0
- package/src/components/Toggle.tsx +50 -0
- package/src/components/Tooltip.tsx +33 -0
- package/src/components/UserProfilePopover.tsx +100 -0
- package/src/components/ValidationAlert.tsx +72 -0
- package/src/index.ts +177 -0
- package/src/styles.css +3237 -0
- package/src/tokens.css +165 -0
- package/src/tokens.registry.json +75 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
export type SelectOption = {
|
|
4
|
+
value: string;
|
|
5
|
+
label: string;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export type SelectProps = Omit<React.SelectHTMLAttributes<HTMLSelectElement>, 'onChange'> & {
|
|
9
|
+
placeholder?: string;
|
|
10
|
+
options?: SelectOption[];
|
|
11
|
+
onChange?: (value: string) => void;
|
|
12
|
+
filled?: boolean;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function Select({
|
|
16
|
+
value,
|
|
17
|
+
placeholder,
|
|
18
|
+
options = [],
|
|
19
|
+
onChange,
|
|
20
|
+
disabled,
|
|
21
|
+
filled,
|
|
22
|
+
className,
|
|
23
|
+
...props
|
|
24
|
+
}: SelectProps) {
|
|
25
|
+
return (
|
|
26
|
+
<div
|
|
27
|
+
className={`malix-select${className ? ` ${className}` : ''}`}
|
|
28
|
+
data-filled={filled || undefined}
|
|
29
|
+
data-disabled={disabled || undefined}
|
|
30
|
+
>
|
|
31
|
+
<select
|
|
32
|
+
className="malix-select__native"
|
|
33
|
+
value={value}
|
|
34
|
+
disabled={disabled}
|
|
35
|
+
onChange={(e) => onChange?.(e.target.value)}
|
|
36
|
+
{...props}
|
|
37
|
+
>
|
|
38
|
+
{placeholder ? (
|
|
39
|
+
<option value="" disabled>
|
|
40
|
+
{placeholder}
|
|
41
|
+
</option>
|
|
42
|
+
) : null}
|
|
43
|
+
{options.map((opt) => (
|
|
44
|
+
<option key={opt.value} value={opt.value}>
|
|
45
|
+
{opt.label}
|
|
46
|
+
</option>
|
|
47
|
+
))}
|
|
48
|
+
</select>
|
|
49
|
+
<span className="malix-select__icon">
|
|
50
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
|
51
|
+
<path
|
|
52
|
+
d="M4 6L8 10L12 6"
|
|
53
|
+
stroke="currentColor"
|
|
54
|
+
strokeWidth="1.5"
|
|
55
|
+
strokeLinecap="round"
|
|
56
|
+
strokeLinejoin="round"
|
|
57
|
+
/>
|
|
58
|
+
</svg>
|
|
59
|
+
</span>
|
|
60
|
+
</div>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
export type SelectGroupProps = React.HTMLAttributes<HTMLDivElement> & {
|
|
4
|
+
label?: string;
|
|
5
|
+
helperText?: string;
|
|
6
|
+
error?: boolean;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export function SelectGroup({
|
|
10
|
+
label,
|
|
11
|
+
helperText,
|
|
12
|
+
error,
|
|
13
|
+
children,
|
|
14
|
+
className,
|
|
15
|
+
...props
|
|
16
|
+
}: SelectGroupProps) {
|
|
17
|
+
return (
|
|
18
|
+
<div
|
|
19
|
+
className={`malix-select-group${className ? ` ${className}` : ''}`}
|
|
20
|
+
data-error={error || undefined}
|
|
21
|
+
{...props}
|
|
22
|
+
>
|
|
23
|
+
{label ? (
|
|
24
|
+
<span className="malix-select-group__label">{label}</span>
|
|
25
|
+
) : null}
|
|
26
|
+
{children}
|
|
27
|
+
{helperText ? (
|
|
28
|
+
<span className="malix-select-group__helper">{helperText}</span>
|
|
29
|
+
) : null}
|
|
30
|
+
</div>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
export type SelectionCardProps = {
|
|
4
|
+
title: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
icon?: React.ReactNode;
|
|
7
|
+
active?: boolean;
|
|
8
|
+
onClick?: () => void;
|
|
9
|
+
className?: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function SelectionCard({
|
|
13
|
+
title,
|
|
14
|
+
description,
|
|
15
|
+
icon,
|
|
16
|
+
active = false,
|
|
17
|
+
onClick,
|
|
18
|
+
className,
|
|
19
|
+
}: SelectionCardProps) {
|
|
20
|
+
return (
|
|
21
|
+
<div
|
|
22
|
+
className={`malix-selection-card${className ? ` ${className}` : ''}`}
|
|
23
|
+
data-active={active || undefined}
|
|
24
|
+
onClick={onClick}
|
|
25
|
+
role={onClick ? 'button' : undefined}
|
|
26
|
+
tabIndex={onClick ? 0 : undefined}
|
|
27
|
+
onKeyDown={
|
|
28
|
+
onClick
|
|
29
|
+
? (e: React.KeyboardEvent) => {
|
|
30
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
31
|
+
e.preventDefault();
|
|
32
|
+
onClick();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
: undefined
|
|
36
|
+
}
|
|
37
|
+
>
|
|
38
|
+
{icon ? (
|
|
39
|
+
<span className="malix-selection-card__icon-wrap">{icon}</span>
|
|
40
|
+
) : null}
|
|
41
|
+
<span className="malix-selection-card__title">{title}</span>
|
|
42
|
+
{description ? (
|
|
43
|
+
<span className="malix-selection-card__description">{description}</span>
|
|
44
|
+
) : null}
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
export type SidebarItemProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
|
|
4
|
+
icon?: React.ReactNode;
|
|
5
|
+
active?: boolean;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export function SidebarItem({
|
|
9
|
+
icon,
|
|
10
|
+
active = false,
|
|
11
|
+
children,
|
|
12
|
+
className,
|
|
13
|
+
...props
|
|
14
|
+
}: SidebarItemProps) {
|
|
15
|
+
return (
|
|
16
|
+
<button
|
|
17
|
+
type="button"
|
|
18
|
+
className={`malix-sidebar-item${className ? ` ${className}` : ''}`}
|
|
19
|
+
data-active={active || undefined}
|
|
20
|
+
aria-current={active ? 'page' : undefined}
|
|
21
|
+
{...props}
|
|
22
|
+
>
|
|
23
|
+
{icon ? <span className="malix-sidebar-item__icon">{icon}</span> : null}
|
|
24
|
+
<span className="malix-sidebar-item__label">{children}</span>
|
|
25
|
+
</button>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
|
|
3
|
+
export type SidebarPanelProps = React.HTMLAttributes<HTMLElement> & {
|
|
4
|
+
collapsed?: boolean;
|
|
5
|
+
logo?: React.ReactNode;
|
|
6
|
+
collapsedLogo?: React.ReactNode;
|
|
7
|
+
navigation?: React.ReactNode;
|
|
8
|
+
footer?: React.ReactNode;
|
|
9
|
+
onToggleCollapse?: () => void;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function SidebarPanel({
|
|
13
|
+
collapsed = false,
|
|
14
|
+
logo,
|
|
15
|
+
collapsedLogo,
|
|
16
|
+
navigation,
|
|
17
|
+
footer,
|
|
18
|
+
onToggleCollapse,
|
|
19
|
+
className,
|
|
20
|
+
...props
|
|
21
|
+
}: SidebarPanelProps) {
|
|
22
|
+
const [hovered, setHovered] = useState(false);
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<aside
|
|
26
|
+
className={`malix-sidebar-panel${className ? ` ${className}` : ''}`}
|
|
27
|
+
data-collapsed={collapsed || undefined}
|
|
28
|
+
onMouseEnter={collapsed ? () => setHovered(true) : undefined}
|
|
29
|
+
onMouseLeave={collapsed ? () => setHovered(false) : undefined}
|
|
30
|
+
{...props}
|
|
31
|
+
>
|
|
32
|
+
<div className="malix-sidebar-panel__top">
|
|
33
|
+
<div className="malix-sidebar-panel__brand">
|
|
34
|
+
{collapsed ? (
|
|
35
|
+
/* Collapsed: show isotype by default, toggle button on hover */
|
|
36
|
+
hovered && onToggleCollapse ? (
|
|
37
|
+
<button
|
|
38
|
+
type="button"
|
|
39
|
+
className="malix-sidebar-panel__toggle"
|
|
40
|
+
onClick={onToggleCollapse}
|
|
41
|
+
aria-label="Expand sidebar"
|
|
42
|
+
>
|
|
43
|
+
{/* panel-left-open */}
|
|
44
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
45
|
+
<rect width="18" height="18" x="3" y="3" rx="2" />
|
|
46
|
+
<path d="M9 3v18" />
|
|
47
|
+
<path d="m14 9 3 3-3 3" />
|
|
48
|
+
</svg>
|
|
49
|
+
</button>
|
|
50
|
+
) : (
|
|
51
|
+
<span className="malix-sidebar-panel__collapsed-logo">
|
|
52
|
+
{collapsedLogo ?? logo}
|
|
53
|
+
</span>
|
|
54
|
+
)
|
|
55
|
+
) : (
|
|
56
|
+
/* Expanded: logo + toggle button */
|
|
57
|
+
<>
|
|
58
|
+
{logo}
|
|
59
|
+
{onToggleCollapse ? (
|
|
60
|
+
<button
|
|
61
|
+
type="button"
|
|
62
|
+
className="malix-sidebar-panel__toggle"
|
|
63
|
+
onClick={onToggleCollapse}
|
|
64
|
+
aria-label="Collapse sidebar"
|
|
65
|
+
>
|
|
66
|
+
{/* panel-left-close */}
|
|
67
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
68
|
+
<rect width="18" height="18" x="3" y="3" rx="2" />
|
|
69
|
+
<path d="M9 3v18" />
|
|
70
|
+
<path d="m16 15-3-3 3-3" />
|
|
71
|
+
</svg>
|
|
72
|
+
</button>
|
|
73
|
+
) : null}
|
|
74
|
+
</>
|
|
75
|
+
)}
|
|
76
|
+
</div>
|
|
77
|
+
<nav className="malix-sidebar-panel__nav">{navigation}</nav>
|
|
78
|
+
</div>
|
|
79
|
+
{footer ? (
|
|
80
|
+
<div className="malix-sidebar-panel__bottom">{footer}</div>
|
|
81
|
+
) : null}
|
|
82
|
+
</aside>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import React, { useState, useCallback, useRef, useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
export type SplitPaneProps = React.HTMLAttributes<HTMLDivElement> & {
|
|
4
|
+
leftPanel: React.ReactNode;
|
|
5
|
+
rightPanel: React.ReactNode;
|
|
6
|
+
leftTitle?: string;
|
|
7
|
+
rightTitle?: string;
|
|
8
|
+
defaultSplit?: number;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function SplitPane({
|
|
12
|
+
leftPanel,
|
|
13
|
+
rightPanel,
|
|
14
|
+
leftTitle,
|
|
15
|
+
rightTitle,
|
|
16
|
+
defaultSplit = 50,
|
|
17
|
+
className,
|
|
18
|
+
...props
|
|
19
|
+
}: SplitPaneProps) {
|
|
20
|
+
const [split, setSplit] = useState(defaultSplit);
|
|
21
|
+
const containerRef = useRef<HTMLDivElement | null>(null);
|
|
22
|
+
const dragging = useRef(false);
|
|
23
|
+
|
|
24
|
+
const onMouseDown = useCallback((e: React.MouseEvent) => {
|
|
25
|
+
e.preventDefault();
|
|
26
|
+
dragging.current = true;
|
|
27
|
+
}, []);
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
function onMouseMove(e: MouseEvent) {
|
|
31
|
+
if (!dragging.current || !containerRef.current) return;
|
|
32
|
+
const rect = containerRef.current.getBoundingClientRect();
|
|
33
|
+
const x = e.clientX - rect.left;
|
|
34
|
+
const pct = Math.min(Math.max((x / rect.width) * 100, 5), 95);
|
|
35
|
+
setSplit(pct);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function onMouseUp() {
|
|
39
|
+
dragging.current = false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
document.addEventListener('mousemove', onMouseMove);
|
|
43
|
+
document.addEventListener('mouseup', onMouseUp);
|
|
44
|
+
return () => {
|
|
45
|
+
document.removeEventListener('mousemove', onMouseMove);
|
|
46
|
+
document.removeEventListener('mouseup', onMouseUp);
|
|
47
|
+
};
|
|
48
|
+
}, []);
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<div
|
|
52
|
+
ref={containerRef}
|
|
53
|
+
className={`malix-split-pane${className ? ` ${className}` : ''}`}
|
|
54
|
+
{...props}
|
|
55
|
+
>
|
|
56
|
+
<div className="malix-split-pane__left" style={{ width: `${split}%` }}>
|
|
57
|
+
{leftTitle ? (
|
|
58
|
+
<span className="malix-split-pane__panel-title">{leftTitle}</span>
|
|
59
|
+
) : null}
|
|
60
|
+
{leftPanel}
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
<div
|
|
64
|
+
className="malix-split-pane__handle"
|
|
65
|
+
role="separator"
|
|
66
|
+
aria-valuenow={Math.round(split)}
|
|
67
|
+
aria-valuemin={5}
|
|
68
|
+
aria-valuemax={95}
|
|
69
|
+
aria-label="Resize panels"
|
|
70
|
+
onMouseDown={onMouseDown}
|
|
71
|
+
>
|
|
72
|
+
<span className="malix-split-pane__handle-dot" aria-hidden="true" />
|
|
73
|
+
<span className="malix-split-pane__handle-dot" aria-hidden="true" />
|
|
74
|
+
<span className="malix-split-pane__handle-dot" aria-hidden="true" />
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<div className="malix-split-pane__right" style={{ width: `${100 - split}%` }}>
|
|
78
|
+
{rightTitle ? (
|
|
79
|
+
<span className="malix-split-pane__panel-title">{rightTitle}</span>
|
|
80
|
+
) : null}
|
|
81
|
+
{rightPanel}
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
export type StatCardChangeType = 'positive' | 'negative' | 'neutral';
|
|
4
|
+
|
|
5
|
+
export type StatCardProps = React.HTMLAttributes<HTMLDivElement> & {
|
|
6
|
+
label: string;
|
|
7
|
+
value: string;
|
|
8
|
+
change?: string;
|
|
9
|
+
changeType?: StatCardChangeType;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
function ChangeIcon({ type }: { type: StatCardChangeType }) {
|
|
13
|
+
const shared = { width: 12, height: 12, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round' as const, strokeLinejoin: 'round' as const };
|
|
14
|
+
|
|
15
|
+
switch (type) {
|
|
16
|
+
case 'positive':
|
|
17
|
+
return (
|
|
18
|
+
<svg {...shared}>
|
|
19
|
+
<polyline points="22 7 13.5 15.5 8.5 10.5 2 17" />
|
|
20
|
+
<polyline points="16 7 22 7 22 13" />
|
|
21
|
+
</svg>
|
|
22
|
+
);
|
|
23
|
+
case 'negative':
|
|
24
|
+
return (
|
|
25
|
+
<svg {...shared}>
|
|
26
|
+
<polyline points="22 17 13.5 8.5 8.5 13.5 2 7" />
|
|
27
|
+
<polyline points="16 17 22 17 22 11" />
|
|
28
|
+
</svg>
|
|
29
|
+
);
|
|
30
|
+
case 'neutral':
|
|
31
|
+
return (
|
|
32
|
+
<svg {...shared}>
|
|
33
|
+
<line x1="5" y1="12" x2="19" y2="12" />
|
|
34
|
+
</svg>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function StatCard({
|
|
40
|
+
label,
|
|
41
|
+
value,
|
|
42
|
+
change,
|
|
43
|
+
changeType = 'neutral',
|
|
44
|
+
className,
|
|
45
|
+
...props
|
|
46
|
+
}: StatCardProps) {
|
|
47
|
+
return (
|
|
48
|
+
<div
|
|
49
|
+
className={`malix-stat-card${className ? ` ${className}` : ''}`}
|
|
50
|
+
{...props}
|
|
51
|
+
>
|
|
52
|
+
<span className="malix-stat-card__label">{label}</span>
|
|
53
|
+
<span className="malix-stat-card__value">{value}</span>
|
|
54
|
+
{change ? (
|
|
55
|
+
<span className="malix-stat-card__change" data-type={changeType}>
|
|
56
|
+
<span className="malix-stat-card__change-icon">
|
|
57
|
+
<ChangeIcon type={changeType} />
|
|
58
|
+
</span>
|
|
59
|
+
{change}
|
|
60
|
+
</span>
|
|
61
|
+
) : null}
|
|
62
|
+
</div>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
export type StatusDotVariant = 'success' | 'warning' | 'error' | 'default';
|
|
4
|
+
|
|
5
|
+
export type StatusDotProps = React.HTMLAttributes<HTMLSpanElement> & {
|
|
6
|
+
variant?: StatusDotVariant;
|
|
7
|
+
label: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function StatusDot({
|
|
11
|
+
variant = 'default',
|
|
12
|
+
label,
|
|
13
|
+
className,
|
|
14
|
+
...props
|
|
15
|
+
}: StatusDotProps) {
|
|
16
|
+
return (
|
|
17
|
+
<span
|
|
18
|
+
className={`malix-status-dot${className ? ` ${className}` : ''}`}
|
|
19
|
+
data-variant={variant}
|
|
20
|
+
{...props}
|
|
21
|
+
>
|
|
22
|
+
<span className="malix-status-dot__dot" aria-hidden="true" />
|
|
23
|
+
<span className="malix-status-dot__label">{label}</span>
|
|
24
|
+
</span>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
export type StepStatus = 'completed' | 'active' | 'pending';
|
|
4
|
+
|
|
5
|
+
export type StepItem = {
|
|
6
|
+
label: string;
|
|
7
|
+
icon?: React.ReactNode;
|
|
8
|
+
status: StepStatus;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type StepperProps = {
|
|
12
|
+
steps: StepItem[];
|
|
13
|
+
className?: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function Stepper({ steps, className }: StepperProps) {
|
|
17
|
+
return (
|
|
18
|
+
<div className={`malix-stepper${className ? ` ${className}` : ''}`}>
|
|
19
|
+
{steps.map((step, index) => (
|
|
20
|
+
<React.Fragment key={index}>
|
|
21
|
+
<div className="malix-stepper__step" data-status={step.status}>
|
|
22
|
+
<span className="malix-stepper__step-icon">
|
|
23
|
+
{step.status === 'completed' ? (
|
|
24
|
+
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
25
|
+
<path d="M11.5 3.5L5.5 10L2.5 7" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
|
26
|
+
</svg>
|
|
27
|
+
) : (
|
|
28
|
+
step.icon ?? null
|
|
29
|
+
)}
|
|
30
|
+
</span>
|
|
31
|
+
<span className="malix-stepper__step-label">{step.label}</span>
|
|
32
|
+
</div>
|
|
33
|
+
{index < steps.length - 1 ? (
|
|
34
|
+
<span className="malix-stepper__connector" />
|
|
35
|
+
) : null}
|
|
36
|
+
</React.Fragment>
|
|
37
|
+
))}
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
export type TabItem = {
|
|
4
|
+
label: string;
|
|
5
|
+
value: string;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export type TabBarProps = Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange'> & {
|
|
9
|
+
items: TabItem[];
|
|
10
|
+
value: string;
|
|
11
|
+
onChange: (value: string) => void;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function TabBar({
|
|
15
|
+
items,
|
|
16
|
+
value,
|
|
17
|
+
onChange,
|
|
18
|
+
className,
|
|
19
|
+
...props
|
|
20
|
+
}: TabBarProps) {
|
|
21
|
+
return (
|
|
22
|
+
<div
|
|
23
|
+
className={`malix-tab-bar${className ? ` ${className}` : ''}`}
|
|
24
|
+
role="tablist"
|
|
25
|
+
{...props}
|
|
26
|
+
>
|
|
27
|
+
{items.map((item) => {
|
|
28
|
+
const isActive = item.value === value;
|
|
29
|
+
return (
|
|
30
|
+
<button
|
|
31
|
+
key={item.value}
|
|
32
|
+
type="button"
|
|
33
|
+
role="tab"
|
|
34
|
+
className="malix-tab-bar__tab"
|
|
35
|
+
data-active={isActive || undefined}
|
|
36
|
+
aria-selected={isActive}
|
|
37
|
+
onClick={() => onChange(item.value)}
|
|
38
|
+
>
|
|
39
|
+
<span className="malix-tab-bar__tab-label">{item.label}</span>
|
|
40
|
+
</button>
|
|
41
|
+
);
|
|
42
|
+
})}
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
export type TextareaProps = React.TextareaHTMLAttributes<HTMLTextAreaElement> & {
|
|
4
|
+
label?: string;
|
|
5
|
+
helperText?: string;
|
|
6
|
+
error?: boolean;
|
|
7
|
+
errorMessage?: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function Textarea({
|
|
11
|
+
label,
|
|
12
|
+
helperText,
|
|
13
|
+
error,
|
|
14
|
+
errorMessage,
|
|
15
|
+
id,
|
|
16
|
+
className,
|
|
17
|
+
...props
|
|
18
|
+
}: TextareaProps) {
|
|
19
|
+
const textareaId = id || `textarea-${React.useId()}`;
|
|
20
|
+
const displayHelper = error && errorMessage ? errorMessage : helperText;
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<div
|
|
24
|
+
className={`malix-textarea-group${className ? ` ${className}` : ''}`}
|
|
25
|
+
data-error={error || undefined}
|
|
26
|
+
>
|
|
27
|
+
{label ? (
|
|
28
|
+
<label htmlFor={textareaId} className="malix-textarea-group__label">
|
|
29
|
+
{label}
|
|
30
|
+
</label>
|
|
31
|
+
) : null}
|
|
32
|
+
<textarea
|
|
33
|
+
id={textareaId}
|
|
34
|
+
className="malix-textarea-group__field"
|
|
35
|
+
aria-invalid={error || undefined}
|
|
36
|
+
{...props}
|
|
37
|
+
/>
|
|
38
|
+
{displayHelper ? (
|
|
39
|
+
<span className="malix-textarea-group__helper">{displayHelper}</span>
|
|
40
|
+
) : null}
|
|
41
|
+
</div>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
export type ToggleProps = Omit<React.HTMLAttributes<HTMLButtonElement>, 'onChange'> & {
|
|
4
|
+
checked?: boolean;
|
|
5
|
+
onChange?: (checked: boolean) => void;
|
|
6
|
+
disabled?: boolean;
|
|
7
|
+
label?: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function Toggle({
|
|
11
|
+
checked = false,
|
|
12
|
+
onChange,
|
|
13
|
+
disabled = false,
|
|
14
|
+
label,
|
|
15
|
+
className,
|
|
16
|
+
...props
|
|
17
|
+
}: ToggleProps) {
|
|
18
|
+
const handleClick = () => {
|
|
19
|
+
if (!disabled && onChange) {
|
|
20
|
+
onChange(!checked);
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const toggle = (
|
|
25
|
+
<button
|
|
26
|
+
type="button"
|
|
27
|
+
role="switch"
|
|
28
|
+
className={`malix-toggle${className ? ` ${className}` : ''}`}
|
|
29
|
+
data-checked={checked}
|
|
30
|
+
data-disabled={disabled}
|
|
31
|
+
aria-checked={checked}
|
|
32
|
+
disabled={disabled}
|
|
33
|
+
onClick={handleClick}
|
|
34
|
+
{...props}
|
|
35
|
+
>
|
|
36
|
+
<span className="malix-toggle__knob" />
|
|
37
|
+
</button>
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
if (label) {
|
|
41
|
+
return (
|
|
42
|
+
<div className="malix-toggle-row">
|
|
43
|
+
<span className="malix-toggle-row__label">{label}</span>
|
|
44
|
+
{toggle}
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return toggle;
|
|
50
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import React, { useId, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
export type TooltipPlacement = 'top' | 'bottom' | 'left' | 'right';
|
|
4
|
+
|
|
5
|
+
export type TooltipProps = {
|
|
6
|
+
content: React.ReactNode;
|
|
7
|
+
children: React.ReactElement;
|
|
8
|
+
placement?: TooltipPlacement;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function Tooltip({ content, children, placement = 'top' }: TooltipProps) {
|
|
12
|
+
const [open, setOpen] = useState(false);
|
|
13
|
+
const tooltipId = useId();
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<span
|
|
17
|
+
className="malix-tooltip-wrap"
|
|
18
|
+
onMouseEnter={() => setOpen(true)}
|
|
19
|
+
onMouseLeave={() => setOpen(false)}
|
|
20
|
+
onFocus={() => setOpen(true)}
|
|
21
|
+
onBlur={() => setOpen(false)}
|
|
22
|
+
>
|
|
23
|
+
{React.cloneElement(children as React.ReactElement<any>, {
|
|
24
|
+
'aria-describedby': open ? tooltipId : undefined
|
|
25
|
+
})}
|
|
26
|
+
{open ? (
|
|
27
|
+
<span role="tooltip" id={tooltipId} className="malix-tooltip" data-placement={placement}>
|
|
28
|
+
{content}
|
|
29
|
+
</span>
|
|
30
|
+
) : null}
|
|
31
|
+
</span>
|
|
32
|
+
);
|
|
33
|
+
}
|