@apptimate/ui 1.0.0 → 1.2.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 (42) hide show
  1. package/CHANGELOG.md +7 -7
  2. package/package.json +22 -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/DateLabel.tsx +68 -0
  6. package/src/base-components/DateRangePicker.tsx +392 -0
  7. package/src/base-components/DesktopFilterPopover.tsx +53 -0
  8. package/src/base-components/DesktopSearchPopover.tsx +51 -0
  9. package/src/{Dropdown.tsx → base-components/Dropdown.tsx} +11 -5
  10. package/src/base-components/ImageDropzone.tsx +80 -0
  11. package/src/{Input.tsx → base-components/Input.tsx} +8 -7
  12. package/src/base-components/Label.tsx +24 -0
  13. package/src/{Modal.tsx → base-components/Modal.tsx} +36 -10
  14. package/src/{PopConfirm.tsx → base-components/PopConfirm.tsx} +15 -4
  15. package/src/base-components/RecordCount.tsx +19 -0
  16. package/src/{SearchableSelect.tsx → base-components/SearchableSelect.tsx} +9 -9
  17. package/src/{Select.tsx → base-components/Select.tsx} +7 -6
  18. package/src/base-components/Table.tsx +83 -0
  19. package/src/base-components/TableMobileOptions.tsx +51 -0
  20. package/src/common-components/DashboardLayout.tsx +473 -0
  21. package/src/common-components/charts/ApexCharts.tsx +222 -0
  22. package/src/common-components/charts/RBarChart.tsx +145 -0
  23. package/src/common-components/charts/RLineChart.tsx +116 -0
  24. package/src/common-components/charts/index.ts +3 -0
  25. package/src/common-components/charts/palette.ts +10 -0
  26. package/src/components/shared/CustomerSelectorComponent.tsx +163 -0
  27. package/src/components/shared/ImageUploadComponent.tsx +228 -0
  28. package/src/components/shared/PaymentModeComponent.tsx +421 -0
  29. package/src/components/shared/ProcurementManagerSelector.tsx +5 -0
  30. package/src/components/shared/ProductSelectorComponent.tsx +166 -0
  31. package/src/components/shared/SupplierSelectorComponent.tsx +5 -0
  32. package/src/index.tsx +30 -18
  33. package/src/Table.tsx +0 -78
  34. /package/src/{Accordion.tsx → base-components/Accordion.tsx} +0 -0
  35. /package/src/{Badge.tsx → base-components/Badge.tsx} +0 -0
  36. /package/src/{ButtonGroup.tsx → base-components/ButtonGroup.tsx} +0 -0
  37. /package/src/{Card.tsx → base-components/Card.tsx} +0 -0
  38. /package/src/{Checkbox.tsx → base-components/Checkbox.tsx} +0 -0
  39. /package/src/{Divider.tsx → base-components/Divider.tsx} +0 -0
  40. /package/src/{EmptyState.tsx → base-components/EmptyState.tsx} +0 -0
  41. /package/src/{HintIcon.tsx → base-components/HintIcon.tsx} +0 -0
  42. /package/src/{Pagination.tsx → base-components/Pagination.tsx} +0 -0
@@ -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}
@@ -0,0 +1,80 @@
1
+ "use client";
2
+
3
+ import React, { useRef } from "react";
4
+ import { Upload } from "lucide-react";
5
+
6
+ export interface ImageDropzoneProps {
7
+ onDrop: (files: File[]) => void;
8
+ maxSizeMB?: number;
9
+ maxFiles?: number;
10
+ accept?: string;
11
+ className?: string;
12
+ title?: string;
13
+ subtitle?: string;
14
+ isLoading?: boolean;
15
+ }
16
+
17
+ export function ImageDropzone({
18
+ onDrop,
19
+ maxSizeMB = 5,
20
+ maxFiles = 10,
21
+ accept = "image/*",
22
+ className = "",
23
+ title = "Drop images here or click to upload",
24
+ subtitle = "JPEG, PNG, WebP — max 5MB each, up to 10 images",
25
+ isLoading = false,
26
+ }: ImageDropzoneProps) {
27
+ const fileInputRef = useRef<HTMLInputElement>(null);
28
+
29
+ const handleFileDrop = (e: React.DragEvent) => {
30
+ e.preventDefault();
31
+ if (isLoading) return;
32
+ const files = Array.from(e.dataTransfer.files).filter((f) => f.type.startsWith("image/"));
33
+ if (files.length) {
34
+ onDrop(files.slice(0, maxFiles));
35
+ }
36
+ };
37
+
38
+ const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
39
+ if (isLoading) return;
40
+ const files = Array.from(e.target.files || []);
41
+ if (files.length) {
42
+ onDrop(files.slice(0, maxFiles));
43
+ }
44
+ if (fileInputRef.current) {
45
+ fileInputRef.current.value = "";
46
+ }
47
+ };
48
+
49
+ return (
50
+ <div
51
+ onDragOver={(e) => e.preventDefault()}
52
+ onDrop={handleFileDrop}
53
+ onClick={() => fileInputRef.current?.click()}
54
+ className={`border-2 border-dashed border-gray-200 hover:border-primary-300 rounded-xl p-6 flex flex-col items-center justify-center cursor-pointer transition-all hover:bg-primary-50/30 group ${
55
+ isLoading ? "opacity-50 cursor-not-allowed" : ""
56
+ } ${className}`}
57
+ >
58
+ <div className="w-10 h-10 rounded-full bg-gray-100 group-hover:bg-primary-100 flex items-center justify-center mb-2 transition-colors">
59
+ {isLoading ? (
60
+ <div className="w-5 h-5 rounded-full border-2 border-gray-200 border-t-primary-500 animate-spin" />
61
+ ) : (
62
+ <Upload size={18} className="text-gray-400 group-hover:text-primary-500" />
63
+ )}
64
+ </div>
65
+ <p className="text-[13px] font-medium text-gray-500 group-hover:text-primary-600">
66
+ {isLoading ? "Uploading..." : title}
67
+ </p>
68
+ <p className="text-[11px] text-gray-400 mt-0.5">{subtitle}</p>
69
+ <input
70
+ ref={fileInputRef}
71
+ type="file"
72
+ accept={accept}
73
+ multiple={maxFiles > 1}
74
+ className="hidden"
75
+ onChange={handleFileSelect}
76
+ disabled={isLoading}
77
+ />
78
+ </div>
79
+ );
80
+ }
@@ -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';
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
 
