@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.
Files changed (29) hide show
  1. package/CHANGELOG.md +7 -7
  2. package/package.json +21 -18
  3. package/src/{Button.tsx → base-components/Button.tsx} +21 -13
  4. package/src/{ChipInput.tsx → base-components/ChipInput.tsx} +1 -1
  5. package/src/base-components/DesktopFilterPopover.tsx +53 -0
  6. package/src/base-components/DesktopSearchPopover.tsx +51 -0
  7. package/src/{Dropdown.tsx → base-components/Dropdown.tsx} +11 -5
  8. package/src/{Input.tsx → base-components/Input.tsx} +8 -7
  9. package/src/base-components/Label.tsx +24 -0
  10. package/src/{Modal.tsx → base-components/Modal.tsx} +21 -7
  11. package/src/base-components/RecordCount.tsx +19 -0
  12. package/src/{SearchableSelect.tsx → base-components/SearchableSelect.tsx} +9 -9
  13. package/src/{Select.tsx → base-components/Select.tsx} +7 -6
  14. package/src/base-components/Table.tsx +83 -0
  15. package/src/base-components/TableMobileOptions.tsx +51 -0
  16. package/src/common-components/Charts.tsx +222 -0
  17. package/src/common-components/DashboardLayout.tsx +462 -0
  18. package/src/index.tsx +25 -18
  19. package/src/Table.tsx +0 -78
  20. /package/src/{Accordion.tsx → base-components/Accordion.tsx} +0 -0
  21. /package/src/{Badge.tsx → base-components/Badge.tsx} +0 -0
  22. /package/src/{ButtonGroup.tsx → base-components/ButtonGroup.tsx} +0 -0
  23. /package/src/{Card.tsx → base-components/Card.tsx} +0 -0
  24. /package/src/{Checkbox.tsx → base-components/Checkbox.tsx} +0 -0
  25. /package/src/{Divider.tsx → base-components/Divider.tsx} +0 -0
  26. /package/src/{EmptyState.tsx → base-components/EmptyState.tsx} +0 -0
  27. /package/src/{HintIcon.tsx → base-components/HintIcon.tsx} +0 -0
  28. /package/src/{Pagination.tsx → base-components/Pagination.tsx} +0 -0
  29. /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.0.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": "latest",
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 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',
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-[12px]',
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-1 border-default-500 hover:bg-default-50' },
133
- { variant: 'bordered', color: 'primary', class: 'bg-transparent text-primary-500 border-1 border-primary-500 hover:bg-primary-50' },
134
- { variant: 'bordered', color: 'secondary', class: 'bg-transparent text-secondary-500 border-1 border-secondary-500 hover:bg-secondary-50' },
135
- { variant: 'bordered', color: 'success', class: 'bg-transparent text-success-500 border-1 border-success-500 hover:bg-success-50' },
136
- { variant: 'bordered', color: 'warning', class: 'bg-transparent text-warning-500 border-1 border-warning-500 hover:bg-warning-50' },
137
- { variant: 'bordered', color: 'danger', class: 'bg-transparent text-danger-500 border-1 border-danger-500 hover:bg-danger-50' },
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 = 'squircle',
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-primary-500',
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.bottom + window.scrollY,
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
- top: `${coords.top}px`,
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] mt-1.5 min-w-[160px] rounded-[10px] bg-surface-0 border border-border-default shadow-[0_4px_12px_rgba(0,0,0,0.05)] p-1.5 animate-in fade-in zoom-in-95 duration-200",
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
- <label className="text-[12px] font-semibold text-foreground-subtle tracking-[0.3px] uppercase">
24
- {label} {isRequired && <span className="text-danger-alt ml-0.5">*</span>}
25
- </label>
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-primary transition-colors">
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.5 border-border-subtle rounded-[10px] px-3.5 py-2.5 text-[13.5px] text-foreground-1 outline-none transition-all focus:border-primary focus:bg-surface-1 placeholder:text-foreground-disabled",
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-primary transition-colors"
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("fixed inset-0 z-50 flex items-center justify-center", size === 'full' ? 'p-0' : 'p-2 sm:p-4')}>
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 py-4 sm:py-6 bg-surface-subtle border-t border-surface-0 flex flex-col-reverse sm:flex-row justify-end gap-2 sm:gap-3">
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-primary animate-spin shrink-0" />
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.5 flex items-center justify-center shrink-0 transition-colors',
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-primary animate-spin" />
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
- <label className="text-[12px] font-semibold text-foreground-subtle tracking-[0.3px] uppercase">
596
+ <Label isRequired={isRequired}>
596
597
  {label}
597
- {isRequired && <span className="text-danger-alt ml-0.5">*</span>}
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.5 rounded-[10px] px-3.5 py-2.5 flex items-center gap-2 cursor-pointer transition-all min-h-[42px]',
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-primary bg-surface-${surface === 0 ? 1 : 2}`
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
- <label className="text-[12px] font-semibold text-foreground-subtle tracking-[0.3px] uppercase">
20
- {label} {isRequired && <span className="text-danger-alt ml-0.5">*</span>}
21
- </label>
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.5 border-border-subtle rounded-[10px] px-3.5 py-2.5 text-[13.5px] text-foreground-1 outline-none transition-all focus:border-primary focus:bg-surface-1 appearance-none cursor-pointer",
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-primary transition-colors">
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
+ };