@apptimate/ui 3.5.0 → 3.7.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 (25) hide show
  1. package/package.json +3 -2
  2. package/src/base-components/Input.tsx +6 -3
  3. package/src/base-components/TagsInput.tsx +106 -0
  4. package/src/base-components/WizardModal.tsx +162 -0
  5. package/src/common-components/item-wizard/ItemFormWizard.tsx +705 -0
  6. package/src/common-components/item-wizard/SkuConfigModal.tsx +314 -0
  7. package/src/common-components/pickers/BrandPicker.tsx +194 -0
  8. package/src/common-components/pickers/CategoryPicker.tsx +342 -0
  9. package/src/common-components/pickers/EntityPickerModal.tsx +221 -0
  10. package/src/common-components/pickers/PartyPicker.tsx +231 -0
  11. package/src/common-components/pickers/TransactionItemPicker.tsx +166 -0
  12. package/src/common-components/pickers/UomGroupPicker.tsx +196 -0
  13. package/src/common-components/pickers/UomPicker.tsx +306 -0
  14. package/src/common-components/pickers/WarehousePicker.tsx +135 -0
  15. package/src/common-components/pickers/index.ts +11 -0
  16. package/src/common-components/pickers/modals/BatchSelectionModal.tsx +359 -0
  17. package/src/common-components/pickers/modals/SerialSelectionModal.tsx +247 -0
  18. package/src/common-components/pickers/modals/types.ts +30 -0
  19. package/src/common-components/transaction/MetadataPanel.tsx +417 -0
  20. package/src/common-components/transaction/ProductSelectionPanel.tsx +587 -0
  21. package/src/common-components/transaction/ProductTransactionScreen.tsx +250 -0
  22. package/src/common-components/transaction/index.ts +17 -0
  23. package/src/common-components/transaction/types.ts +300 -0
  24. package/src/components/shared/ImageUploadComponent.tsx +1 -2
  25. package/src/index.tsx +20 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apptimate/ui",
3
- "version": "3.5.0",
3
+ "version": "3.7.0",
4
4
  "main": "src/index.tsx",
5
5
  "types": "src/index.tsx",
6
6
  "dependencies": {
@@ -13,7 +13,8 @@
13
13
  "react": "^19.2.5",
14
14
  "react-apexcharts": "^2.1.1",
15
15
  "recharts": "^2.15.4",
16
- "tailwind-merge": "^3.5.0"
16
+ "tailwind-merge": "^3.5.0",
17
+ "use-debounce": "^10.1.1"
17
18
  },
18
19
  "publishConfig": {
19
20
  "access": "public"
@@ -10,10 +10,11 @@ export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement>
10
10
  error?: string;
11
11
  icon?: React.ReactNode;
12
12
  isRequired?: boolean;
13
+ inputClassName?: string;
13
14
  }
14
15
 
