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