@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.
- package/package.json +3 -2
- package/src/base-components/Input.tsx +6 -3
- package/src/base-components/TagsInput.tsx +106 -0
- package/src/base-components/WizardModal.tsx +162 -0
- package/src/common-components/item-wizard/ItemFormWizard.tsx +705 -0
- package/src/common-components/item-wizard/SkuConfigModal.tsx +314 -0
- package/src/common-components/pickers/BrandPicker.tsx +194 -0
- package/src/common-components/pickers/CategoryPicker.tsx +342 -0
- package/src/common-components/pickers/EntityPickerModal.tsx +221 -0
- package/src/common-components/pickers/PartyPicker.tsx +231 -0
- package/src/common-components/pickers/TransactionItemPicker.tsx +166 -0
- package/src/common-components/pickers/UomGroupPicker.tsx +196 -0
- package/src/common-components/pickers/UomPicker.tsx +306 -0
- package/src/common-components/pickers/WarehousePicker.tsx +135 -0
- package/src/common-components/pickers/index.ts +11 -0
- package/src/common-components/pickers/modals/BatchSelectionModal.tsx +359 -0
- package/src/common-components/pickers/modals/SerialSelectionModal.tsx +247 -0
- package/src/common-components/pickers/modals/types.ts +30 -0
- package/src/common-components/transaction/MetadataPanel.tsx +417 -0
- package/src/common-components/transaction/ProductSelectionPanel.tsx +587 -0
- package/src/common-components/transaction/ProductTransactionScreen.tsx +250 -0
- package/src/common-components/transaction/index.ts +17 -0
- package/src/common-components/transaction/types.ts +300 -0
- package/src/components/shared/ImageUploadComponent.tsx +1 -2
- package/src/index.tsx +20 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@apptimate/ui",
|
|
3
|
-
"version": "3.
|
|
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
|
+
};
|