3
- import React, { useEffect } from 'react';
3
+ import React, { useEffect, useState } from 'react';
4
+ import { createPortal } from 'react-dom';
4
5
  import { motion, AnimatePresence } from 'framer-motion';
5
6
  import { X } from 'lucide-react';
6
7
  import { cn } from '@apptimate/core-lib';
@@ -14,9 +15,16 @@ export interface ModalProps {
14
15
  footer?: React.ReactNode;
15
16
  backdrop?: 'transparent' | 'opaque' | 'blur';
16
17
  size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'full';
18
+ position?: 'center' | 'bottom';
19
+ /** Override z-index layer (default 50). Increase for nested modals. */
20
+ zIndex?: number;
17
21
  }
18
22
 
19
- export const Modal = ({ isOpen, onClose, title, children, className, footer, backdrop = 'opaque', size = 'sm' }: ModalProps) => {
23
+ export const Modal = ({ isOpen, onClose, title, children, className, footer, backdrop = 'opaque', size = 'sm', position = 'center', zIndex }: ModalProps) => {
24
+ // SSR-safe portal mount guard — document is only available on the client
25
+ const [mounted, setMounted] = useState(false);
26
+ useEffect(() => { setMounted(true); }, []);
27
+
20
28
  useEffect(() => {
21
29
  if (isOpen) {
22
30
  document.body.style.overflow = 'hidden';
@@ -43,10 +51,14 @@ export const Modal = ({ isOpen, onClose, title, children, className, footer, bac
43
51
  full: 'max-w-[100vw]',
44
52
  };
45
53
 
46
- return (
54
+ const content = (
47
55
  <AnimatePresence>
48
56
  {isOpen && (
49
- <div className={cn("fixed inset-0 z-50 flex items-center justify-center", size === 'full' ? 'p-0' : 'p-2 sm:p-4')}>
57
+ <div className={cn(
58
+ "fixed inset-0 flex",
59
+ position === 'center' ? "items-center justify-center p-2 sm:p-4" : "items-end justify-center sm:items-center p-0 sm:p-4",
60
+ size === 'full' ? 'p-0 sm:p-0' : ''
61
+ )} style={{ zIndex: zIndex ?? 50 }}>
50
62
  <motion.div
51
63
  initial={{ opacity: 0 }}
52
64
  animate={{ opacity: 1 }}
@@ -55,19 +67,20 @@ export const Modal = ({ isOpen, onClose, title, children, className, footer, bac
55
67
  className={backdropClasses}
56
68
  />
57
69
  <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 }}
70
+ initial={position === 'bottom' ? { opacity: 0, y: "100%" } : { opacity: 0, scale: 0.95, y: 20 }}
71
+ animate={position === 'bottom' ? { opacity: 1, y: 0 } : { opacity: 1, scale: 1, y: 0 }}
72
+ exit={position === 'bottom' ? { opacity: 0, y: "100%" } : { opacity: 0, scale: 0.95, y: 20 }}
73
+ transition={{ type: "spring", damping: 25, stiffness: 300 }}
61
74
  className={cn(
62
75
  "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]",
76
+ 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
77
  sizeClasses[size],
65
78
  className
66
79
  )}
67
80
  >
68
81
  {/* Header */}
69
82
  <div className="flex items-center justify-between px-5 sm:px-8 py-4 border-b border-surface-0">
70
- <h3 className="text-[16px] sm:text-[18px] font-bold text-foreground-0 tracking-tight">{title}</h3>
83
+ <h3 className="text-[16px] sm:text-[18px] font-bold text-foreground-0 tracking-tight flex-1 min-w-0">{title}</h3>
71
84
  <button
72
85
  onClick={onClose}
73
86
  className="w-8 h-8 rounded-full flex items-center justify-center text-foreground-disabled hover:bg-surface-0 hover:text-foreground-0 transition-all"
@@ -86,7 +99,7 @@ export const Modal = ({ isOpen, onClose, title, children, className, footer, bac
86
99
 
87
100
  {/* Footer */}
88
101
  {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">
102
+ <div className="px-5 sm:px-8 pb-5 flex flex-col-reverse sm:flex-row justify-end gap-2 sm:gap-3">
90
103
  {footer}
91
104
  </div>
92
105
  )}
@@ -95,4 +108,17 @@ export const Modal = ({ isOpen, onClose, title, children, className, footer, bac
95
108
  )}
96
109
  </AnimatePresence>
97
110
  );
111
+
112
+ // Always render via a portal into <body> so the modal escapes any parent
113
+ // stacking context created by Framer Motion transforms or CSS transforms.
114
+ if (!mounted) return null;
115
+ return createPortal(content, document.body);
116
+ };
117
+
118
+ export const ModalFooter = ({ children, className }: { children: React.ReactNode; className?: string }) => {
119
+ return (
120
+ <div className={cn("flex flex-col-reverse sm:flex-row justify-end gap-2 sm:gap-3 pt-4 sm:pt-6", className)}>
121
+ {children}
122
+ </div>
123
+ );
98
124
  };
@@ -12,8 +12,8 @@ export interface PopConfirmProps {
12
12
  /** Element that triggers the popover (e.g. Button) */
13
13
  trigger: React.ReactElement<{ onClick?: (e: React.MouseEvent) => void }>;
14
14
 
15
- /** Called when user confirms. Call callback() to close. setLoading controls confirm button loading state. */
16
- onConfirm: (callback: () => void, setLoading: (loading: boolean) => void) => void;
15
+ /** Called when user confirms. Call callback() to close. Returning a Promise automatically handles loading state. */
16
+ onConfirm: (callback: () => void, setLoading: (loading: boolean) => void) => void | Promise<any>;
17
17
 
18
18
  /** Called when user cancels. Call callback() to close. */
19
19
  onCancel: (callback: () => void) => void;
@@ -76,8 +76,19 @@ export const PopConfirm = ({
76
76
  setIsLoading(false);
77
77
  }, []);
78
78
 
79
- const handleConfirm = useCallback(() => {
80
- onConfirm(close, setIsLoading);
79
+ const handleConfirm = useCallback(async () => {
80
+ setIsLoading(true);
81
+ try {
82
+ const result = onConfirm(close, setIsLoading);
83
+ if (result instanceof Promise) {
84
+ await result;
85
+ // Automatically close if the promise resolves successfully
86
+ close();
87
+ }
88
+ } catch (e) {
89
+ console.error(e);
90
+ setIsLoading(false);
91
+ }
81
92
  }, [onConfirm, close]);
82
93
 
83
94
  const handleCancel = useCallback(() => {
@@ -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
+ };
@@ -0,0 +1,51 @@
1
+ import React from 'react';
2
+ import { Search, Settings2 } from 'lucide-react';
3
+ import { Dropdown, DropdownItem } from './Dropdown';
4
+ import { Input } from './Input';
5
+
6
+ export interface TableMobileOptionsProps {
7
+ searchValue: string;
8
+ onSearchChange: (value: string) => void;
9
+ searchPlaceholder?: string;
10
+ searchLabel?: string;
11
+ actionsLabel?: string;
12
+ children?: React.ReactNode;
13
+ }
14
+
15
+ export const TableMobileOptions = ({
16
+ searchValue,
17
+ onSearchChange,
18
+ searchPlaceholder = "Search...",
19
+ searchLabel = "Search",
20
+ actionsLabel = "Actions",
21
+ children
22
+ }: TableMobileOptionsProps) => {
23
+ return (
24
+ <Dropdown
25
+ align="right"
26
+ contentClassName="w-[280px] p-2"
27
+ trigger={
28
+ <button className="flex items-center justify-center gap-2 px-3 py-2 rounded-[10px] bg-white border border-gray-200 text-gray-700 hover:bg-gray-50 transition-colors shadow-sm font-bold text-[13px]">
29
+ <Settings2 size={16} /> Options
30
+ </button>
31
+ }
32
+ >
33
+ <div className="px-2 pb-2 pt-1 mb-1 border-b border-gray-100">
34
+ <p className="text-[11px] font-bold text-gray-400 uppercase tracking-wider mb-2">{searchLabel}</p>
35
+ <Input
36
+ placeholder={searchPlaceholder}
37
+ icon={<Search size={16} className="text-gray-400" />}
38
+ 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"
39
+ value={searchValue}
40
+ onChange={(e) => onSearchChange(e.target.value)}
41
+ />
42
+ </div>
43
+ {children && (
44
+ <div className="px-2 pt-1 pb-1">
45
+ <p className="text-[11px] font-bold text-gray-400 uppercase tracking-wider mb-1">{actionsLabel}</p>
46
+ {children}
47
+ </div>
48
+ )}
49
+ </Dropdown>
50
+ );
51
+ };