@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,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
|
+
}
|