@apptimate/ui 1.0.0 → 1.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/CHANGELOG.md +7 -7
- package/package.json +21 -18
- package/src/{Button.tsx → base-components/Button.tsx} +21 -13
- package/src/{ChipInput.tsx → base-components/ChipInput.tsx} +1 -1
- package/src/base-components/DesktopFilterPopover.tsx +53 -0
- package/src/base-components/DesktopSearchPopover.tsx +51 -0
- package/src/{Dropdown.tsx → base-components/Dropdown.tsx} +11 -5
- package/src/{Input.tsx → base-components/Input.tsx} +8 -7
- package/src/base-components/Label.tsx +24 -0
- package/src/{Modal.tsx → base-components/Modal.tsx} +21 -7
- package/src/base-components/RecordCount.tsx +19 -0
- package/src/{SearchableSelect.tsx → base-components/SearchableSelect.tsx} +9 -9
- package/src/{Select.tsx → base-components/Select.tsx} +7 -6
- package/src/base-components/Table.tsx +83 -0
- package/src/base-components/TableMobileOptions.tsx +51 -0
- package/src/common-components/Charts.tsx +222 -0
- package/src/common-components/DashboardLayout.tsx +462 -0
- package/src/index.tsx +25 -18
- package/src/Table.tsx +0 -78
- /package/src/{Accordion.tsx → base-components/Accordion.tsx} +0 -0
- /package/src/{Badge.tsx → base-components/Badge.tsx} +0 -0
- /package/src/{ButtonGroup.tsx → base-components/ButtonGroup.tsx} +0 -0
- /package/src/{Card.tsx → base-components/Card.tsx} +0 -0
- /package/src/{Checkbox.tsx → base-components/Checkbox.tsx} +0 -0
- /package/src/{Divider.tsx → base-components/Divider.tsx} +0 -0
- /package/src/{EmptyState.tsx → base-components/EmptyState.tsx} +0 -0
- /package/src/{HintIcon.tsx → base-components/HintIcon.tsx} +0 -0
- /package/src/{Pagination.tsx → base-components/Pagination.tsx} +0 -0
- /package/src/{PopConfirm.tsx → base-components/PopConfirm.tsx} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
# @apptimate/ui
|
|
2
|
-
|
|
3
|
-
## 1.0.0
|
|
4
|
-
|
|
5
|
-
### Major Changes
|
|
6
|
-
|
|
7
|
-
- init
|
|
1
|
+
# @apptimate/ui
|
|
2
|
+
|
|
3
|
+
## 1.0.0
|
|
4
|
+
|
|
5
|
+
### Major Changes
|
|
6
|
+
|
|
7
|
+
- init
|
package/package.json
CHANGED
|
@@ -1,18 +1,21 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@apptimate/ui",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"main": "src/index.tsx",
|
|
5
|
-
"types": "src/index.tsx",
|
|
6
|
-
"dependencies": {
|
|
7
|
-
"class-variance-authority": "^0.7.1",
|
|
8
|
-
"clsx": "^2.1.1",
|
|
9
|
-
"framer-motion": "^12.38.0",
|
|
10
|
-
"lucide-react": "^1.14.0",
|
|
11
|
-
"react": "
|
|
12
|
-
"tailwind-merge": "^3.5.0",
|
|
13
|
-
"@apptimate/core-lib": "*"
|
|
14
|
-
},
|
|
15
|
-
"publishConfig": {
|
|
16
|
-
"access": "public"
|
|
17
|
-
}
|
|
18
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@apptimate/ui",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"main": "src/index.tsx",
|
|
5
|
+
"types": "src/index.tsx",
|
|
6
|
+
"dependencies": {
|
|
7
|
+
"class-variance-authority": "^0.7.1",
|
|
8
|
+
"clsx": "^2.1.1",
|
|
9
|
+
"framer-motion": "^12.38.0",
|
|
10
|
+
"lucide-react": "^1.14.0",
|
|
11
|
+
"react": "^19.2.5",
|
|
12
|
+
"tailwind-merge": "^3.5.0",
|
|
13
|
+
"@apptimate/core-lib": "*"
|
|
14
|
+
},
|
|
15
|
+
"publishConfig": {
|
|
16
|
+
"access": "public"
|
|
17
|
+
},
|
|
18
|
+
"peerDependencies": {
|
|
19
|
+
"next": "^16.0.0"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -14,7 +14,7 @@ export interface ButtonProps {
|
|
|
14
14
|
children?: React.ReactNode;
|
|
15
15
|
|
|
16
16
|
/** The visual style variant of the button */
|
|
17
|
-
variant?: 'solid' | 'bordered' | 'light' | 'flat' | 'shadow' | 'ghost';
|
|
17
|
+
variant?: 'solid' | 'bordered' | 'light' | 'flat' | 'shadow' | 'ghost' | 'soft';
|
|
18
18
|
|
|
19
19
|
/** The color scheme of the button */
|
|
20
20
|
color?: 'default' | 'primary' | 'secondary' | 'success' | 'warning' | 'danger';
|
|
@@ -73,7 +73,7 @@ const iconSizeMap = {
|
|
|
73
73
|
|
|
74
74
|
export const buttonVariants = cva(
|
|
75
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',
|
|
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 shrink-0 shadow-[0_1px_2px_rgba(0,0,0,0.02)]',
|
|
77
77
|
'focus-visible:outline-2 focus-visible:outline-[var(--aceui-color-focus)] focus-visible:outline-offset-2',
|
|
78
78
|
],
|
|
79
79
|
{
|
|
@@ -85,6 +85,7 @@ export const buttonVariants = cva(
|
|
|
85
85
|
flat: '',
|
|
86
86
|
shadow: '',
|
|
87
87
|
ghost: '',
|
|
88
|
+
soft: '',
|
|
88
89
|
},
|
|
89
90
|
color: {
|
|
90
91
|
default: '',
|
|
@@ -95,14 +96,14 @@ export const buttonVariants = cva(
|
|
|
95
96
|
danger: '',
|
|
96
97
|
},
|
|
97
98
|
size: {
|
|
98
|
-
small: 'text-[0.75rem] leading-[1rem] px-3
|
|
99
|
-
medium: 'text-[0.
|
|
100
|
-
large: 'text-[1rem] leading-[1.5rem] px-6
|
|
99
|
+
small: 'text-[0.75rem] leading-[1rem] px-3 h-8',
|
|
100
|
+
medium: 'text-[0.8125rem] leading-[1.25rem] px-4 h-10',
|
|
101
|
+
large: 'text-[1rem] leading-[1.5rem] px-6 h-12',
|
|
101
102
|
},
|
|
102
103
|
radius: {
|
|
103
104
|
none: 'rounded-none',
|
|
104
105
|
small: 'rounded-[8px]',
|
|
105
|
-
medium: 'rounded-[
|
|
106
|
+
medium: 'rounded-[10px]',
|
|
106
107
|
large: 'rounded-[14px]',
|
|
107
108
|
full: 'rounded-full',
|
|
108
109
|
squircle: 'rounded-full',
|
|
@@ -129,12 +130,12 @@ export const buttonVariants = cva(
|
|
|
129
130
|
{ variant: 'solid', color: 'warning', class: 'border-none bg-warning-500 text-warning-foreground hover:bg-warning-600 active:bg-warning-700' },
|
|
130
131
|
{ variant: 'solid', color: 'danger', class: 'border-none bg-danger-500 text-danger-foreground hover:bg-danger-600 active:bg-danger-700' },
|
|
131
132
|
/* Bordered */
|
|
132
|
-
{ variant: 'bordered', color: 'default', class: 'bg-transparent text-default-500 border
|
|
133
|
-
{ variant: 'bordered', color: 'primary', class: 'bg-transparent text-primary-500 border
|
|
134
|
-
{ variant: 'bordered', color: 'secondary', class: 'bg-transparent text-secondary-500 border
|
|
135
|
-
{ variant: 'bordered', color: 'success', class: 'bg-transparent text-success-500 border
|
|
136
|
-
{ variant: 'bordered', color: 'warning', class: 'bg-transparent text-warning-500 border
|
|
137
|
-
{ variant: 'bordered', color: 'danger', class: 'bg-transparent text-danger-500 border
|
|
133
|
+
{ variant: 'bordered', color: 'default', class: 'bg-transparent text-default-500 border border-default-500 hover:bg-default-50' },
|
|
134
|
+
{ variant: 'bordered', color: 'primary', class: 'bg-transparent text-primary-500 border border-primary-500 hover:bg-primary-50' },
|
|
135
|
+
{ variant: 'bordered', color: 'secondary', class: 'bg-transparent text-secondary-500 border border-secondary-500 hover:bg-secondary-50' },
|
|
136
|
+
{ variant: 'bordered', color: 'success', class: 'bg-transparent text-success-500 border border-success-500 hover:bg-success-50' },
|
|
137
|
+
{ variant: 'bordered', color: 'warning', class: 'bg-transparent text-warning-500 border border-warning-500 hover:bg-warning-50' },
|
|
138
|
+
{ variant: 'bordered', color: 'danger', class: 'bg-transparent text-danger-500 border border-danger-500 hover:bg-danger-50' },
|
|
138
139
|
/* Light */
|
|
139
140
|
{ variant: 'light', color: 'default', class: 'bg-default-50 text-default-500 border-none hover:bg-default-100' },
|
|
140
141
|
{ variant: 'light', color: 'primary', class: 'bg-primary-50 text-primary-500 border-none hover:bg-primary-100' },
|
|
@@ -149,6 +150,13 @@ export const buttonVariants = cva(
|
|
|
149
150
|
{ variant: 'flat', color: 'success', class: 'bg-transparent text-success-500 border-none hover:bg-success-50' },
|
|
150
151
|
{ variant: 'flat', color: 'warning', class: 'bg-transparent text-warning-500 border-none hover:bg-warning-50' },
|
|
151
152
|
{ variant: 'flat', color: 'danger', class: 'bg-transparent text-danger-500 border-none hover:bg-danger-50' },
|
|
153
|
+
/* Soft */
|
|
154
|
+
{ variant: 'soft', color: 'default', class: 'bg-gray-50 text-gray-700 border border-gray-100 hover:bg-gray-100 hover:border-gray-200' },
|
|
155
|
+
{ variant: 'soft', color: 'primary', class: 'bg-primary-50 text-primary-700 border border-primary-200 hover:bg-primary-100 hover:border-primary-300' },
|
|
156
|
+
{ variant: 'soft', color: 'secondary', class: 'bg-secondary-50 text-secondary-700 border border-secondary-200 hover:bg-secondary-100 hover:border-secondary-300' },
|
|
157
|
+
{ variant: 'soft', color: 'success', class: 'bg-success-50 text-success-700 border border-success-200 hover:bg-success-100 hover:border-success-300' },
|
|
158
|
+
{ variant: 'soft', color: 'warning', class: 'bg-warning-50 text-warning-700 border border-warning-200 hover:bg-warning-100 hover:border-warning-300' },
|
|
159
|
+
{ variant: 'soft', color: 'danger', class: 'bg-danger-50 text-danger-700 border border-danger-200 hover:bg-danger-100 hover:border-danger-300' },
|
|
152
160
|
/* Shadow */
|
|
153
161
|
{ variant: 'shadow', color: 'default', class: 'border-none bg-default-500 text-default-foreground hover:bg-default-600 active:bg-default-700' },
|
|
154
162
|
{ variant: 'shadow', color: 'primary', class: 'border-none bg-primary-500 text-primary-foreground hover:bg-primary-600 active:bg-primary-700' },
|
|
@@ -197,7 +205,7 @@ export const Button = ({
|
|
|
197
205
|
variant = 'solid',
|
|
198
206
|
color = 'primary',
|
|
199
207
|
size = 'medium',
|
|
200
|
-
radius = '
|
|
208
|
+
radius = 'full',
|
|
201
209
|
isDisabled = false,
|
|
202
210
|
isLoading = false,
|
|
203
211
|
icon,
|
|
@@ -136,7 +136,7 @@ export const ChipInput = ({
|
|
|
136
136
|
<div
|
|
137
137
|
className={cn(
|
|
138
138
|
'flex flex-wrap items-center gap-1.5 px-3 py-2 rounded-[10px] border bg-surface-0 transition-colors min-h-[42px] cursor-text',
|
|
139
|
-
error ? 'border-danger-500' : 'border-border-default focus-within:border-
|
|
139
|
+
error ? 'border-danger-500 hover:border-danger-500' : 'border-border-default hover:border-gray-300 focus-within:border-gray-300',
|
|
140
140
|
disabled && 'opacity-50 pointer-events-none'
|
|
141
141
|
)}
|
|
142
142
|
onClick={() => inputRef.current?.focus()}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { SlidersHorizontal } from 'lucide-react';
|
|
3
|
+
import { Dropdown } from './Dropdown';
|
|
4
|
+
import { Button } from './Button';
|
|
5
|
+
import { cn } from '@apptimate/core-lib';
|
|
6
|
+
|
|
7
|
+
export interface DesktopFilterPopoverProps {
|
|
8
|
+
/** Content to render inside the filter popover (typically Select components) */
|
|
9
|
+
children: React.ReactNode;
|
|
10
|
+
/** Custom label for the trigger button aria-label */
|
|
11
|
+
label?: string;
|
|
12
|
+
/** Additional class for outer wrapper */
|
|
13
|
+
className?: string;
|
|
14
|
+
/** Additional class for the trigger button */
|
|
15
|
+
triggerClassName?: string;
|
|
16
|
+
/** Whether any filter is active — shows a visual indicator dot */
|
|
17
|
+
hasActiveFilter?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const DesktopFilterPopover = ({
|
|
21
|
+
children,
|
|
22
|
+
label = "Filters",
|
|
23
|
+
className,
|
|
24
|
+
triggerClassName,
|
|
25
|
+
hasActiveFilter = false,
|
|
26
|
+
}: DesktopFilterPopoverProps) => {
|
|
27
|
+
return (
|
|
28
|
+
<div className={cn("hidden lg:block relative", className)}>
|
|
29
|
+
<Dropdown
|
|
30
|
+
align="right"
|
|
31
|
+
contentClassName="w-[260px] p-3 space-y-3"
|
|
32
|
+
trigger={
|
|
33
|
+
<div className="relative">
|
|
34
|
+
<Button
|
|
35
|
+
variant="soft"
|
|
36
|
+
color="default"
|
|
37
|
+
className={cn("!p-0 text-gray-500", triggerClassName)}
|
|
38
|
+
icon={<SlidersHorizontal size={18} strokeWidth={2} />}
|
|
39
|
+
ariaLabel={label}
|
|
40
|
+
/>
|
|
41
|
+
{hasActiveFilter && (
|
|
42
|
+
<span className="absolute -top-0.5 -right-0.5 w-2 h-2 bg-primary-500 rounded-full ring-2 ring-white" />
|
|
43
|
+
)}
|
|
44
|
+
</div>
|
|
45
|
+
}
|
|
46
|
+
>
|
|
47
|
+
<div onClick={(e) => e.stopPropagation()} className="space-y-3">
|
|
48
|
+
{children}
|
|
49
|
+
</div>
|
|
50
|
+
</Dropdown>
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
53
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Search } from 'lucide-react';
|
|
3
|
+
import { Dropdown } from './Dropdown';
|
|
4
|
+
import { Input } from './Input';
|
|
5
|
+
import { Button } from './Button';
|
|
6
|
+
import { cn } from '@apptimate/core-lib';
|
|
7
|
+
|
|
8
|
+
export interface DesktopSearchPopoverProps {
|
|
9
|
+
searchValue: string;
|
|
10
|
+
onSearchChange: (value: string) => void;
|
|
11
|
+
placeholder?: string;
|
|
12
|
+
className?: string;
|
|
13
|
+
triggerClassName?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const DesktopSearchPopover = ({
|
|
17
|
+
searchValue,
|
|
18
|
+
onSearchChange,
|
|
19
|
+
placeholder = "Search...",
|
|
20
|
+
className,
|
|
21
|
+
triggerClassName
|
|
22
|
+
}: DesktopSearchPopoverProps) => {
|
|
23
|
+
return (
|
|
24
|
+
<div className={cn("hidden lg:block", className)}>
|
|
25
|
+
<Dropdown
|
|
26
|
+
align="right"
|
|
27
|
+
contentClassName="w-[300px] p-2"
|
|
28
|
+
trigger={
|
|
29
|
+
<Button
|
|
30
|
+
variant="soft"
|
|
31
|
+
color="default"
|
|
32
|
+
className={cn("!p-0 text-gray-500", triggerClassName)}
|
|
33
|
+
icon={<Search size={18} strokeWidth={2} />}
|
|
34
|
+
ariaLabel="Search"
|
|
35
|
+
/>
|
|
36
|
+
}
|
|
37
|
+
>
|
|
38
|
+
<div className="p-1">
|
|
39
|
+
<Input
|
|
40
|
+
autoFocus
|
|
41
|
+
placeholder={placeholder}
|
|
42
|
+
icon={<Search size={16} className="text-gray-400" />}
|
|
43
|
+
className="[&_input]:rounded-[8px] [&_input]:border [&_input]:border-gray-200 [&_input]:bg-white [&_input]:hover:border-gray-300 [&_input]:focus:border-gray-300 w-full"
|
|
44
|
+
value={searchValue}
|
|
45
|
+
onChange={(e) => onSearchChange(e.target.value)}
|
|
46
|
+
/>
|
|
47
|
+
</div>
|
|
48
|
+
</Dropdown>
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
|
+
};
|
|
@@ -8,13 +8,14 @@ interface DropdownProps {
|
|
|
8
8
|
trigger: ReactNode;
|
|
9
9
|
children: ReactNode;
|
|
10
10
|
align?: 'left' | 'right';
|
|
11
|
+
valign?: 'top' | 'bottom';
|
|
11
12
|
className?: string;
|
|
12
13
|
contentClassName?: string;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
export function Dropdown({ trigger, children, align = 'left', className, contentClassName }: DropdownProps) {
|
|
16
|
+
export function Dropdown({ trigger, children, align = 'left', valign = 'bottom', className, contentClassName }: DropdownProps) {
|
|
16
17
|
const [isOpen, setIsOpen] = useState(false);
|
|
17
|
-
const [coords, setCoords] = useState({ top: 0, left: 0, right: 0 });
|
|
18
|
+
const [coords, setCoords] = useState({ top: 0, bottom: 0, left: 0, right: 0 });
|
|
18
19
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
|
19
20
|
const contentRef = useRef<HTMLDivElement>(null);
|
|
20
21
|
|
|
@@ -22,7 +23,8 @@ export function Dropdown({ trigger, children, align = 'left', className, content
|
|
|
22
23
|
if (dropdownRef.current) {
|
|
23
24
|
const rect = dropdownRef.current.getBoundingClientRect();
|
|
24
25
|
setCoords({
|
|
25
|
-
top: rect.
|
|
26
|
+
top: rect.top + window.scrollY,
|
|
27
|
+
bottom: rect.bottom + window.scrollY,
|
|
26
28
|
left: rect.left + window.scrollX,
|
|
27
29
|
right: window.innerWidth - rect.right - window.scrollX
|
|
28
30
|
});
|
|
@@ -86,11 +88,15 @@ export function Dropdown({ trigger, children, align = 'left', className, content
|
|
|
86
88
|
ref={contentRef}
|
|
87
89
|
style={{
|
|
88
90
|
position: 'absolute',
|
|
89
|
-
|
|
91
|
+
...(valign === 'bottom'
|
|
92
|
+
? { top: `${coords.bottom}px` }
|
|
93
|
+
: { bottom: `${window.innerHeight - coords.top}px` }
|
|
94
|
+
),
|
|
90
95
|
...(align === 'left' ? { left: `${coords.left}px` } : { right: `${coords.right}px` })
|
|
91
96
|
}}
|
|
92
97
|
className={cn(
|
|
93
|
-
"z-[9999]
|
|
98
|
+
"z-[9999] min-w-[160px] rounded-[10px] bg-white border border-gray-200 shadow-[0_8px_30px_rgb(0,0,0,0.12)] p-1.5 animate-in fade-in zoom-in-95 duration-200",
|
|
99
|
+
valign === 'bottom' ? "mt-1.5" : "mb-1.5",
|
|
94
100
|
contentClassName
|
|
95
101
|
)}
|
|
96
102
|
onClick={closeDropdown}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import React, { useState } from 'react';
|
|
4
4
|
import { cn } from '@apptimate/core-lib';
|
|
5
5
|
import { Eye, EyeOff } from 'lucide-react';
|
|
6
|
+
import { Label } from './Label';
|
|
6
7
|
|
|
7
8
|
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
|
8
9
|
label?: string;
|
|
@@ -20,13 +21,13 @@ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|
|
20
21
|
return (
|
|
21
22
|
<div className={cn("flex flex-col gap-1.5 w-full", className)}>
|
|
22
23
|
{label && (
|
|
23
|
-
<
|
|
24
|
-
{label}
|
|
25
|
-
</
|
|
24
|
+
<Label isRequired={isRequired}>
|
|
25
|
+
{label}
|
|
26
|
+
</Label>
|
|
26
27
|
)}
|
|
27
28
|
<div className="relative group">
|
|
28
29
|
{icon && (
|
|
29
|
-
<div className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-foreground-disabled group-focus-within:text-
|
|
30
|
+
<div className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-foreground-disabled group-focus-within:text-[#2D3142] transition-colors">
|
|
30
31
|
{icon}
|
|
31
32
|
</div>
|
|
32
33
|
)}
|
|
@@ -34,10 +35,10 @@ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|
|
34
35
|
ref={ref}
|
|
35
36
|
type={inputType}
|
|
36
37
|
className={cn(
|
|
37
|
-
"w-full bg-surface-0 border-1.
|
|
38
|
+
"w-full bg-surface-0 border-[1.5px] border-border-subtle rounded-[10px] px-3.5 py-2.5 text-[13.5px] text-foreground-1 outline-none transition-all hover:border-gray-300 focus:border-gray-300 focus:bg-surface-1 placeholder:text-foreground-disabled",
|
|
38
39
|
icon && "pl-10",
|
|
39
40
|
isPassword && "pr-10",
|
|
40
|
-
error && "border-danger-alt focus:border-danger-alt"
|
|
41
|
+
error && "border-danger-alt focus:border-danger-alt hover:border-danger-alt"
|
|
41
42
|
)}
|
|
42
43
|
{...props}
|
|
43
44
|
/>
|
|
@@ -45,7 +46,7 @@ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|
|
45
46
|
<button
|
|
46
47
|
type="button"
|
|
47
48
|
onClick={() => setShowPassword(!showPassword)}
|
|
48
|
-
className="absolute right-3 top-1/2 -translate-y-1/2 text-foreground-disabled hover:text-
|
|
49
|
+
className="absolute right-3 top-1/2 -translate-y-1/2 text-foreground-disabled hover:text-[#2D3142] transition-colors"
|
|
49
50
|
>
|
|
50
51
|
{showPassword ? <EyeOff size={16} /> : <Eye size={16} />}
|
|
51
52
|
</button>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { cn } from '@apptimate/core-lib';
|
|
5
|
+
|
|
6
|
+
export interface LabelProps extends React.LabelHTMLAttributes<HTMLLabelElement> {
|
|
7
|
+
isRequired?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const Label = React.forwardRef<HTMLLabelElement, LabelProps>(
|
|
11
|
+
({ children, className, isRequired, ...props }, ref) => {
|
|
12
|
+
return (
|
|
13
|
+
<label
|
|
14
|
+
ref={ref}
|
|
15
|
+
className={cn("text-[11px] font-normal text-gray-500 tracking-normal apptimate-input-label", className)}
|
|
16
|
+
{...props}
|
|
17
|
+
>
|
|
18
|
+
{children} {isRequired && <span className="text-danger-alt ml-0.5">*</span>}
|
|
19
|
+
</label>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
Label.displayName = 'Label';
|
|
@@ -14,9 +14,10 @@ export interface ModalProps {
|
|
|
14
14
|
footer?: React.ReactNode;
|
|
15
15
|
backdrop?: 'transparent' | 'opaque' | 'blur';
|
|
16
16
|
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'full';
|
|
17
|
+
position?: 'center' | 'bottom';
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
export const Modal = ({ isOpen, onClose, title, children, className, footer, backdrop = 'opaque', size = 'sm' }: ModalProps) => {
|
|
20
|
+
export const Modal = ({ isOpen, onClose, title, children, className, footer, backdrop = 'opaque', size = 'sm', position = 'center' }: ModalProps) => {
|
|
20
21
|
useEffect(() => {
|
|
21
22
|
if (isOpen) {
|
|
22
23
|
document.body.style.overflow = 'hidden';
|
|
@@ -46,7 +47,11 @@ export const Modal = ({ isOpen, onClose, title, children, className, footer, bac
|
|
|
46
47
|
return (
|
|
47
48
|
<AnimatePresence>
|
|
48
49
|
{isOpen && (
|
|
49
|
-
<div className={cn(
|
|
50
|
+
<div className={cn(
|
|
51
|
+
"fixed inset-0 z-50 flex",
|
|
52
|
+
position === 'center' ? "items-center justify-center p-2 sm:p-4" : "items-end justify-center sm:items-center p-0 sm:p-4",
|
|
53
|
+
size === 'full' ? 'p-0 sm:p-0' : ''
|
|
54
|
+
)}>
|
|
50
55
|
<motion.div
|
|
51
56
|
initial={{ opacity: 0 }}
|
|
52
57
|
animate={{ opacity: 1 }}
|
|
@@ -55,12 +60,13 @@ export const Modal = ({ isOpen, onClose, title, children, className, footer, bac
|
|
|
55
60
|
className={backdropClasses}
|
|
56
61
|
/>
|
|
57
62
|
<motion.div
|
|
58
|
-
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
|
59
|
-
animate={{ opacity: 1, scale: 1, y: 0 }}
|
|
60
|
-
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
|
63
|
+
initial={position === 'bottom' ? { opacity: 0, y: "100%" } : { opacity: 0, scale: 0.95, y: 20 }}
|
|
64
|
+
animate={position === 'bottom' ? { opacity: 1, y: 0 } : { opacity: 1, scale: 1, y: 0 }}
|
|
65
|
+
exit={position === 'bottom' ? { opacity: 0, y: "100%" } : { opacity: 0, scale: 0.95, y: 20 }}
|
|
66
|
+
transition={{ type: "spring", damping: 25, stiffness: 300 }}
|
|
61
67
|
className={cn(
|
|
62
68
|
"relative w-full bg-surface-1 shadow-[0_8px_40px_rgba(0,0,0,0.2)] flex flex-col overflow-hidden",
|
|
63
|
-
size === 'full' ? "rounded-none h-screen max-h-screen" : "rounded-[16px] sm:rounded-[24px] max-h-[90vh]",
|
|
69
|
+
size === 'full' ? "rounded-none h-screen max-h-screen" : position === 'bottom' ? "rounded-t-[24px] sm:rounded-[24px] max-h-[90vh]" : "rounded-[16px] sm:rounded-[24px] max-h-[90vh]",
|
|
64
70
|
sizeClasses[size],
|
|
65
71
|
className
|
|
66
72
|
)}
|
|
@@ -86,7 +92,7 @@ export const Modal = ({ isOpen, onClose, title, children, className, footer, bac
|
|
|
86
92
|
|
|
87
93
|
{/* Footer */}
|
|
88
94
|
{footer && (
|
|
89
|
-
<div className="px-5 sm:px-8
|
|
95
|
+
<div className="px-5 sm:px-8 pb-5 sm:pb-7 flex flex-col-reverse sm:flex-row justify-end gap-2 sm:gap-3">
|
|
90
96
|
{footer}
|
|
91
97
|
</div>
|
|
92
98
|
)}
|
|
@@ -96,3 +102,11 @@ export const Modal = ({ isOpen, onClose, title, children, className, footer, bac
|
|
|
96
102
|
</AnimatePresence>
|
|
97
103
|
);
|
|
98
104
|
};
|
|
105
|
+
|
|
106
|
+
export const ModalFooter = ({ children, className }: { children: React.ReactNode; className?: string }) => {
|
|
107
|
+
return (
|
|
108
|
+
<div className={cn("flex flex-col-reverse sm:flex-row justify-end gap-2 sm:gap-3 pt-4 sm:pt-6", className)}>
|
|
109
|
+
{children}
|
|
110
|
+
</div>
|
|
111
|
+
);
|
|
112
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { cn } from '@apptimate/core-lib';
|
|
5
|
+
|
|
6
|
+
export interface RecordCountProps {
|
|
7
|
+
total: number;
|
|
8
|
+
label?: string;
|
|
9
|
+
className?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const RecordCount = ({ total, label = "records", className }: RecordCountProps) => {
|
|
13
|
+
return (
|
|
14
|
+
<div className={cn("flex flex-col justify-center items-center px-4 min-w-[60px] h-10 bg-gray-50 border border-gray-100 rounded-full shadow-[0_1px_2px_rgba(0,0,0,0.02)]", className)}>
|
|
15
|
+
<span className="text-[14px] font-bold text-[#2D3142] leading-none">{total}</span>
|
|
16
|
+
<span className="text-[6px] font-bold text-gray-400 uppercase tracking-widest mt-1 leading-none">{label}</span>
|
|
17
|
+
</div>
|
|
18
|
+
);
|
|
19
|
+
};
|
|
@@ -4,6 +4,7 @@ import React, { useState, useEffect, useRef, useCallback } from 'react';
|
|
|
4
4
|
import ReactDOM from 'react-dom';
|
|
5
5
|
import { cn } from '@apptimate/core-lib';
|
|
6
6
|
import { ChevronDown, X, Search, Loader2 } from 'lucide-react';
|
|
7
|
+
import { Label } from './Label';
|
|
7
8
|
|
|
8
9
|
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
9
10
|
|
|
@@ -493,7 +494,7 @@ function SelectCore<T extends Record<string, unknown>>({
|
|
|
493
494
|
aria-label="Search options"
|
|
494
495
|
/>
|
|
495
496
|
{isAsync && isSearching && (
|
|
496
|
-
<Loader2 size={15} className="text-
|
|
497
|
+
<Loader2 size={15} className="text-[#2D3142] animate-spin shrink-0" />
|
|
497
498
|
)}
|
|
498
499
|
</div>
|
|
499
500
|
)}
|
|
@@ -544,7 +545,7 @@ function SelectCore<T extends Record<string, unknown>>({
|
|
|
544
545
|
{multiple && (
|
|
545
546
|
<span
|
|
546
547
|
className={cn(
|
|
547
|
-
'w-4 h-4 rounded border-1.
|
|
548
|
+
'w-4 h-4 rounded border-[1.5px] flex items-center justify-center shrink-0 transition-colors',
|
|
548
549
|
isSelected(item)
|
|
549
550
|
? 'bg-primary border-primary'
|
|
550
551
|
: 'border-border-subtle bg-surface-0'
|
|
@@ -570,7 +571,7 @@ function SelectCore<T extends Record<string, unknown>>({
|
|
|
570
571
|
{/* Async pagination loader */}
|
|
571
572
|
{isAsync && isOptionLoading && (
|
|
572
573
|
<li className="flex items-center justify-center py-3">
|
|
573
|
-
<Loader2 size={18} className="text-
|
|
574
|
+
<Loader2 size={18} className="text-[#2D3142] animate-spin" />
|
|
574
575
|
</li>
|
|
575
576
|
)}
|
|
576
577
|
</ul>
|
|
@@ -592,10 +593,9 @@ function SelectCore<T extends Record<string, unknown>>({
|
|
|
592
593
|
<div className={cn('flex flex-col gap-1.5 w-full', className)}>
|
|
593
594
|
{/* Label */}
|
|
594
595
|
{label && (
|
|
595
|
-
<
|
|
596
|
+
<Label isRequired={isRequired}>
|
|
596
597
|
{label}
|
|
597
|
-
|
|
598
|
-
</label>
|
|
598
|
+
</Label>
|
|
599
599
|
)}
|
|
600
600
|
|
|
601
601
|
{/* Select container */}
|
|
@@ -615,10 +615,10 @@ function SelectCore<T extends Record<string, unknown>>({
|
|
|
615
615
|
<div
|
|
616
616
|
ref={controlRef}
|
|
617
617
|
className={cn(
|
|
618
|
-
'w-full border-1.
|
|
618
|
+
'w-full border-[1.5px] rounded-[10px] px-3.5 py-2.5 flex items-center gap-2 cursor-pointer transition-all min-h-[42px]',
|
|
619
619
|
isOpen
|
|
620
|
-
? `border-
|
|
621
|
-
: `border-border-subtle bg-surface-${surface}`,
|
|
620
|
+
? `border-gray-300 bg-surface-${surface === 0 ? 1 : 2}`
|
|
621
|
+
: `border-border-subtle bg-surface-${surface} hover:border-gray-300`,
|
|
622
622
|
error && 'border-danger-alt focus:border-danger-alt'
|
|
623
623
|
)}
|
|
624
624
|
>
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import React from 'react';
|
|
4
4
|
import { cn } from '@apptimate/core-lib';
|
|
5
5
|
import { ChevronDown } from 'lucide-react';
|
|
6
|
+
import { Label } from './Label';
|
|
6
7
|
|
|
7
8
|
export interface SelectProps extends React.SelectHTMLAttributes<HTMLSelectElement> {
|
|
8
9
|
label?: string;
|
|
@@ -16,16 +17,16 @@ export const Select = React.forwardRef<HTMLSelectElement, SelectProps>(
|
|
|
16
17
|
return (
|
|
17
18
|
<div className={cn("flex flex-col gap-1.5 w-full", className)}>
|
|
18
19
|
{label && (
|
|
19
|
-
<
|
|
20
|
-
{label}
|
|
21
|
-
</
|
|
20
|
+
<Label isRequired={isRequired}>
|
|
21
|
+
{label}
|
|
22
|
+
</Label>
|
|
22
23
|
)}
|
|
23
24
|
<div className="relative group">
|
|
24
25
|
<select
|
|
25
26
|
ref={ref}
|
|
26
27
|
className={cn(
|
|
27
|
-
"w-full bg-surface-0 border-1.
|
|
28
|
-
error && "border-danger-alt focus:border-danger-alt"
|
|
28
|
+
"w-full bg-surface-0 border-[1.5px] border-border-subtle rounded-[10px] px-3.5 py-2.5 text-[13.5px] text-foreground-1 outline-none transition-all hover:border-gray-300 focus:border-gray-300 focus:bg-surface-1 appearance-none cursor-pointer",
|
|
29
|
+
error && "border-danger-alt focus:border-danger-alt hover:border-danger-alt"
|
|
29
30
|
)}
|
|
30
31
|
{...props}
|
|
31
32
|
>
|
|
@@ -36,7 +37,7 @@ export const Select = React.forwardRef<HTMLSelectElement, SelectProps>(
|
|
|
36
37
|
</option>
|
|
37
38
|
))}
|
|
38
39
|
</select>
|
|
39
|
-
<div className="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none text-foreground-disabled group-focus-within:text-
|
|
40
|
+
<div className="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none text-foreground-disabled group-focus-within:text-[#2D3142] transition-colors">
|
|
40
41
|
<ChevronDown size={16} />
|
|
41
42
|
</div>
|
|
42
43
|
</div>
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { cn } from '@apptimate/core-lib';
|
|
5
|
+
|
|
6
|
+
export interface TableProps {
|
|
7
|
+
children?: React.ReactNode;
|
|
8
|
+
className?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const Table = ({ children, className }: TableProps) => {
|
|
12
|
+
return (
|
|
13
|
+
<div className={cn("w-full", className)}>
|
|
14
|
+
<table className="w-full text-left lg:min-w-[600px] border-separate border-spacing-0 block lg:table">
|
|
15
|
+
{children}
|
|
16
|
+
</table>
|
|
17
|
+
</div>
|
|
18
|
+
);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const THeader = ({ children, className }: TableProps) => (
|
|
22
|
+
<thead className={cn("hidden lg:table-header-group", className)}>
|
|
23
|
+
{children}
|
|
24
|
+
</thead>
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
export const TBody = ({ children, className }: TableProps) => (
|
|
28
|
+
<tbody className={cn("block lg:table-row-group", className)}>
|
|
29
|
+
{children}
|
|
30
|
+
</tbody>
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
export const TRow = ({ children, className, onClick }: TableProps & { onClick?: () => void }) => (
|
|
34
|
+
<tr
|
|
35
|
+
className={cn(
|
|
36
|
+
"group transition-colors duration-200 animate-fade-in",
|
|
37
|
+
"block lg:table-row",
|
|
38
|
+
"bg-white max-lg:shadow-sm max-lg:border max-lg:border-gray-100 max-lg:rounded-[10px] max-lg:overflow-hidden max-lg:mb-3", // Reduced mobile row gap
|
|
39
|
+
onClick && "cursor-pointer hover:bg-gray-50",
|
|
40
|
+
className
|
|
41
|
+
)}
|
|
42
|
+
onClick={onClick}
|
|
43
|
+
>
|
|
44
|
+
{children}
|
|
45
|
+
</tr>
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
export const TCell = ({ children, className, isHeader = false, colSpan, rowSpan, label }: TableProps & { isHeader?: boolean; colSpan?: number; rowSpan?: number; label?: string }) => {
|
|
49
|
+
const Tag = isHeader ? 'th' : 'td';
|
|
50
|
+
return (
|
|
51
|
+
<Tag
|
|
52
|
+
colSpan={colSpan}
|
|
53
|
+
rowSpan={rowSpan}
|
|
54
|
+
className={cn(
|
|
55
|
+
"transition-all",
|
|
56
|
+
"block lg:table-cell",
|
|
57
|
+
isHeader
|
|
58
|
+
? "py-2.5 px-3 lg:px-4 text-[12px] font-bold text-gray-400 capitalize tracking-wide bg-[#F8F9FA] first:rounded-l-[8px] last:rounded-r-[8px] border-b-0"
|
|
59
|
+
: "py-1.5 px-3 lg:px-4 lg:py-2 text-[13px] font-bold text-[#2D3142]", // Highly compact row padding
|
|
60
|
+
!isHeader && "flex justify-between items-center lg:table-cell border-b border-gray-100 last:border-b-0 lg:last:border-b", // Adds separator line across rows on desktop
|
|
61
|
+
className
|
|
62
|
+
)}>
|
|
63
|
+
{label && <span className="lg:hidden text-[11px] font-bold text-gray-400 uppercase tracking-wider">{label}</span>}
|
|
64
|
+
<div className="flex justify-end lg:block w-full lg:w-auto">{children}</div>
|
|
65
|
+
</Tag>
|
|
66
|
+
);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export const TLoader = ({ rows = 8, cols = 4 }: { rows?: number; cols?: number }) => {
|
|
70
|
+
return (
|
|
71
|
+
<>
|
|
72
|
+
{Array.from({ length: rows }).map((_, i) => (
|
|
73
|
+
<TRow key={i}>
|
|
74
|
+
{Array.from({ length: cols }).map((_, j) => (
|
|
75
|
+
<TCell key={j}>
|
|
76
|
+
<div className="h-4 bg-border-default animate-pulse rounded w-full" />
|
|
77
|
+
</TCell>
|
|
78
|
+
))}
|
|
79
|
+
</TRow>
|
|
80
|
+
))}
|
|
81
|
+
</>
|
|
82
|
+
);
|
|
83
|
+
};
|