15
16
  export const Input = React.forwardRef<HTMLInputElement, InputProps>(
16
- ({ label, error, icon, isRequired, className, type, ...props }, ref) => {
17
+ ({ label, error, icon, isRequired, className, inputClassName, type, ...props }, ref) => {
17
18
  const [showPassword, setShowPassword] = useState(false);
18
19
  const isPassword = type === 'password';
19
20
  const inputType = isPassword ? (showPassword ? 'text' : 'password') : type;
@@ -25,7 +26,7 @@ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
25
26
  {label}
26
27
  </Label>
27
28
  )}
28
- <div className="relative group">
29
+ <div className="relative group flex-1 flex flex-col justify-center">
29
30
  {icon && (
30
31
  <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">
31
32
  {icon}
@@ -36,9 +37,11 @@ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
36
37
  type={inputType}
37
38
  className={cn(
38
39
  "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",
40
+ "disabled:opacity-50 disabled:cursor-not-allowed disabled:bg-gray-50 disabled:text-gray-500",
39
41
  icon && "pl-10",
40
42
  isPassword && "pr-10",
41
- error && "border-danger-alt focus:border-danger-alt hover:border-danger-alt"
43
+ error && "border-danger-alt focus:border-danger-alt hover:border-danger-alt",
44
+ inputClassName
42
45
  )}
43
46
  {...props}
44
47
  />
@@ -0,0 +1,106 @@
1
+ "use client";
2
+
3
+ import { cn } from '@apptimate/core-lib';
4
+ import { X } from 'lucide-react';
5
+ import React, { useState, KeyboardEvent, useRef } from 'react';
6
+ import { Label } from './Label';
7
+
8
+ export interface TagsInputProps {
9
+ label?: string;
10
+ error?: string;
11
+ isRequired?: boolean;
12
+ values: string[];
13
+ onChange: (values: string[]) => void;
14
+ placeholder?: string;
15
+ className?: string;
16
+ disabled?: boolean;
17
+ }
18
+
19
+ export const TagsInput: React.FC<TagsInputProps> = ({
20
+ label,
21
+ error,
22
+ isRequired,
23
+ values,
24
+ onChange,
25
+ placeholder = "Type and press Enter...",
26
+ className,
27
+ disabled = false,
28
+ }) => {
29
+ const [inputValue, setInputValue] = useState('');
30
+ const inputRef = useRef<HTMLInputElement>(null);
31
+
32
+ const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
33
+ if (disabled) return;
34
+ if (e.key === 'Enter' || e.key === ',') {
35
+ e.preventDefault();
36
+ const newTag = inputValue.trim();
37
+ if (newTag && !values.includes(newTag)) {
38
+ onChange([...values, newTag]);
39
+ }
40
+ setInputValue('');
41
+ } else if (e.key === 'Backspace' && !inputValue && values.length > 0) {
42
+ e.preventDefault();
43
+ onChange(values.slice(0, -1));
44
+ }
45
+ };
46
+
47
+ const removeTag = (tagToRemove: string) => {
48
+ if (disabled) return;
49
+ onChange(values.filter(tag => tag !== tagToRemove));
50
+ };
51
+
52
+ return (
53
+ <div className={cn("flex flex-col gap-1.5 w-full", className)}>
54
+ {label && (
55
+ <Label isRequired={isRequired}>
56
+ {label}
57
+ </Label>
58
+ )}
59
+ <div
60
+ className={cn(
61
+ "flex flex-wrap gap-2 w-full bg-surface-0 border-[1.5px] border-border-subtle rounded-[10px] px-2 py-2 min-h-[42px] transition-all",
62
+ !disabled && "cursor-text hover:border-gray-300 focus-within:border-gray-300 focus-within:bg-surface-1",
63
+ disabled && "opacity-50 cursor-not-allowed bg-gray-50",
64
+ error && !disabled && "border-danger-alt focus-within:border-danger-alt hover:border-danger-alt"
65
+ )}
66
+ onClick={() => !disabled && inputRef.current?.focus()}
67
+ >
68
+ {values.map((tag) => (
69
+ <span
70
+ key={tag}
71
+ className="flex items-center gap-1.5 px-2.5 py-1 text-[12px] font-medium bg-[#F3F4F6] text-[#2D3142] rounded-md border border-[#E5E7EB]"
72
+ >
73
+ {tag}
74
+ {!disabled && (
75
+ <button
76
+ type="button"
77
+ onClick={(e) => { e.stopPropagation(); removeTag(tag); }}
78
+ className="text-gray-400 hover:text-gray-700 transition-colors"
79
+ >
80
+ <X size={12} strokeWidth={3} />
81
+ </button>
82
+ )}
83
+ </span>
84
+ ))}
85
+ <input
86
+ ref={inputRef}
87
+ type="text"
88
+ disabled={disabled}
89
+ value={inputValue}
90
+ onChange={(e) => setInputValue(e.target.value)}
91
+ onKeyDown={handleKeyDown}
92
+ onBlur={() => {
93
+ const newTag = inputValue.trim();
94
+ if (newTag && !values.includes(newTag)) {
95
+ onChange([...values, newTag]);
96
+ }
97
+ setInputValue('');
98
+ }}
99
+ placeholder={values.length === 0 ? placeholder : ""}
100
+ className={cn("flex-1 min-w-[100px] bg-transparent text-[13.5px] text-foreground-1 outline-none placeholder:text-foreground-disabled", disabled && "cursor-not-allowed")}
101
+ />
102
+ </div>
103
+ {error && <span className="text-[11px] text-danger-alt font-medium">{error}</span>}
104
+ </div>
105
+ );
106
+ };
@@ -0,0 +1,162 @@
1
+ "use client";
2
+
3
+ import React, { ReactNode } from "react";
4
+ import { Modal, ModalFooter } from "./Modal";
5
+ import { Button } from "./Button";
6
+ import { ChevronLeft, ChevronRight } from "lucide-react";
7
+
8
+ export interface WizardStep {
9
+ key: string;
10
+ label: string;
11
+ num: number;
12
+ }
13
+
14
+ export interface WizardModalProps {
15
+ isOpen: boolean;
16
+ onClose: () => void;
17
+ title: string;
18
+ size?: "xs" | "sm" | "md" | "lg" | "xl" | "2xl" | "full";
19
+ steps: WizardStep[];
20
+ currentStep: number;
21
+ onStepChange: (step: number) => void;
22
+ canNext?: (step: number) => boolean;
23
+ onSubmit: () => void;
24
+ submitLabel?: string;
25
+ cancelLabel?: string;
26
+ nextLabel?: string;
27
+ backLabel?: string;
28
+ isSubmitting?: boolean;
29
+ children: ReactNode;
30
+ }
31
+
32
+ export const WizardModal = ({
33
+ isOpen,
34
+ onClose,
35
+ title,
36
+ size = "xl",
37
+ steps,
38
+ currentStep,
39
+ onStepChange,
40
+ canNext,
41
+ onSubmit,
42
+ submitLabel = "Submit",
43
+ cancelLabel = "Cancel",
44
+ nextLabel = "Next",
45
+ backLabel = "Back",
46
+ isSubmitting = false,
47
+ children,
48
+ }: WizardModalProps) => {
49
+ const isNextDisabled = canNext ? !canNext(currentStep) : false;
50
+ const isLastStep = currentStep === steps.length - 1;
51
+
52
+ const handleNext = () => {
53
+ if (!isNextDisabled && !isLastStep) {
54
+ onStepChange(currentStep + 1);
55
+ }
56
+ };
57
+
58
+ const handleBack = () => {
59
+ if (currentStep > 0) {
60
+ onStepChange(currentStep - 1);
61
+ }
62
+ };
63
+
64
+ const handleStepClick = (index: number) => {
65
+ if (index < currentStep) {
66
+ onStepChange(index);
67
+ } else if (index === currentStep + 1 && !isNextDisabled) {
68
+ onStepChange(index);
69
+ }
70
+ };
71
+
72
+ return (
73
+ <Modal isOpen={isOpen} onClose={onClose} title={title} size={size}>
74
+ <div className="min-h-[400px] flex flex-col">
75
+ {/* Stepper Header */}
76
+ <div className="flex items-center gap-1 mb-6">
77
+ {steps.map((s, i) => (
78
+ <React.Fragment key={s.key}>
79
+ <button
80
+ type="button"
81
+ onClick={() => handleStepClick(i)}
82
+ className={`flex items-center gap-2 px-3 py-2 rounded-lg text-[13px] font-semibold transition-all ${
83
+ i === currentStep
84
+ ? "bg-primary-500 text-white shadow-md shadow-primary-500/20"
85
+ : i < currentStep
86
+ ? "bg-primary-50 text-primary-600 hover:bg-primary-100"
87
+ : "bg-gray-100 text-gray-400"
88
+ }`}
89
+ >
90
+ <span
91
+ className={`w-5 h-5 rounded-full flex items-center justify-center text-[11px] font-bold ${
92
+ i === currentStep
93
+ ? "bg-white/20 text-white"
94
+ : i < currentStep
95
+ ? "bg-primary-200 text-primary-700"
96
+ : "bg-gray-200 text-gray-400"
97
+ }`}
98
+ >
99
+ {s.num}
100
+ </span>
101
+ <span className="hidden sm:inline">{s.label}</span>
102
+ </button>
103
+ {i < steps.length - 1 && (
104
+ <div className={`flex-1 h-0.5 rounded-full mx-1 ${i < currentStep ? "bg-primary-300" : "bg-gray-200"}`} />
105
+ )}
106
+ </React.Fragment>
107
+ ))}
108
+ </div>
109
+
110
+ {/* Content */}
111
+ <div className="flex-1 pb-2">
112
+ {children}
113
+ </div>
114
+
115
+ {/* Footer */}
116
+ <ModalFooter>
117
+ <div className="flex items-center justify-between w-full">
118
+ <div>
119
+ {currentStep > 0 && (
120
+ <Button
121
+ variant="flat"
122
+ color="default"
123
+ onClick={handleBack}
124
+ icon={<ChevronLeft size={15} />}
125
+ iconPosition="left"
126
+ isDisabled={isSubmitting}
127
+ >
128
+ {backLabel}
129
+ </Button>
130
+ )}
131
+ </div>
132
+ <div className="flex items-center gap-2">
133
+ <Button
134
+ variant="flat"
135
+ color="default"
136
+ onClick={onClose}
137
+ isDisabled={isSubmitting}
138
+ >
139
+ {cancelLabel}
140
+ </Button>
141
+ {!isLastStep ? (
142
+ <Button
143
+ color="primary"
144
+ onClick={handleNext}
145
+ isDisabled={isNextDisabled || isSubmitting}
146
+ icon={<ChevronRight size={15} />}
147
+ iconPosition="right"
148
+ >
149
+ {nextLabel}
150
+ </Button>
151
+ ) : (
152
+ <Button color="primary" onClick={onSubmit} isLoading={isSubmitting}>
153
+ {submitLabel}
154
+ </Button>
155
+ )}
156
+ </div>
157
+ </div>
158
+ </ModalFooter>
159
+ </div>
160
+ </Modal>
161
+ );
162
+ };