@exotic-holidays/ui 0.1.0
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 +46 -0
- package/package.json +42 -0
- package/src/base-components/Accordion.tsx +561 -0
- package/src/base-components/Badge.tsx +191 -0
- package/src/base-components/Button.tsx +331 -0
- package/src/base-components/ButtonGroup.tsx +149 -0
- package/src/base-components/Card.tsx +250 -0
- package/src/base-components/Checkbox.tsx +49 -0
- package/src/base-components/ChipInput.tsx +208 -0
- package/src/base-components/CommonButton.tsx +33 -0
- package/src/base-components/DataTable.tsx +82 -0
- package/src/base-components/Divider.tsx +82 -0
- package/src/base-components/Dropdown.tsx +85 -0
- package/src/base-components/EmptyState.tsx +18 -0
- package/src/base-components/FilterPopover.tsx +50 -0
- package/src/base-components/Input.tsx +60 -0
- package/src/base-components/Modal.tsx +107 -0
- package/src/base-components/OtpVerificationModal.tsx +251 -0
- package/src/base-components/Pagination.tsx +51 -0
- package/src/base-components/PhoneInput.tsx +142 -0
- package/src/base-components/PopConfirm.tsx +350 -0
- package/src/base-components/SearchPopover.tsx +70 -0
- package/src/base-components/SearchableSelect.tsx +734 -0
- package/src/base-components/Select.tsx +49 -0
- package/src/base-components/Table.tsx +78 -0
- package/src/base-components/Textarea.tsx +45 -0
- package/src/base-components/ThemeProvider.tsx +92 -0
- package/src/base-components/Toaster.tsx +198 -0
- package/src/base-components/index.ts +32 -0
- package/src/components/DashboardLayout.tsx +326 -0
- package/src/components/ListPage.tsx +140 -0
- package/src/components/QuickAccess.tsx +118 -0
- package/src/components/UserMenu.tsx +138 -0
- package/src/helpers/bem.ts +13 -0
- package/src/helpers/cn.ts +9 -0
- package/src/index.ts +16 -0
- package/src/theme.css +285 -0
- package/tsconfig.json +11 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { cva } from 'class-variance-authority';
|
|
5
|
+
import { block, modifier } from '../helpers/bem';
|
|
6
|
+
import { cn } from '../helpers/cn';
|
|
7
|
+
|
|
8
|
+
export interface BadgeProps {
|
|
9
|
+
children?: React.ReactNode;
|
|
10
|
+
variant?: 'solid' | 'bordered' | 'light' | 'shadow' | 'dot' | 'flat';
|
|
11
|
+
color?: 'default' | 'primary' | 'secondary' | 'success' | 'warning' | 'danger';
|
|
12
|
+
size?: 'small' | 'medium' | 'large';
|
|
13
|
+
radius?: 'full' | 'medium' | 'small';
|
|
14
|
+
isDisabled?: boolean;
|
|
15
|
+
startContent?: React.ReactNode;
|
|
16
|
+
endContent?: React.ReactNode;
|
|
17
|
+
ariaLabel?: string;
|
|
18
|
+
className?: string;
|
|
19
|
+
/** BEM class name prefix (default 'aceui'). Usually set via AceUIProvider when using @aceuidev/core. */
|
|
20
|
+
prefix?: string;
|
|
21
|
+
testId?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const badgeVariants = cva(
|
|
25
|
+
[
|
|
26
|
+
'inline-flex items-center justify-center gap-[8px] font-medium leading-none whitespace-nowrap box-border',
|
|
27
|
+
],
|
|
28
|
+
{
|
|
29
|
+
variants: {
|
|
30
|
+
variant: {
|
|
31
|
+
solid: '',
|
|
32
|
+
bordered: '',
|
|
33
|
+
light: '',
|
|
34
|
+
shadow: '',
|
|
35
|
+
dot: '',
|
|
36
|
+
flat: '',
|
|
37
|
+
},
|
|
38
|
+
color: {
|
|
39
|
+
default: '',
|
|
40
|
+
primary: '',
|
|
41
|
+
secondary: '',
|
|
42
|
+
success: '',
|
|
43
|
+
warning: '',
|
|
44
|
+
danger: '',
|
|
45
|
+
},
|
|
46
|
+
size: {
|
|
47
|
+
small: 'text-[0.75rem] py-1 px-[8px] min-h-6',
|
|
48
|
+
medium: 'text-[0.875rem] py-1.5 px-[12px] min-h-7',
|
|
49
|
+
large: 'text-[1rem] py-1.5 px-[12px] min-h-8',
|
|
50
|
+
},
|
|
51
|
+
radius: {
|
|
52
|
+
full: 'rounded-full',
|
|
53
|
+
medium: 'rounded-[12px]',
|
|
54
|
+
small: 'rounded-[8px]',
|
|
55
|
+
},
|
|
56
|
+
isDisabled: {
|
|
57
|
+
true: 'opacity-[0.5] cursor-default',
|
|
58
|
+
false: '',
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
compoundVariants: [
|
|
62
|
+
// Solid (no border)
|
|
63
|
+
{ variant: 'solid', color: 'default', class: 'border-0 bg-default-500 text-default-foreground' },
|
|
64
|
+
{ variant: 'solid', color: 'primary', class: 'border-0 bg-primary-500 text-primary-foreground' },
|
|
65
|
+
{ variant: 'solid', color: 'secondary', class: 'border-0 bg-secondary-500 text-secondary-foreground' },
|
|
66
|
+
{ variant: 'solid', color: 'success', class: 'border-0 bg-success-500 text-success-foreground' },
|
|
67
|
+
{ variant: 'solid', color: 'warning', class: 'border-0 bg-warning-500 text-warning-foreground' },
|
|
68
|
+
{ variant: 'solid', color: 'danger', class: 'border-0 bg-danger-500 text-danger-foreground' },
|
|
69
|
+
// Bordered
|
|
70
|
+
{ variant: 'bordered', color: 'default', class: 'bg-transparent text-default-500 border border-default-500' },
|
|
71
|
+
{ variant: 'bordered', color: 'primary', class: 'bg-transparent text-primary-500 border border-primary-500' },
|
|
72
|
+
{ variant: 'bordered', color: 'secondary', class: 'bg-transparent text-secondary-500 border border-secondary-500' },
|
|
73
|
+
{ variant: 'bordered', color: 'success', class: 'bg-transparent text-success-500 border border-success-500' },
|
|
74
|
+
{ variant: 'bordered', color: 'warning', class: 'bg-transparent text-warning-500 border border-warning-500' },
|
|
75
|
+
{ variant: 'bordered', color: 'danger', class: 'bg-transparent text-danger-500 border border-danger-500' },
|
|
76
|
+
// Light (no border)
|
|
77
|
+
{ variant: 'light', color: 'default', class: 'border-0 bg-default-50 text-default-500' },
|
|
78
|
+
{ variant: 'light', color: 'primary', class: 'border-0 bg-primary-50 text-primary-500' },
|
|
79
|
+
{ variant: 'light', color: 'secondary', class: 'border-0 bg-secondary-50 text-secondary-500' },
|
|
80
|
+
{ variant: 'light', color: 'success', class: 'border-0 bg-success-50 text-success-500' },
|
|
81
|
+
{ variant: 'light', color: 'warning', class: 'border-0 bg-warning-50 text-warning-500' },
|
|
82
|
+
{ variant: 'light', color: 'danger', class: 'border-0 bg-danger-50 text-danger-500' },
|
|
83
|
+
// Shadow (no border; box-shadow via inline style)
|
|
84
|
+
{ variant: 'shadow', color: 'default', class: 'border-0 bg-default-500 text-default-foreground' },
|
|
85
|
+
{ variant: 'shadow', color: 'primary', class: 'border-0 bg-primary-500 text-primary-foreground' },
|
|
86
|
+
{ variant: 'shadow', color: 'secondary', class: 'border-0 bg-secondary-500 text-secondary-foreground' },
|
|
87
|
+
{ variant: 'shadow', color: 'success', class: 'border-0 bg-success-500 text-success-foreground' },
|
|
88
|
+
{ variant: 'shadow', color: 'warning', class: 'border-0 bg-warning-500 text-warning-foreground' },
|
|
89
|
+
{ variant: 'shadow', color: 'danger', class: 'border-0 bg-danger-500 text-danger-foreground' },
|
|
90
|
+
// Dot (no border)
|
|
91
|
+
{ variant: 'dot', color: 'default', class: 'border-0 bg-default-200 text-default-700' },
|
|
92
|
+
{ variant: 'dot', color: 'primary', class: 'border-0 bg-primary-50 text-primary-600' },
|
|
93
|
+
{ variant: 'dot', color: 'secondary', class: 'border-0 bg-secondary-50 text-secondary-600' },
|
|
94
|
+
{ variant: 'dot', color: 'success', class: 'border-0 bg-success-50 text-success-600' },
|
|
95
|
+
{ variant: 'dot', color: 'warning', class: 'border-0 bg-warning-50 text-warning-600' },
|
|
96
|
+
{ variant: 'dot', color: 'danger', class: 'border-0 bg-danger-50 text-danger-600' },
|
|
97
|
+
// Flat (light background)
|
|
98
|
+
{ variant: 'flat', color: 'default', class: 'border-0 bg-surface-0 text-foreground-muted' },
|
|
99
|
+
{ variant: 'flat', color: 'primary', class: 'border-0 bg-primary/10 text-primary' },
|
|
100
|
+
{ variant: 'flat', color: 'secondary', class: 'border-0 bg-secondary/10 text-secondary' },
|
|
101
|
+
{ variant: 'flat', color: 'success', class: 'border-0 bg-success/10 text-success' },
|
|
102
|
+
{ variant: 'flat', color: 'warning', class: 'border-0 bg-warning/10 text-warning' },
|
|
103
|
+
{ variant: 'flat', color: 'danger', class: 'border-0 bg-danger-alt/10 text-danger-alt' },
|
|
104
|
+
],
|
|
105
|
+
defaultVariants: {
|
|
106
|
+
variant: 'solid',
|
|
107
|
+
color: 'primary',
|
|
108
|
+
size: 'medium',
|
|
109
|
+
radius: 'full',
|
|
110
|
+
isDisabled: false,
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
const dotColorMap: Record<string, string> = {
|
|
116
|
+
default: 'bg-default-500',
|
|
117
|
+
primary: 'bg-primary-500',
|
|
118
|
+
secondary: 'bg-secondary-500',
|
|
119
|
+
success: 'bg-success-500',
|
|
120
|
+
warning: 'bg-warning-500',
|
|
121
|
+
danger: 'bg-danger-500',
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
export const Badge = ({
|
|
125
|
+
children,
|
|
126
|
+
variant = 'solid',
|
|
127
|
+
color = 'primary',
|
|
128
|
+
size = 'medium',
|
|
129
|
+
radius = 'full',
|
|
130
|
+
isDisabled = false,
|
|
131
|
+
startContent,
|
|
132
|
+
endContent,
|
|
133
|
+
ariaLabel,
|
|
134
|
+
className,
|
|
135
|
+
prefix = 'aceui',
|
|
136
|
+
testId,
|
|
137
|
+
}: BadgeProps) => {
|
|
138
|
+
const blockClass = block(prefix, 'badge');
|
|
139
|
+
const bemClasses = [
|
|
140
|
+
blockClass,
|
|
141
|
+
modifier(prefix, 'badge', variant),
|
|
142
|
+
modifier(prefix, 'badge', color),
|
|
143
|
+
modifier(prefix, 'badge', size),
|
|
144
|
+
modifier(prefix, 'badge', radius),
|
|
145
|
+
isDisabled && modifier(prefix, 'badge', 'disabled'),
|
|
146
|
+
].filter(Boolean) as string[];
|
|
147
|
+
|
|
148
|
+
const shadowStyle =
|
|
149
|
+
variant === 'shadow'
|
|
150
|
+
? {
|
|
151
|
+
boxShadow: `0 2px 8px 0 color-mix(in srgb, var(--color-${color}-500) calc(var(--aceui-badge-shadow-opacity) * 100%), transparent)`,
|
|
152
|
+
}
|
|
153
|
+
: undefined;
|
|
154
|
+
|
|
155
|
+
const showDot = variant === 'dot';
|
|
156
|
+
|
|
157
|
+
return (
|
|
158
|
+
<span
|
|
159
|
+
className={cn(bemClasses, badgeVariants({ variant, color, size, radius, isDisabled }), className)}
|
|
160
|
+
style={shadowStyle}
|
|
161
|
+
aria-disabled={isDisabled ? true : undefined}
|
|
162
|
+
aria-label={ariaLabel}
|
|
163
|
+
data-testid={testId}
|
|
164
|
+
>
|
|
165
|
+
{startContent != null ? (
|
|
166
|
+
<span className="inline-flex items-center leading-none">{startContent}</span>
|
|
167
|
+
) : null}
|
|
168
|
+
{showDot ? (
|
|
169
|
+
<span
|
|
170
|
+
className={cn('w-2 h-2 rounded-full shrink-0', dotColorMap[color])}
|
|
171
|
+
aria-hidden
|
|
172
|
+
/>
|
|
173
|
+
) : null}
|
|
174
|
+
{children != null ? (
|
|
175
|
+
<span
|
|
176
|
+
className={cn(
|
|
177
|
+
'inline-flex items-center leading-none',
|
|
178
|
+
size !== 'small' && '-translate-y-[0.06em]',
|
|
179
|
+
)}
|
|
180
|
+
>
|
|
181
|
+
{children}
|
|
182
|
+
</span>
|
|
183
|
+
) : null}
|
|
184
|
+
{endContent != null ? (
|
|
185
|
+
<span className="inline-flex items-center leading-none">{endContent}</span>
|
|
186
|
+
) : null}
|
|
187
|
+
</span>
|
|
188
|
+
);
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
Badge.displayName = 'Badge';
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useState, useRef } from 'react';
|
|
4
|
+
import { motion } from 'framer-motion';
|
|
5
|
+
import { cva } from 'class-variance-authority';
|
|
6
|
+
import { block, modifier } from '../helpers/bem';
|
|
7
|
+
import { cn } from '../helpers/cn';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Button component props interface
|
|
11
|
+
*/
|
|
12
|
+
export interface ButtonProps {
|
|
13
|
+
/** The content to display inside the button */
|
|
14
|
+
children?: React.ReactNode;
|
|
15
|
+
|
|
16
|
+
/** The visual style variant of the button */
|
|
17
|
+
variant?: 'solid' | 'bordered' | 'light' | 'flat' | 'shadow' | 'ghost';
|
|
18
|
+
|
|
19
|
+
/** The color scheme of the button */
|
|
20
|
+
color?: 'default' | 'primary' | 'secondary' | 'success' | 'warning' | 'danger';
|
|
21
|
+
|
|
22
|
+
/** The size of the button */
|
|
23
|
+
size?: 'small' | 'medium' | 'large';
|
|
24
|
+
|
|
25
|
+
/** The border radius of the button */
|
|
26
|
+
radius?: 'none' | 'small' | 'medium' | 'large' | 'full' | 'squircle';
|
|
27
|
+
|
|
28
|
+
/** Whether the button is disabled */
|
|
29
|
+
isDisabled?: boolean;
|
|
30
|
+
|
|
31
|
+
/** Whether the button is in loading state */
|
|
32
|
+
isLoading?: boolean;
|
|
33
|
+
|
|
34
|
+
/** Icon element to display */
|
|
35
|
+
icon?: React.ReactNode;
|
|
36
|
+
|
|
37
|
+
/** Position of the icon relative to text */
|
|
38
|
+
iconPosition?: 'left' | 'right';
|
|
39
|
+
|
|
40
|
+
/** Custom text to show during loading state */
|
|
41
|
+
loadingText?: string;
|
|
42
|
+
|
|
43
|
+
/** Click event handler */
|
|
44
|
+
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
|
45
|
+
|
|
46
|
+
/** Accessible label for screen readers */
|
|
47
|
+
ariaLabel?: string;
|
|
48
|
+
|
|
49
|
+
/** HTML button type */
|
|
50
|
+
type?: 'button' | 'submit' | 'reset';
|
|
51
|
+
|
|
52
|
+
/** Whether to disable the click wave (ripple) effect */
|
|
53
|
+
disableWave?: boolean;
|
|
54
|
+
|
|
55
|
+
/** Custom width (any valid CSS width: e.g. "4rem", "auto", "10px", "100%", "100vw") */
|
|
56
|
+
width?: React.CSSProperties['width'];
|
|
57
|
+
|
|
58
|
+
/** Additional CSS class names */
|
|
59
|
+
className?: string;
|
|
60
|
+
|
|
61
|
+
/** BEM class name prefix (default 'aceui'). Usually set via AceUIProvider when using @aceuidev/core. */
|
|
62
|
+
prefix?: string;
|
|
63
|
+
|
|
64
|
+
/** Test ID for testing */
|
|
65
|
+
testId?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const iconSizeMap = {
|
|
69
|
+
small: 'w-4 h-4',
|
|
70
|
+
medium: 'w-5 h-5',
|
|
71
|
+
large: 'w-6 h-6',
|
|
72
|
+
} as const;
|
|
73
|
+
|
|
74
|
+
export const buttonVariants = cva(
|
|
75
|
+
[
|
|
76
|
+
'inline-flex items-center justify-center gap-2 font-[inherit] font-medium cursor-pointer transition-all duration-200 relative whitespace-nowrap select-none outline-none overflow-hidden',
|
|
77
|
+
'focus-visible:outline-2 focus-visible:outline-[var(--aceui-color-focus)] focus-visible:outline-offset-2',
|
|
78
|
+
],
|
|
79
|
+
{
|
|
80
|
+
variants: {
|
|
81
|
+
variant: {
|
|
82
|
+
solid: '',
|
|
83
|
+
bordered: '',
|
|
84
|
+
light: '',
|
|
85
|
+
flat: '',
|
|
86
|
+
shadow: '',
|
|
87
|
+
ghost: '',
|
|
88
|
+
},
|
|
89
|
+
color: {
|
|
90
|
+
default: '',
|
|
91
|
+
primary: '',
|
|
92
|
+
secondary: '',
|
|
93
|
+
success: '',
|
|
94
|
+
warning: '',
|
|
95
|
+
danger: '',
|
|
96
|
+
},
|
|
97
|
+
size: {
|
|
98
|
+
small: 'text-[0.75rem] leading-[1rem] px-3 min-h-8',
|
|
99
|
+
medium: 'text-[0.875rem] leading-[1.25rem] px-4 min-h-10',
|
|
100
|
+
large: 'text-[1rem] leading-[1.5rem] px-6 min-h-12',
|
|
101
|
+
},
|
|
102
|
+
radius: {
|
|
103
|
+
none: 'rounded-none',
|
|
104
|
+
small: 'rounded-[8px]',
|
|
105
|
+
medium: 'rounded-[12px]',
|
|
106
|
+
large: 'rounded-[14px]',
|
|
107
|
+
full: 'rounded-full',
|
|
108
|
+
squircle: 'rounded-full',
|
|
109
|
+
},
|
|
110
|
+
isIconOnly: {
|
|
111
|
+
true: 'aspect-square !px-0 w-auto',
|
|
112
|
+
false: '',
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
compoundVariants: [
|
|
116
|
+
/* Solid */
|
|
117
|
+
{ variant: 'solid', color: 'default', class: 'border-none bg-default-500 text-default-foreground hover:bg-default-600 active:bg-default-700' },
|
|
118
|
+
{ variant: 'solid', color: 'primary', class: 'border-none bg-primary-500 text-primary-foreground hover:bg-primary-600 active:bg-primary-700' },
|
|
119
|
+
{ variant: 'solid', color: 'secondary', class: 'border-none bg-secondary-500 text-secondary-foreground hover:bg-secondary-600 active:bg-secondary-700' },
|
|
120
|
+
{ variant: 'solid', color: 'success', class: 'border-none bg-success-500 text-success-foreground hover:bg-success-600 active:bg-success-700' },
|
|
121
|
+
{ variant: 'solid', color: 'warning', class: 'border-none bg-warning-500 text-warning-foreground hover:bg-warning-600 active:bg-warning-700' },
|
|
122
|
+
{ variant: 'solid', color: 'danger', class: 'border-none bg-danger-500 text-danger-foreground hover:bg-danger-600 active:bg-danger-700' },
|
|
123
|
+
/* Bordered */
|
|
124
|
+
{ variant: 'bordered', color: 'default', class: 'bg-transparent text-foreground-1 border-[1px] border-border-strong hover:bg-surface-hover' },
|
|
125
|
+
{ variant: 'bordered', color: 'primary', class: 'bg-transparent text-primary-500 border-[1px] border-primary-500 hover:bg-primary-50' },
|
|
126
|
+
{ variant: 'bordered', color: 'secondary', class: 'bg-transparent text-foreground-1 border-[1px] border-border-strong hover:bg-surface-hover' },
|
|
127
|
+
{ variant: 'bordered', color: 'success', class: 'bg-transparent text-success-500 border-[1px] border-success-500 hover:bg-success-50' },
|
|
128
|
+
{ variant: 'bordered', color: 'warning', class: 'bg-transparent text-warning-500 border-[1px] border-warning-500 hover:bg-warning-50' },
|
|
129
|
+
{ variant: 'bordered', color: 'danger', class: 'bg-transparent text-danger-500 border-[1px] border-danger-500 hover:bg-danger-50' },
|
|
130
|
+
/* Light */
|
|
131
|
+
{ variant: 'light', color: 'default', class: 'bg-surface-hover text-foreground-1 border-none hover:bg-surface-subtle hover:text-foreground-0' },
|
|
132
|
+
{ variant: 'light', color: 'primary', class: 'bg-primary-50 text-primary-500 border-none hover:bg-primary-100' },
|
|
133
|
+
{ variant: 'light', color: 'secondary', class: 'bg-surface-hover text-foreground-1 border-none hover:bg-surface-subtle hover:text-foreground-0' },
|
|
134
|
+
{ variant: 'light', color: 'success', class: 'bg-success-50 text-success-500 border-none hover:bg-success-100' },
|
|
135
|
+
{ variant: 'light', color: 'warning', class: 'bg-warning-50 text-warning-500 border-none hover:bg-warning-100' },
|
|
136
|
+
{ variant: 'light', color: 'danger', class: 'bg-danger-50 text-danger-500 border-none hover:bg-danger-100' },
|
|
137
|
+
/* Flat */
|
|
138
|
+
{ variant: 'flat', color: 'default', class: 'bg-transparent text-foreground-1 border-none hover:bg-surface-hover hover:text-foreground-0' },
|
|
139
|
+
{ variant: 'flat', color: 'primary', class: 'bg-transparent text-primary-500 border-none hover:bg-primary-50' },
|
|
140
|
+
{ variant: 'flat', color: 'secondary', class: 'bg-transparent text-foreground-1 border-none hover:bg-surface-hover hover:text-foreground-0' },
|
|
141
|
+
{ variant: 'flat', color: 'success', class: 'bg-transparent text-success-500 border-none hover:bg-success-50' },
|
|
142
|
+
{ variant: 'flat', color: 'warning', class: 'bg-transparent text-warning-500 border-none hover:bg-warning-50' },
|
|
143
|
+
{ variant: 'flat', color: 'danger', class: 'bg-transparent text-danger-500 border-none hover:bg-danger-50' },
|
|
144
|
+
/* Shadow */
|
|
145
|
+
{ variant: 'shadow', color: 'default', class: 'border-none bg-default-500 text-default-foreground hover:bg-default-600 active:bg-default-700' },
|
|
146
|
+
{ variant: 'shadow', color: 'primary', class: 'border-none bg-primary-500 text-primary-foreground hover:bg-primary-600 active:bg-primary-700' },
|
|
147
|
+
{ variant: 'shadow', color: 'secondary', class: 'border-none bg-secondary-500 text-secondary-foreground hover:bg-secondary-600 active:bg-secondary-700' },
|
|
148
|
+
{ variant: 'shadow', color: 'success', class: 'border-none bg-success-500 text-success-foreground hover:bg-success-600 active:bg-success-700' },
|
|
149
|
+
{ variant: 'shadow', color: 'warning', class: 'border-none bg-warning-500 text-warning-foreground hover:bg-warning-600 active:bg-warning-700' },
|
|
150
|
+
{ variant: 'shadow', color: 'danger', class: 'border-none bg-danger-500 text-danger-foreground hover:bg-danger-600 active:bg-danger-700' },
|
|
151
|
+
/* Ghost */
|
|
152
|
+
{ variant: 'ghost', color: 'default', class: 'border-none bg-transparent text-foreground-1 hover:bg-surface-hover hover:text-foreground-0' },
|
|
153
|
+
{ variant: 'ghost', color: 'primary', class: 'border-none bg-transparent text-primary-500 hover:bg-primary-500 hover:text-primary-foreground' },
|
|
154
|
+
{ variant: 'ghost', color: 'secondary', class: 'border-none bg-transparent text-foreground-1 hover:bg-surface-hover hover:text-foreground-0' },
|
|
155
|
+
{ variant: 'ghost', color: 'success', class: 'border-none bg-transparent text-success-500 hover:bg-success-500 hover:text-success-foreground' },
|
|
156
|
+
{ variant: 'ghost', color: 'warning', class: 'border-none bg-transparent text-warning-500 hover:bg-warning-500 hover:text-warning-foreground' },
|
|
157
|
+
{ variant: 'ghost', color: 'danger', class: 'border-none bg-transparent text-danger-500 hover:bg-danger-500 hover:text-danger-foreground' },
|
|
158
|
+
/* Icon-only size overrides */
|
|
159
|
+
{ isIconOnly: true, size: 'small', class: 'p-2 min-w-8' },
|
|
160
|
+
{ isIconOnly: true, size: 'medium', class: 'p-3 min-w-10' },
|
|
161
|
+
{ isIconOnly: true, size: 'large', class: 'p-4 min-w-12' },
|
|
162
|
+
],
|
|
163
|
+
defaultVariants: {
|
|
164
|
+
variant: 'solid',
|
|
165
|
+
color: 'primary',
|
|
166
|
+
size: 'medium',
|
|
167
|
+
radius: 'medium',
|
|
168
|
+
isIconOnly: false,
|
|
169
|
+
},
|
|
170
|
+
}
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
export const Button = ({
|
|
174
|
+
children,
|
|
175
|
+
variant = 'solid',
|
|
176
|
+
color = 'primary',
|
|
177
|
+
size = 'medium',
|
|
178
|
+
radius = 'squircle',
|
|
179
|
+
isDisabled = false,
|
|
180
|
+
isLoading = false,
|
|
181
|
+
icon,
|
|
182
|
+
iconPosition = 'left',
|
|
183
|
+
loadingText,
|
|
184
|
+
onClick,
|
|
185
|
+
ariaLabel,
|
|
186
|
+
type = 'button',
|
|
187
|
+
width,
|
|
188
|
+
className = '',
|
|
189
|
+
prefix = 'aceui',
|
|
190
|
+
testId,
|
|
191
|
+
disableWave = false,
|
|
192
|
+
}: ButtonProps) => {
|
|
193
|
+
const isIconOnly = Boolean(!children && icon);
|
|
194
|
+
const isActuallyDisabled = isDisabled || isLoading;
|
|
195
|
+
|
|
196
|
+
const blockClass = block(prefix, 'button');
|
|
197
|
+
const bemClasses = [
|
|
198
|
+
blockClass,
|
|
199
|
+
modifier(prefix, 'button', variant),
|
|
200
|
+
modifier(prefix, 'button', color),
|
|
201
|
+
modifier(prefix, 'button', size),
|
|
202
|
+
modifier(prefix, 'button', radius),
|
|
203
|
+
isActuallyDisabled && modifier(prefix, 'button', 'disabled'),
|
|
204
|
+
isLoading && modifier(prefix, 'button', 'loading'),
|
|
205
|
+
isIconOnly && modifier(prefix, 'button', 'icon-only'),
|
|
206
|
+
].filter(Boolean) as string[];
|
|
207
|
+
|
|
208
|
+
const [wave, setWave] = useState<{ x: number; y: number; size: number; key: number } | null>(null);
|
|
209
|
+
const waveKeyRef = useRef(0);
|
|
210
|
+
|
|
211
|
+
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
|
212
|
+
if (isActuallyDisabled) {
|
|
213
|
+
event.preventDefault();
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
if (!disableWave) {
|
|
217
|
+
const el = event.currentTarget;
|
|
218
|
+
const rect = el.getBoundingClientRect();
|
|
219
|
+
const x = event.clientX - rect.left;
|
|
220
|
+
const y = event.clientY - rect.top;
|
|
221
|
+
const size = 2 * Math.max(rect.width, rect.height);
|
|
222
|
+
waveKeyRef.current += 1;
|
|
223
|
+
setWave({ x, y, size, key: waveKeyRef.current });
|
|
224
|
+
}
|
|
225
|
+
onClick?.(event);
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const shadowStyle =
|
|
229
|
+
variant === 'shadow'
|
|
230
|
+
? {
|
|
231
|
+
boxShadow: `0 4px 14px 0 color-mix(in srgb, var(--color-${color}-500) 40%, transparent)`,
|
|
232
|
+
}
|
|
233
|
+
: undefined;
|
|
234
|
+
// const cornerShapeStyle = radius === 'squircle' ? {
|
|
235
|
+
// cornerShape: 'squircle',
|
|
236
|
+
// } : undefined;
|
|
237
|
+
// const style = { ...shadowStyle, ...cornerShapeStyle, ...(width != null ? { width } : {}) };
|
|
238
|
+
const style = { ...shadowStyle, ...(width != null ? { width } : {}) };
|
|
239
|
+
|
|
240
|
+
const renderContent = () => {
|
|
241
|
+
if (isLoading) {
|
|
242
|
+
return (
|
|
243
|
+
<>
|
|
244
|
+
<span
|
|
245
|
+
className="inline-block w-[1em] h-[1em] border-2 border-current border-r-transparent rounded-full animate-spin"
|
|
246
|
+
aria-hidden="true"
|
|
247
|
+
/>
|
|
248
|
+
{loadingText && <span className="inline-block leading-none">{loadingText}</span>}
|
|
249
|
+
{!loadingText && children && <span className="inline-block leading-none">{children}</span>}
|
|
250
|
+
</>
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (isIconOnly) {
|
|
255
|
+
return (
|
|
256
|
+
<span className={cn('inline-flex items-center justify-center shrink-0', iconSizeMap[size])}>
|
|
257
|
+
{icon}
|
|
258
|
+
</span>
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (icon) {
|
|
263
|
+
const iconElement = (
|
|
264
|
+
<span className={cn('inline-flex items-center justify-center shrink-0', iconSizeMap[size])}>
|
|
265
|
+
{icon}
|
|
266
|
+
</span>
|
|
267
|
+
);
|
|
268
|
+
const textElement = children && <span className="inline-block leading-none">{children}</span>;
|
|
269
|
+
|
|
270
|
+
return iconPosition === 'left' ? (
|
|
271
|
+
<>
|
|
272
|
+
{iconElement}
|
|
273
|
+
{textElement}
|
|
274
|
+
</>
|
|
275
|
+
) : (
|
|
276
|
+
<>
|
|
277
|
+
{textElement}
|
|
278
|
+
{iconElement}
|
|
279
|
+
</>
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return children ? <span className="inline-block leading-none">{children}</span> : null;
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
return (
|
|
287
|
+
<button
|
|
288
|
+
type={type}
|
|
289
|
+
className={cn(
|
|
290
|
+
bemClasses,
|
|
291
|
+
buttonVariants({
|
|
292
|
+
variant,
|
|
293
|
+
color,
|
|
294
|
+
size,
|
|
295
|
+
radius,
|
|
296
|
+
isIconOnly,
|
|
297
|
+
}),
|
|
298
|
+
isActuallyDisabled ? '!opacity-50 !cursor-not-allowed pointer-events-none' : '',
|
|
299
|
+
className
|
|
300
|
+
)}
|
|
301
|
+
style={Object.keys(style).length > 0 ? style : undefined}
|
|
302
|
+
disabled={isActuallyDisabled}
|
|
303
|
+
onClick={handleClick}
|
|
304
|
+
aria-label={ariaLabel}
|
|
305
|
+
aria-busy={isLoading ? 'true' : undefined}
|
|
306
|
+
data-testid={testId}
|
|
307
|
+
>
|
|
308
|
+
{wave && (
|
|
309
|
+
<motion.span
|
|
310
|
+
key={wave.key}
|
|
311
|
+
className="absolute rounded-full pointer-events-none"
|
|
312
|
+
style={{
|
|
313
|
+
left: wave.x,
|
|
314
|
+
top: wave.y,
|
|
315
|
+
width: wave.size,
|
|
316
|
+
height: wave.size,
|
|
317
|
+
background: 'color-mix(in srgb, currentColor 30%, transparent)',
|
|
318
|
+
}}
|
|
319
|
+
initial={{ scale: 0, x: '-50%', y: '-50%', opacity: 1 }}
|
|
320
|
+
animate={{ scale: 1, x: '-50%', y: '-50%', opacity: 0 }}
|
|
321
|
+
transition={{ duration: 0.65, ease: 'easeOut' }}
|
|
322
|
+
onAnimationComplete={() => setWave(null)}
|
|
323
|
+
aria-hidden="true"
|
|
324
|
+
/>
|
|
325
|
+
)}
|
|
326
|
+
{renderContent()}
|
|
327
|
+
</button>
|
|
328
|
+
);
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
Button.displayName = 'Button';
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { block, modifier } from '../helpers/bem';
|
|
5
|
+
import { cn } from '../helpers/cn';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* ButtonGroup component props interface
|
|
9
|
+
*/
|
|
10
|
+
export interface ButtonGroupProps {
|
|
11
|
+
/** Button components (or compatible elements) to group */
|
|
12
|
+
children?: React.ReactNode;
|
|
13
|
+
|
|
14
|
+
/** When true, disables all buttons inside the group */
|
|
15
|
+
isDisabled?: boolean;
|
|
16
|
+
|
|
17
|
+
/** Layout direction of the group */
|
|
18
|
+
orientation?: 'horizontal' | 'vertical';
|
|
19
|
+
|
|
20
|
+
/** Accessible label for the group */
|
|
21
|
+
ariaLabel?: string;
|
|
22
|
+
|
|
23
|
+
/** Additional CSS class names */
|
|
24
|
+
className?: string;
|
|
25
|
+
|
|
26
|
+
/** BEM class name prefix (default 'aceui'). Usually set via AceUIProvider when using @aceuidev/core. */
|
|
27
|
+
prefix?: string;
|
|
28
|
+
|
|
29
|
+
/** Test ID for testing */
|
|
30
|
+
testId?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const BUTTON_DISABLED_PROP = 'isDisabled';
|
|
34
|
+
|
|
35
|
+
function isReactElement(child: React.ReactNode): child is React.ReactElement<Record<string, unknown>> {
|
|
36
|
+
return typeof child === 'object' && child !== null && 'type' in child && 'props' in child;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function getPositionClasses(
|
|
40
|
+
isHorizontal: boolean,
|
|
41
|
+
isFirst: boolean,
|
|
42
|
+
isLast: boolean
|
|
43
|
+
): string {
|
|
44
|
+
if (isFirst && isLast) return '';
|
|
45
|
+
if (isHorizontal) {
|
|
46
|
+
if (isFirst) return '!rounded-tr-none !rounded-br-none';
|
|
47
|
+
if (isLast) return '!rounded-tl-none !rounded-bl-none';
|
|
48
|
+
return '!rounded-none';
|
|
49
|
+
}
|
|
50
|
+
if (isFirst) return '!rounded-bl-none !rounded-br-none';
|
|
51
|
+
if (isLast) return '!rounded-tl-none !rounded-tr-none';
|
|
52
|
+
return '!rounded-none';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function getBorderCollapseClass(
|
|
56
|
+
isHorizontal: boolean,
|
|
57
|
+
isLast: boolean,
|
|
58
|
+
child: React.ReactElement<Record<string, unknown>>
|
|
59
|
+
): string {
|
|
60
|
+
if (isLast) return '';
|
|
61
|
+
if (child.props?.variant !== 'bordered') return '';
|
|
62
|
+
return isHorizontal ? '!border-r-0' : '!border-b-0';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Groups multiple Button components into a single visual unit.
|
|
67
|
+
* Supports horizontal/vertical layout, seamless styling, and group-level isDisabled.
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* ```tsx
|
|
71
|
+
* <ButtonGroup>
|
|
72
|
+
* <Button>One</Button>
|
|
73
|
+
* <Button>Two</Button>
|
|
74
|
+
* <Button>Three</Button>
|
|
75
|
+
* </ButtonGroup>
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
78
|
+
export const ButtonGroup = ({
|
|
79
|
+
children,
|
|
80
|
+
isDisabled = false,
|
|
81
|
+
orientation = 'horizontal',
|
|
82
|
+
ariaLabel,
|
|
83
|
+
className = '',
|
|
84
|
+
prefix = 'aceui',
|
|
85
|
+
testId,
|
|
86
|
+
}: ButtonGroupProps) => {
|
|
87
|
+
const isHorizontal = orientation === 'horizontal';
|
|
88
|
+
|
|
89
|
+
const blockClass = block(prefix, 'button-group');
|
|
90
|
+
const bemClasses = [
|
|
91
|
+
blockClass,
|
|
92
|
+
modifier(prefix, 'button-group', orientation),
|
|
93
|
+
isDisabled && modifier(prefix, 'button-group', 'disabled'),
|
|
94
|
+
].filter(Boolean) as string[];
|
|
95
|
+
|
|
96
|
+
const classes = cn(
|
|
97
|
+
bemClasses,
|
|
98
|
+
'inline-flex items-stretch gap-0',
|
|
99
|
+
isHorizontal ? 'flex-row' : 'flex-col',
|
|
100
|
+
className
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
const allChildren = React.Children.toArray(children);
|
|
104
|
+
const elementChildren = allChildren.filter(isReactElement);
|
|
105
|
+
const elementCount = elementChildren.length;
|
|
106
|
+
let elementIdx = 0;
|
|
107
|
+
|
|
108
|
+
const renderedChildren = allChildren.map((child, mapIdx) => {
|
|
109
|
+
if (!isReactElement(child)) {
|
|
110
|
+
if (isDisabled && child != null) {
|
|
111
|
+
return (
|
|
112
|
+
<div key={mapIdx} style={{ pointerEvents: 'none' }} aria-disabled="true">
|
|
113
|
+
{child}
|
|
114
|
+
</div>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
return child;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const idx = elementIdx++;
|
|
121
|
+
const isFirst = idx === 0;
|
|
122
|
+
const isLast = idx === elementCount - 1;
|
|
123
|
+
|
|
124
|
+
const positionCls = getPositionClasses(isHorizontal, isFirst, isLast);
|
|
125
|
+
const borderCls = getBorderCollapseClass(isHorizontal, isLast, child);
|
|
126
|
+
const extraClasses = cn(positionCls, borderCls);
|
|
127
|
+
|
|
128
|
+
const props: Record<string, unknown> = {};
|
|
129
|
+
if (isDisabled) props[BUTTON_DISABLED_PROP] = true;
|
|
130
|
+
if (extraClasses) props.className = cn(child.props?.className as string, extraClasses);
|
|
131
|
+
|
|
132
|
+
return Object.keys(props).length > 0
|
|
133
|
+
? React.cloneElement(child, props)
|
|
134
|
+
: child;
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
return (
|
|
138
|
+
<div
|
|
139
|
+
role="group"
|
|
140
|
+
className={classes}
|
|
141
|
+
aria-label={ariaLabel}
|
|
142
|
+
data-testid={testId}
|
|
143
|
+
>
|
|
144
|
+
{renderedChildren}
|
|
145
|
+
</div>
|
|
146
|
+
);
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
ButtonGroup.displayName = 'ButtonGroup';
|