@affinda/react 0.0.25 → 0.0.27
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/README.md +412 -416
- package/dist/components/AfAccordionTrigger.d.ts +22 -0
- package/dist/components/AfAccordionTrigger.js +18 -0
- package/dist/components/AfAlert.d.ts +22 -0
- package/dist/components/AfAlert.js +67 -0
- package/dist/components/AfAvatar.d.ts +21 -0
- package/dist/components/AfAvatar.js +54 -0
- package/dist/components/AfBreadcrumb.d.ts +21 -0
- package/dist/components/AfBreadcrumb.js +32 -0
- package/dist/components/AfLink.d.ts +21 -0
- package/dist/components/AfLink.js +30 -0
- package/dist/components/AfMarketingNavbar.d.ts +53 -0
- package/dist/components/AfMarketingNavbar.js +23 -0
- package/dist/components/AfModal.d.ts +25 -0
- package/dist/components/AfModal.js +71 -0
- package/dist/components/AfNumberBadge.d.ts +20 -0
- package/dist/components/{NumberBadge.js → AfNumberBadge.js} +3 -3
- package/dist/components/AfPaperclipDecoration.d.ts +23 -0
- package/dist/components/{PaperclipDecoration.js → AfPaperclipDecoration.js} +9 -8
- package/dist/components/AfSelect.d.ts +34 -0
- package/dist/components/AfSelect.js +71 -0
- package/dist/components/AfSkeleton.d.ts +19 -0
- package/dist/components/AfSkeleton.js +50 -0
- package/dist/components/AfSpinner.d.ts +47 -0
- package/dist/components/AfSpinner.js +54 -0
- package/dist/components/{Tab.d.ts → AfTab.d.ts} +5 -5
- package/dist/components/{Tab.js → AfTab.js} +3 -3
- package/dist/components/AfTabBar.d.ts +20 -0
- package/dist/components/{TabBar.js → AfTabBar.js} +3 -3
- package/dist/components/AfToast.d.ts +22 -0
- package/dist/components/AfToast.js +77 -0
- package/dist/components/AfTooltip.d.ts +19 -0
- package/dist/components/AfTooltip.js +80 -0
- package/dist/generated/components.d.ts +58 -3
- package/dist/generated/components.js +117 -9
- package/dist/index.d.ts +31 -8
- package/dist/index.js +20 -9
- package/package.json +4 -4
- package/dist/components/NumberBadge.d.ts +0 -20
- package/dist/components/PaperclipDecoration.d.ts +0 -22
- package/dist/components/TabBar.d.ts +0 -20
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
export interface AfSelectOption {
|
|
3
|
+
value: string;
|
|
4
|
+
label: string;
|
|
5
|
+
disabled?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export interface AfSelectProps {
|
|
8
|
+
/** Array of options to display */
|
|
9
|
+
options: AfSelectOption[];
|
|
10
|
+
/** Currently selected value */
|
|
11
|
+
value?: string;
|
|
12
|
+
/** Placeholder text when no value is selected */
|
|
13
|
+
placeholder?: string;
|
|
14
|
+
/** Label text for the select */
|
|
15
|
+
label?: string;
|
|
16
|
+
/** Description text below the label */
|
|
17
|
+
description?: string;
|
|
18
|
+
/** Error message to display */
|
|
19
|
+
error?: string;
|
|
20
|
+
/** Whether the select is disabled */
|
|
21
|
+
disabled?: boolean;
|
|
22
|
+
/** Whether the select is required */
|
|
23
|
+
required?: boolean;
|
|
24
|
+
/** Callback when selection changes */
|
|
25
|
+
onChange?: (value: string) => void;
|
|
26
|
+
/** Additional CSS styles */
|
|
27
|
+
style?: React.CSSProperties;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* AfSelect - Draft dropdown select component.
|
|
31
|
+
*
|
|
32
|
+
* ⚠️ DRAFT: This component is not yet designed in Figma. Styles may change.
|
|
33
|
+
*/
|
|
34
|
+
export declare const AfSelect: React.FC<AfSelectProps>;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useRef, useEffect } from 'react';
|
|
3
|
+
/**
|
|
4
|
+
* AfSelect - Draft dropdown select component.
|
|
5
|
+
*
|
|
6
|
+
* ⚠️ DRAFT: This component is not yet designed in Figma. Styles may change.
|
|
7
|
+
*/
|
|
8
|
+
export const AfSelect = ({ options, value, placeholder = 'Select an option', label, description, error, disabled = false, required = false, onChange, style, }) => {
|
|
9
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
10
|
+
const [selectedValue, setSelectedValue] = useState(value || '');
|
|
11
|
+
const containerRef = useRef(null);
|
|
12
|
+
const selectedOption = options.find(opt => opt.value === selectedValue);
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
const handleClickOutside = (event) => {
|
|
15
|
+
if (containerRef.current && !containerRef.current.contains(event.target)) {
|
|
16
|
+
setIsOpen(false);
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
20
|
+
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
21
|
+
}, []);
|
|
22
|
+
const handleSelect = (optionValue) => {
|
|
23
|
+
setSelectedValue(optionValue);
|
|
24
|
+
setIsOpen(false);
|
|
25
|
+
onChange?.(optionValue);
|
|
26
|
+
};
|
|
27
|
+
const baseStyles = {
|
|
28
|
+
fontFamily: 'var(--typography-bodyfont, system-ui, sans-serif)',
|
|
29
|
+
fontSize: '16px',
|
|
30
|
+
};
|
|
31
|
+
const triggerStyles = {
|
|
32
|
+
display: 'flex',
|
|
33
|
+
alignItems: 'center',
|
|
34
|
+
justifyContent: 'space-between',
|
|
35
|
+
padding: '12px 16px',
|
|
36
|
+
borderRadius: '8px',
|
|
37
|
+
border: `1px solid ${error ? 'var(--colour-feedback-error, #dc3545)' : 'var(--colour-tints-inkwell-100, #e0e5e6)'}`,
|
|
38
|
+
background: disabled ? 'var(--colour-background-level1, #f5f5f5)' : 'var(--colour-background-white, #fff)',
|
|
39
|
+
cursor: disabled ? 'not-allowed' : 'pointer',
|
|
40
|
+
opacity: disabled ? 0.6 : 1,
|
|
41
|
+
color: selectedValue ? 'var(--colour-brand-inkwell, #14343b)' : 'var(--colour-tints-inkwell-400, #7a8a8d)',
|
|
42
|
+
width: '100%',
|
|
43
|
+
boxSizing: 'border-box',
|
|
44
|
+
};
|
|
45
|
+
const dropdownStyles = {
|
|
46
|
+
position: 'absolute',
|
|
47
|
+
top: '100%',
|
|
48
|
+
left: 0,
|
|
49
|
+
right: 0,
|
|
50
|
+
marginTop: '4px',
|
|
51
|
+
background: 'var(--colour-background-white, #fff)',
|
|
52
|
+
border: '1px solid var(--colour-tints-inkwell-100, #e0e5e6)',
|
|
53
|
+
borderRadius: '8px',
|
|
54
|
+
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.1)',
|
|
55
|
+
zIndex: 100,
|
|
56
|
+
maxHeight: '240px',
|
|
57
|
+
overflow: 'auto',
|
|
58
|
+
};
|
|
59
|
+
return (_jsxs("div", { ref: containerRef, style: { position: 'relative', ...baseStyles, ...style }, children: [label && (_jsxs("label", { style: { display: 'block', marginBottom: '4px', fontWeight: 500, color: 'var(--colour-brand-inkwell, #14343b)' }, children: [label, required && _jsx("span", { style: { color: 'var(--colour-feedback-error, #dc3545)' }, children: " *" })] })), description && (_jsx("p", { style: { margin: '0 0 8px', fontSize: '14px', color: 'var(--colour-tints-inkwell-400, #7a8a8d)' }, children: description })), _jsxs("button", { type: "button", onClick: () => !disabled && setIsOpen(!isOpen), style: triggerStyles, disabled: disabled, "aria-haspopup": "listbox", "aria-expanded": isOpen, children: [_jsx("span", { children: selectedOption?.label || placeholder }), _jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", style: { transform: isOpen ? 'rotate(180deg)' : 'rotate(0)', transition: 'transform 0.2s' }, children: _jsx("path", { d: "M6 9L12 15L18 9", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }) })] }), isOpen && (_jsx("ul", { style: dropdownStyles, role: "listbox", children: options.map((option) => (_jsx("li", { role: "option", "aria-selected": option.value === selectedValue, onClick: () => !option.disabled && handleSelect(option.value), style: {
|
|
60
|
+
padding: '12px 16px',
|
|
61
|
+
cursor: option.disabled ? 'not-allowed' : 'pointer',
|
|
62
|
+
opacity: option.disabled ? 0.5 : 1,
|
|
63
|
+
background: option.value === selectedValue ? 'var(--colour-tints-mist-green-100, #e8f0ee)' : 'transparent',
|
|
64
|
+
listStyle: 'none',
|
|
65
|
+
}, onMouseEnter: (e) => {
|
|
66
|
+
if (!option.disabled)
|
|
67
|
+
e.currentTarget.style.background = 'var(--colour-tints-mist-green-50, #f4f8f7)';
|
|
68
|
+
}, onMouseLeave: (e) => {
|
|
69
|
+
e.currentTarget.style.background = option.value === selectedValue ? 'var(--colour-tints-mist-green-100, #e8f0ee)' : 'transparent';
|
|
70
|
+
}, children: option.label }, option.value))) })), error && (_jsx("p", { style: { margin: '4px 0 0', fontSize: '14px', color: 'var(--colour-feedback-error, #dc3545)' }, children: error }))] }));
|
|
71
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
export interface AfSkeletonProps {
|
|
3
|
+
/** Width of the skeleton */
|
|
4
|
+
width?: string | number;
|
|
5
|
+
/** Height of the skeleton */
|
|
6
|
+
height?: string | number;
|
|
7
|
+
/** Shape of the skeleton */
|
|
8
|
+
variant?: 'text' | 'circular' | 'rectangular';
|
|
9
|
+
/** Whether to animate the skeleton */
|
|
10
|
+
animate?: boolean;
|
|
11
|
+
/** Additional CSS styles */
|
|
12
|
+
style?: React.CSSProperties;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* AfSkeleton - Draft skeleton loading placeholder component.
|
|
16
|
+
*
|
|
17
|
+
* ⚠️ DRAFT: This component is not yet designed in Figma. Styles may change.
|
|
18
|
+
*/
|
|
19
|
+
export declare const AfSkeleton: React.FC<AfSkeletonProps>;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* AfSkeleton - Draft skeleton loading placeholder component.
|
|
4
|
+
*
|
|
5
|
+
* ⚠️ DRAFT: This component is not yet designed in Figma. Styles may change.
|
|
6
|
+
*/
|
|
7
|
+
export const AfSkeleton = ({ width = '100%', height, variant = 'text', animate = true, style, }) => {
|
|
8
|
+
const keyframes = `
|
|
9
|
+
@keyframes af-skeleton-pulse {
|
|
10
|
+
0%, 100% { opacity: 1; }
|
|
11
|
+
50% { opacity: 0.5; }
|
|
12
|
+
}
|
|
13
|
+
`;
|
|
14
|
+
const getHeight = () => {
|
|
15
|
+
if (height)
|
|
16
|
+
return height;
|
|
17
|
+
switch (variant) {
|
|
18
|
+
case 'text':
|
|
19
|
+
return '1em';
|
|
20
|
+
case 'circular':
|
|
21
|
+
return width;
|
|
22
|
+
case 'rectangular':
|
|
23
|
+
return '100px';
|
|
24
|
+
default:
|
|
25
|
+
return '1em';
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
const getBorderRadius = () => {
|
|
29
|
+
switch (variant) {
|
|
30
|
+
case 'text':
|
|
31
|
+
return '4px';
|
|
32
|
+
case 'circular':
|
|
33
|
+
return '50%';
|
|
34
|
+
case 'rectangular':
|
|
35
|
+
return '8px';
|
|
36
|
+
default:
|
|
37
|
+
return '4px';
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
const skeletonStyles = {
|
|
41
|
+
display: 'block',
|
|
42
|
+
width: typeof width === 'number' ? `${width}px` : width,
|
|
43
|
+
height: typeof getHeight() === 'number' ? `${getHeight()}px` : getHeight(),
|
|
44
|
+
borderRadius: getBorderRadius(),
|
|
45
|
+
background: 'var(--colour-tints-inkwell-100, #e0e5e6)',
|
|
46
|
+
animation: animate ? 'af-skeleton-pulse 1.5s ease-in-out infinite' : 'none',
|
|
47
|
+
...style,
|
|
48
|
+
};
|
|
49
|
+
return (_jsxs(_Fragment, { children: [_jsx("style", { children: keyframes }), _jsx("span", { style: skeletonStyles, "aria-hidden": "true" })] }));
|
|
50
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
export interface AfSpinnerProps {
|
|
3
|
+
/**
|
|
4
|
+
* Size of the spinner.
|
|
5
|
+
* - 'small' (16px), 'medium' (24px), 'large' (40px) for standalone usage
|
|
6
|
+
* - number (e.g. 20) for pixel size - use this when inside a button slot to match icon sizing
|
|
7
|
+
*/
|
|
8
|
+
size?: 'small' | 'medium' | 'large' | number;
|
|
9
|
+
/**
|
|
10
|
+
* Color variant of the spinner - automatically picks theme-appropriate colors.
|
|
11
|
+
* Use 'currentColor' when inside a button to inherit the button's icon color.
|
|
12
|
+
*/
|
|
13
|
+
variant?: 'inkwell' | 'white' | 'mist-green' | 'soft-clay' | 'currentColor';
|
|
14
|
+
/**
|
|
15
|
+
* Custom color override - takes precedence over variant.
|
|
16
|
+
* @deprecated Use variant prop instead for theme-aware colors
|
|
17
|
+
*/
|
|
18
|
+
color?: string;
|
|
19
|
+
/** Accessible label */
|
|
20
|
+
label?: string;
|
|
21
|
+
/** Additional CSS class names */
|
|
22
|
+
className?: string;
|
|
23
|
+
/** Additional CSS styles */
|
|
24
|
+
style?: React.CSSProperties;
|
|
25
|
+
/** Slot name for use inside web components (e.g. 'icon-left' for AfButton) */
|
|
26
|
+
slot?: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* AfSpinner - Loading spinner component.
|
|
30
|
+
*
|
|
31
|
+
* Supports theme-aware color variants and can be used standalone or inside buttons.
|
|
32
|
+
* When used in a button, place in the icon-left or icon-right slot and use
|
|
33
|
+
* a numeric size (e.g. size={20}) with variant="currentColor" to match icon sizing.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```tsx
|
|
37
|
+
* // Standalone usage
|
|
38
|
+
* <AfSpinner size="medium" variant="inkwell" />
|
|
39
|
+
*
|
|
40
|
+
* // In a button with proper slot placement (matches icon sizing)
|
|
41
|
+
* <AfButton variant="primary" disabled>
|
|
42
|
+
* <AfSpinner slot="icon-left" size={20} variant="currentColor" />
|
|
43
|
+
* Loading...
|
|
44
|
+
* </AfButton>
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export declare const AfSpinner: React.FC<AfSpinnerProps>;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
const variantColors = {
|
|
3
|
+
'inkwell': 'var(--colour-brand-inkwell, #14343b)',
|
|
4
|
+
'white': 'var(--colour-brand-white, #ffffff)',
|
|
5
|
+
'mist-green': 'var(--colour-brand-mist-green, #c6d5d1)',
|
|
6
|
+
'soft-clay': 'var(--colour-brand-soft-clay, #b09670)',
|
|
7
|
+
'currentColor': 'currentColor',
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* AfSpinner - Loading spinner component.
|
|
11
|
+
*
|
|
12
|
+
* Supports theme-aware color variants and can be used standalone or inside buttons.
|
|
13
|
+
* When used in a button, place in the icon-left or icon-right slot and use
|
|
14
|
+
* a numeric size (e.g. size={20}) with variant="currentColor" to match icon sizing.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```tsx
|
|
18
|
+
* // Standalone usage
|
|
19
|
+
* <AfSpinner size="medium" variant="inkwell" />
|
|
20
|
+
*
|
|
21
|
+
* // In a button with proper slot placement (matches icon sizing)
|
|
22
|
+
* <AfButton variant="primary" disabled>
|
|
23
|
+
* <AfSpinner slot="icon-left" size={20} variant="currentColor" />
|
|
24
|
+
* Loading...
|
|
25
|
+
* </AfButton>
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export const AfSpinner = ({ size = 'medium', variant = 'inkwell', color, label = 'Loading', className, style, slot, }) => {
|
|
29
|
+
const sizeMap = {
|
|
30
|
+
small: 16,
|
|
31
|
+
medium: 24,
|
|
32
|
+
large: 40,
|
|
33
|
+
};
|
|
34
|
+
// Support both named sizes and numeric pixel values
|
|
35
|
+
const dimension = typeof size === 'number' ? size : sizeMap[size];
|
|
36
|
+
const strokeWidth = dimension <= 16 ? 2 : dimension >= 40 ? 4 : 3;
|
|
37
|
+
// Use explicit color if provided, otherwise use variant color
|
|
38
|
+
const strokeColor = color ?? variantColors[variant] ?? variantColors['inkwell'];
|
|
39
|
+
const spinnerStyles = {
|
|
40
|
+
display: 'inline-flex',
|
|
41
|
+
alignItems: 'center',
|
|
42
|
+
justifyContent: 'center',
|
|
43
|
+
width: dimension,
|
|
44
|
+
height: dimension,
|
|
45
|
+
...style,
|
|
46
|
+
};
|
|
47
|
+
const keyframes = `
|
|
48
|
+
@keyframes af-spinner-rotate {
|
|
49
|
+
from { transform: rotate(0deg); }
|
|
50
|
+
to { transform: rotate(360deg); }
|
|
51
|
+
}
|
|
52
|
+
`;
|
|
53
|
+
return (_jsxs(_Fragment, { children: [_jsx("style", { children: keyframes }), _jsxs("div", { slot: slot, className: className, style: spinnerStyles, role: "status", "aria-label": label, children: [_jsxs("svg", { width: "100%", height: "100%", viewBox: "0 0 24 24", fill: "none", style: { animation: 'af-spinner-rotate 1s linear infinite' }, children: [_jsx("circle", { cx: "12", cy: "12", r: "10", stroke: strokeColor, strokeWidth: strokeWidth, strokeLinecap: "round", strokeDasharray: "31.4 31.4", opacity: "0.25" }), _jsx("circle", { cx: "12", cy: "12", r: "10", stroke: strokeColor, strokeWidth: strokeWidth, strokeLinecap: "round", strokeDasharray: "31.4 31.4", strokeDashoffset: "23.55" })] }), _jsx("span", { style: { position: 'absolute', width: 1, height: 1, padding: 0, margin: -1, overflow: 'hidden', clip: 'rect(0, 0, 0, 0)', whiteSpace: 'nowrap', border: 0 }, children: label })] })] }));
|
|
54
|
+
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
export type
|
|
3
|
-
export interface
|
|
2
|
+
export type AfTabShape = 'square' | 'pill';
|
|
3
|
+
export interface AfTabProps {
|
|
4
4
|
/** The text label for the tab */
|
|
5
5
|
label: string;
|
|
6
6
|
/** Whether the tab is currently active/selected */
|
|
@@ -8,7 +8,7 @@ export interface TabProps {
|
|
|
8
8
|
/** Whether the tab is disabled */
|
|
9
9
|
disabled?: boolean;
|
|
10
10
|
/** Visual shape variant */
|
|
11
|
-
shape?:
|
|
11
|
+
shape?: AfTabShape;
|
|
12
12
|
/** Whether to show the icon slot */
|
|
13
13
|
displayIcon?: boolean;
|
|
14
14
|
/** Whether to show the number badge slot */
|
|
@@ -27,7 +27,7 @@ export interface TabProps {
|
|
|
27
27
|
children?: React.ReactNode;
|
|
28
28
|
}
|
|
29
29
|
/**
|
|
30
|
-
*
|
|
30
|
+
* AfTab component for use within an AfTabBar.
|
|
31
31
|
* Represents an individual selectable tab with support for icons and number badges.
|
|
32
32
|
*/
|
|
33
|
-
export declare const
|
|
33
|
+
export declare const AfTab: React.ForwardRefExoticComponent<AfTabProps & React.RefAttributes<HTMLElement>>;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* AfTab component for use within an AfTabBar.
|
|
4
4
|
* Represents an individual selectable tab with support for icons and number badges.
|
|
5
5
|
*/
|
|
6
|
-
export const
|
|
6
|
+
export const AfTab = React.forwardRef(({ label, active, disabled, shape, displayIcon, displayNumber, value, className, style, onAfTabClick, children, ...props }, ref) => {
|
|
7
7
|
const tabRef = React.useRef(null);
|
|
8
8
|
React.useEffect(() => {
|
|
9
9
|
const el = tabRef.current;
|
|
@@ -29,4 +29,4 @@ export const Tab = React.forwardRef(({ label, active, disabled, shape, displayIc
|
|
|
29
29
|
...props,
|
|
30
30
|
}, children);
|
|
31
31
|
});
|
|
32
|
-
|
|
32
|
+
AfTab.displayName = 'AfTab';
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
export type AfTabBarShape = 'square' | 'pill';
|
|
3
|
+
export type AfTabBarBreakpoint = 'mobile' | 'desktop';
|
|
4
|
+
export interface AfTabBarProps {
|
|
5
|
+
/** Visual shape variant for all tabs */
|
|
6
|
+
shape?: AfTabBarShape;
|
|
7
|
+
/** Responsive breakpoint mode */
|
|
8
|
+
breakpoint?: AfTabBarBreakpoint;
|
|
9
|
+
/** Additional class name */
|
|
10
|
+
className?: string;
|
|
11
|
+
/** Inline styles */
|
|
12
|
+
style?: React.CSSProperties;
|
|
13
|
+
/** Tab elements */
|
|
14
|
+
children?: React.ReactNode;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* AfTabBar component that contains and manages a group of tabs.
|
|
18
|
+
* Provides horizontal layout, keyboard navigation, and consistent styling.
|
|
19
|
+
*/
|
|
20
|
+
export declare const AfTabBar: React.ForwardRefExoticComponent<AfTabBarProps & React.RefAttributes<HTMLElement>>;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* AfTabBar component that contains and manages a group of tabs.
|
|
4
4
|
* Provides horizontal layout, keyboard navigation, and consistent styling.
|
|
5
5
|
*/
|
|
6
|
-
export const
|
|
6
|
+
export const AfTabBar = React.forwardRef(({ shape, breakpoint, className, style, children, ...props }, ref) => {
|
|
7
7
|
return React.createElement('af-tab-bar', {
|
|
8
8
|
ref,
|
|
9
9
|
shape,
|
|
@@ -13,4 +13,4 @@ export const TabBar = React.forwardRef(({ shape, breakpoint, className, style, c
|
|
|
13
13
|
...props,
|
|
14
14
|
}, children);
|
|
15
15
|
});
|
|
16
|
-
|
|
16
|
+
AfTabBar.displayName = 'AfTabBar';
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
export type AfToastVariant = 'info' | 'success' | 'warning' | 'error';
|
|
3
|
+
export interface AfToastProps {
|
|
4
|
+
/** Toast message */
|
|
5
|
+
message: string;
|
|
6
|
+
/** Toast variant */
|
|
7
|
+
variant?: AfToastVariant;
|
|
8
|
+
/** Whether the toast is visible */
|
|
9
|
+
isVisible: boolean;
|
|
10
|
+
/** Callback when toast should close */
|
|
11
|
+
onClose: () => void;
|
|
12
|
+
/** Auto-dismiss duration in ms (0 to disable) */
|
|
13
|
+
duration?: number;
|
|
14
|
+
/** Additional CSS styles */
|
|
15
|
+
style?: React.CSSProperties;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* AfToast - Draft toast/notification component.
|
|
19
|
+
*
|
|
20
|
+
* ⚠️ DRAFT: This component is not yet designed in Figma. Styles may change.
|
|
21
|
+
*/
|
|
22
|
+
export declare const AfToast: React.FC<AfToastProps>;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
/**
|
|
4
|
+
* AfToast - Draft toast/notification component.
|
|
5
|
+
*
|
|
6
|
+
* ⚠️ DRAFT: This component is not yet designed in Figma. Styles may change.
|
|
7
|
+
*/
|
|
8
|
+
export const AfToast = ({ message, variant = 'info', isVisible, onClose, duration = 5000, style, }) => {
|
|
9
|
+
const [isExiting, setIsExiting] = useState(false);
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
if (isVisible && duration > 0) {
|
|
12
|
+
const timer = setTimeout(() => {
|
|
13
|
+
setIsExiting(true);
|
|
14
|
+
setTimeout(onClose, 200);
|
|
15
|
+
}, duration);
|
|
16
|
+
return () => clearTimeout(timer);
|
|
17
|
+
}
|
|
18
|
+
}, [isVisible, duration, onClose]);
|
|
19
|
+
if (!isVisible)
|
|
20
|
+
return null;
|
|
21
|
+
const variantStyles = {
|
|
22
|
+
info: {
|
|
23
|
+
bg: 'var(--colour-tints-mist-green-100, #e8f0ee)',
|
|
24
|
+
border: 'var(--colour-brand-mist-green, #c6d5d1)',
|
|
25
|
+
icon: 'ℹ️',
|
|
26
|
+
},
|
|
27
|
+
success: {
|
|
28
|
+
bg: 'var(--colour-tints-mist-green-100, #e8f0ee)',
|
|
29
|
+
border: 'var(--colour-brand-azure, #7fe2d4)',
|
|
30
|
+
icon: '✓',
|
|
31
|
+
},
|
|
32
|
+
warning: {
|
|
33
|
+
bg: 'var(--colour-tints-softclay-100, #f5efe6)',
|
|
34
|
+
border: 'var(--colour-brand-soft-clay, #b09670)',
|
|
35
|
+
icon: '⚠',
|
|
36
|
+
},
|
|
37
|
+
error: {
|
|
38
|
+
bg: '#fef2f2',
|
|
39
|
+
border: '#fca5a5',
|
|
40
|
+
icon: '✕',
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
const currentVariant = variantStyles[variant];
|
|
44
|
+
const toastStyles = {
|
|
45
|
+
position: 'fixed',
|
|
46
|
+
bottom: '24px',
|
|
47
|
+
right: '24px',
|
|
48
|
+
display: 'flex',
|
|
49
|
+
alignItems: 'center',
|
|
50
|
+
gap: '12px',
|
|
51
|
+
padding: '14px 20px',
|
|
52
|
+
background: currentVariant.bg,
|
|
53
|
+
border: `1px solid ${currentVariant.border}`,
|
|
54
|
+
borderRadius: '8px',
|
|
55
|
+
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.1)',
|
|
56
|
+
fontFamily: 'var(--typography-bodyfont, system-ui, sans-serif)',
|
|
57
|
+
fontSize: '14px',
|
|
58
|
+
color: 'var(--colour-brand-inkwell, #14343b)',
|
|
59
|
+
zIndex: 1200,
|
|
60
|
+
transform: isExiting ? 'translateX(120%)' : 'translateX(0)',
|
|
61
|
+
opacity: isExiting ? 0 : 1,
|
|
62
|
+
transition: 'transform 0.2s ease, opacity 0.2s ease',
|
|
63
|
+
...style,
|
|
64
|
+
};
|
|
65
|
+
const closeButtonStyles = {
|
|
66
|
+
background: 'none',
|
|
67
|
+
border: 'none',
|
|
68
|
+
cursor: 'pointer',
|
|
69
|
+
padding: '4px',
|
|
70
|
+
display: 'flex',
|
|
71
|
+
alignItems: 'center',
|
|
72
|
+
justifyContent: 'center',
|
|
73
|
+
color: 'var(--colour-tints-inkwell-400, #7a8a8d)',
|
|
74
|
+
marginLeft: '8px',
|
|
75
|
+
};
|
|
76
|
+
return (_jsxs("div", { style: toastStyles, role: "alert", children: [_jsx("span", { style: { fontSize: '16px' }, children: currentVariant.icon }), _jsx("span", { style: { flex: 1 }, children: message }), _jsx("button", { type: "button", onClick: () => { setIsExiting(true); setTimeout(onClose, 200); }, style: closeButtonStyles, "aria-label": "Dismiss", children: _jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", children: _jsx("path", { d: "M18 6L6 18M6 6L18 18", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }) }) })] }));
|
|
77
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
export interface AfTooltipProps {
|
|
3
|
+
/** Tooltip content */
|
|
4
|
+
content: React.ReactNode;
|
|
5
|
+
/** Element that triggers the tooltip */
|
|
6
|
+
children: React.ReactElement;
|
|
7
|
+
/** Position of the tooltip */
|
|
8
|
+
position?: 'top' | 'bottom' | 'left' | 'right';
|
|
9
|
+
/** Delay before showing tooltip (ms) */
|
|
10
|
+
delay?: number;
|
|
11
|
+
/** Additional CSS styles */
|
|
12
|
+
style?: React.CSSProperties;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* AfTooltip - Draft tooltip component.
|
|
16
|
+
*
|
|
17
|
+
* ⚠️ DRAFT: This component is not yet designed in Figma. Styles may change.
|
|
18
|
+
*/
|
|
19
|
+
export declare const AfTooltip: React.FC<AfTooltipProps>;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useRef, useLayoutEffect } from 'react';
|
|
3
|
+
import { createPortal } from 'react-dom';
|
|
4
|
+
/**
|
|
5
|
+
* AfTooltip - Draft tooltip component.
|
|
6
|
+
*
|
|
7
|
+
* ⚠️ DRAFT: This component is not yet designed in Figma. Styles may change.
|
|
8
|
+
*/
|
|
9
|
+
export const AfTooltip = ({ content, children, position = 'top', delay = 200, style, }) => {
|
|
10
|
+
const [isVisible, setIsVisible] = useState(false);
|
|
11
|
+
const [coords, setCoords] = useState({ top: 0, left: 0 });
|
|
12
|
+
const triggerRef = useRef(null);
|
|
13
|
+
const tooltipRef = useRef(null);
|
|
14
|
+
const timeoutRef = useRef();
|
|
15
|
+
const updatePosition = () => {
|
|
16
|
+
if (!triggerRef.current || !tooltipRef.current)
|
|
17
|
+
return;
|
|
18
|
+
const triggerRect = triggerRef.current.getBoundingClientRect();
|
|
19
|
+
const tooltipRect = tooltipRef.current.getBoundingClientRect();
|
|
20
|
+
const gap = 8;
|
|
21
|
+
let top = 0;
|
|
22
|
+
let left = 0;
|
|
23
|
+
switch (position) {
|
|
24
|
+
case 'top':
|
|
25
|
+
top = triggerRect.top - tooltipRect.height - gap;
|
|
26
|
+
left = triggerRect.left + (triggerRect.width - tooltipRect.width) / 2;
|
|
27
|
+
break;
|
|
28
|
+
case 'bottom':
|
|
29
|
+
top = triggerRect.bottom + gap;
|
|
30
|
+
left = triggerRect.left + (triggerRect.width - tooltipRect.width) / 2;
|
|
31
|
+
break;
|
|
32
|
+
case 'left':
|
|
33
|
+
top = triggerRect.top + (triggerRect.height - tooltipRect.height) / 2;
|
|
34
|
+
left = triggerRect.left - tooltipRect.width - gap;
|
|
35
|
+
break;
|
|
36
|
+
case 'right':
|
|
37
|
+
top = triggerRect.top + (triggerRect.height - tooltipRect.height) / 2;
|
|
38
|
+
left = triggerRect.right + gap;
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
setCoords({ top, left });
|
|
42
|
+
};
|
|
43
|
+
// Use useLayoutEffect to position the tooltip before paint
|
|
44
|
+
useLayoutEffect(() => {
|
|
45
|
+
if (isVisible) {
|
|
46
|
+
updatePosition();
|
|
47
|
+
}
|
|
48
|
+
}, [isVisible, position]);
|
|
49
|
+
const handleMouseEnter = () => {
|
|
50
|
+
timeoutRef.current = setTimeout(() => {
|
|
51
|
+
setIsVisible(true);
|
|
52
|
+
}, delay);
|
|
53
|
+
};
|
|
54
|
+
const handleMouseLeave = () => {
|
|
55
|
+
if (timeoutRef.current) {
|
|
56
|
+
clearTimeout(timeoutRef.current);
|
|
57
|
+
}
|
|
58
|
+
setIsVisible(false);
|
|
59
|
+
};
|
|
60
|
+
const tooltipStyles = {
|
|
61
|
+
position: 'fixed',
|
|
62
|
+
top: coords.top,
|
|
63
|
+
left: coords.left,
|
|
64
|
+
background: 'var(--colour-brand-inkwell, #14343b)',
|
|
65
|
+
color: 'var(--colour-background-white, #fff)',
|
|
66
|
+
padding: '8px 12px',
|
|
67
|
+
borderRadius: '6px',
|
|
68
|
+
fontSize: '14px',
|
|
69
|
+
fontFamily: 'var(--typography-bodyfont, system-ui, sans-serif)',
|
|
70
|
+
zIndex: 1100,
|
|
71
|
+
pointerEvents: 'none',
|
|
72
|
+
whiteSpace: 'nowrap',
|
|
73
|
+
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
|
|
74
|
+
opacity: isVisible ? 1 : 0,
|
|
75
|
+
transition: 'opacity 0.15s ease',
|
|
76
|
+
...style,
|
|
77
|
+
};
|
|
78
|
+
const tooltipElement = isVisible ? (_jsx("div", { ref: tooltipRef, style: tooltipStyles, role: "tooltip", children: content })) : null;
|
|
79
|
+
return (_jsxs(_Fragment, { children: [_jsx("div", { ref: triggerRef, onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, onFocus: handleMouseEnter, onBlur: handleMouseLeave, style: { display: 'inline-block' }, children: children }), tooltipElement && createPortal(tooltipElement, document.body)] }));
|
|
80
|
+
};
|