notionsoft-ui 1.0.20 → 1.0.22
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/cli/index.cjs +89 -7
- package/package.json +2 -1
- package/src/notion-ui/button/button.tsx +1 -1
- package/src/notion-ui/date-picker/DatePicker.stories.tsx +99 -0
- package/src/notion-ui/date-picker/date-picker.tsx +271 -0
- package/src/notion-ui/date-picker/index.ts +3 -0
- package/src/notion-ui/input/input.tsx +19 -12
- package/src/notion-ui/multi-date-picker/MultiDatePicker.stories.tsx +94 -0
- package/src/notion-ui/multi-date-picker/index.ts +3 -0
- package/src/notion-ui/multi-date-picker/multi-date-picker.tsx +265 -0
- package/src/notion-ui/multi-select-input/multi-select-input.tsx +202 -54
- package/src/notion-ui/password-input/password-input.tsx +39 -34
- package/src/notion-ui/phone-input/country-data.ts +420 -0
- package/src/notion-ui/phone-input/lazy-flag.tsx +83 -0
- package/src/notion-ui/phone-input/phone-input.tsx +400 -0
- package/src/notion-ui/phone-input/type.ts +227 -0
- package/src/notion-ui/phone-input/utils.ts +23 -0
|
@@ -8,8 +8,8 @@ import React, {
|
|
|
8
8
|
import { createPortal } from "react-dom";
|
|
9
9
|
import { buildNestedFiltersQuery, cn, useDebounce } from "../../utils/cn";
|
|
10
10
|
import Input from "../input";
|
|
11
|
-
import { Check, Eraser, ListFilter, X } from "lucide-react";
|
|
12
|
-
import
|
|
11
|
+
import { Check, Eraser, List, ListFilter, LoaderCircle, X } from "lucide-react";
|
|
12
|
+
import { NastranInputSize } from "../input/input";
|
|
13
13
|
|
|
14
14
|
export interface FilterItem {
|
|
15
15
|
key: string;
|
|
@@ -25,7 +25,6 @@ export type MultiSelectInputProps<T = any> = Omit<
|
|
|
25
25
|
React.InputHTMLAttributes<HTMLInputElement>,
|
|
26
26
|
"onSelect"
|
|
27
27
|
> & {
|
|
28
|
-
// Either `fetch` function OR `apiConfig` must be provided
|
|
29
28
|
fetch?: (
|
|
30
29
|
value: string,
|
|
31
30
|
filters?: Record<string, boolean>,
|
|
@@ -37,20 +36,31 @@ export type MultiSelectInputProps<T = any> = Omit<
|
|
|
37
36
|
filters?: FilterItem[];
|
|
38
37
|
onFiltersChange?: (filtersState: Record<string, boolean>) => void;
|
|
39
38
|
debounceValue?: number;
|
|
40
|
-
|
|
39
|
+
classNames?: {
|
|
40
|
+
rootDivClassName?: string;
|
|
41
|
+
};
|
|
41
42
|
text?: {
|
|
42
|
-
fetch?: string;
|
|
43
43
|
notItem?: string;
|
|
44
44
|
maxRecord?: string;
|
|
45
45
|
clearFilters?: string;
|
|
46
|
+
required?: string;
|
|
47
|
+
label?: string;
|
|
46
48
|
};
|
|
49
|
+
fixedOptions?: T[];
|
|
50
|
+
refechDependency?: any[];
|
|
51
|
+
showMaxFetch?: boolean;
|
|
47
52
|
endContent?: React.ReactNode;
|
|
53
|
+
startContent?: React.ReactNode;
|
|
54
|
+
errorMessage?: string;
|
|
48
55
|
selectionMode?: "single" | "multiple";
|
|
49
56
|
selected?: T | T[];
|
|
50
57
|
onItemsSelect?: (selected: T | T[]) => void;
|
|
58
|
+
onClear?: () => void;
|
|
51
59
|
searchBy?: keyof T | (keyof T)[];
|
|
52
60
|
itemKey?: keyof T;
|
|
53
61
|
STORAGE_KEY?: string;
|
|
62
|
+
measurement?: NastranInputSize;
|
|
63
|
+
readOnly?: boolean;
|
|
54
64
|
} & (
|
|
55
65
|
| {
|
|
56
66
|
fetch: (
|
|
@@ -70,14 +80,14 @@ function MultiSelectInputInner<T = any>(
|
|
|
70
80
|
filters = [],
|
|
71
81
|
onFiltersChange,
|
|
72
82
|
debounceValue = 500,
|
|
73
|
-
|
|
83
|
+
classNames,
|
|
74
84
|
text = {
|
|
75
|
-
fetch: "Fetching...",
|
|
76
85
|
notItem: "No results found",
|
|
77
86
|
maxRecord: "Max records",
|
|
78
87
|
clearFilters: "Clear Filters",
|
|
79
88
|
},
|
|
80
89
|
endContent,
|
|
90
|
+
startContent,
|
|
81
91
|
STORAGE_KEY = "FILTER_STORAGE_KEY",
|
|
82
92
|
selectionMode,
|
|
83
93
|
selected,
|
|
@@ -85,6 +95,12 @@ function MultiSelectInputInner<T = any>(
|
|
|
85
95
|
searchBy,
|
|
86
96
|
itemKey,
|
|
87
97
|
apiConfig,
|
|
98
|
+
errorMessage,
|
|
99
|
+
readOnly,
|
|
100
|
+
showMaxFetch,
|
|
101
|
+
fixedOptions,
|
|
102
|
+
refechDependency = [],
|
|
103
|
+
onClear,
|
|
88
104
|
...props
|
|
89
105
|
}: MultiSelectInputProps<T>,
|
|
90
106
|
ref: React.Ref<HTMLInputElement>
|
|
@@ -94,7 +110,10 @@ function MultiSelectInputInner<T = any>(
|
|
|
94
110
|
const [showFilters, setShowFilters] = useState(false);
|
|
95
111
|
const [isFetching, setIsFetching] = useState(false);
|
|
96
112
|
const [position, setPosition] = useState({ top: 0, left: 0, width: 0 });
|
|
97
|
-
const [items, setItems] = useState<T[]>([]);
|
|
113
|
+
const [items, setItems] = useState<T[]>(fixedOptions ?? []);
|
|
114
|
+
const [showSelectedOnly, setShowSelectedOnly] = useState(false);
|
|
115
|
+
const [shouldFetch, setShouldFetch] = useState(false);
|
|
116
|
+
const { rootDivClassName } = classNames || {};
|
|
98
117
|
const [filtersState, setFiltersState] = useState<Record<string, boolean>>(
|
|
99
118
|
() => {
|
|
100
119
|
try {
|
|
@@ -104,19 +123,47 @@ function MultiSelectInputInner<T = any>(
|
|
|
104
123
|
return filters.reduce((acc, f) => ({ ...acc, [f.key]: false }), {});
|
|
105
124
|
}
|
|
106
125
|
);
|
|
126
|
+
const [dropDirection, setDropDirection] = useState<"down" | "up">("down");
|
|
107
127
|
|
|
108
|
-
const [maxFetch, setMaxFetch] = useState<number
|
|
128
|
+
const [maxFetch, setMaxFetch] = useState<number>(() => {
|
|
109
129
|
try {
|
|
110
130
|
const saved = localStorage.getItem(`${STORAGE_KEY}_MAX_FETCH`);
|
|
111
|
-
return saved ? Number(saved) :
|
|
131
|
+
return saved ? Number(saved) : 30;
|
|
112
132
|
} catch {
|
|
113
|
-
return
|
|
133
|
+
return 30;
|
|
114
134
|
}
|
|
115
135
|
});
|
|
116
136
|
|
|
117
137
|
const [selectedItems, setSelectedItems] = useState<T[]>(
|
|
118
138
|
Array.isArray(selected) ? selected : selected ? [selected] : []
|
|
119
139
|
);
|
|
140
|
+
useEffect(() => {
|
|
141
|
+
const newSelected = Array.isArray(selected)
|
|
142
|
+
? selected
|
|
143
|
+
: selected
|
|
144
|
+
? [selected]
|
|
145
|
+
: [];
|
|
146
|
+
const key = itemKey as keyof T;
|
|
147
|
+
|
|
148
|
+
setSelectedItems((prev) => {
|
|
149
|
+
const combinedMap = new Map<string, T>();
|
|
150
|
+
|
|
151
|
+
// Add newSelected from prop first
|
|
152
|
+
newSelected.forEach((item) => {
|
|
153
|
+
const id = key ? String((item as any)[key]) : String(item);
|
|
154
|
+
combinedMap.set(id, item);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
prev.forEach((item) => {
|
|
158
|
+
const id = key ? String((item as any)[key]) : String(item);
|
|
159
|
+
if (!combinedMap.has(id)) {
|
|
160
|
+
combinedMap.delete(id);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
return Array.from(combinedMap.values());
|
|
164
|
+
});
|
|
165
|
+
}, [selected]);
|
|
166
|
+
|
|
120
167
|
const [pendingSelection, setPendingSelection] = useState<T[] | T | null>(
|
|
121
168
|
null
|
|
122
169
|
);
|
|
@@ -126,8 +173,16 @@ function MultiSelectInputInner<T = any>(
|
|
|
126
173
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
|
127
174
|
|
|
128
175
|
const debouncedValue = useDebounce(inputValue, debounceValue);
|
|
176
|
+
const fetchRef = useRef(fetch);
|
|
129
177
|
|
|
130
178
|
useEffect(() => {
|
|
179
|
+
fetchRef.current = fetch;
|
|
180
|
+
}, [fetch]);
|
|
181
|
+
useEffect(() => {
|
|
182
|
+
if (shouldFetch) setShouldFetch(false); // Allow fetch when function changes
|
|
183
|
+
}, [maxFetch, ...refechDependency]);
|
|
184
|
+
useEffect(() => {
|
|
185
|
+
if (!shouldFetch || !isFocused || fixedOptions || !fetch) return; // ⛔ skip until first focus
|
|
131
186
|
const get = async () => {
|
|
132
187
|
setIsFetching(true);
|
|
133
188
|
try {
|
|
@@ -135,7 +190,7 @@ function MultiSelectInputInner<T = any>(
|
|
|
135
190
|
|
|
136
191
|
if (fetch) {
|
|
137
192
|
// User-provided fetch function
|
|
138
|
-
data = await
|
|
193
|
+
data = await fetchRef.current(
|
|
139
194
|
debouncedValue,
|
|
140
195
|
filtersState,
|
|
141
196
|
maxFetch && !isNaN(Number(maxFetch)) ? Number(maxFetch) : undefined
|
|
@@ -151,7 +206,7 @@ function MultiSelectInputInner<T = any>(
|
|
|
151
206
|
|
|
152
207
|
const combinedParams = new URLSearchParams({
|
|
153
208
|
q: debouncedValue,
|
|
154
|
-
|
|
209
|
+
max: maxFetch?.toString() ?? "",
|
|
155
210
|
...apiConfig.params,
|
|
156
211
|
}).toString();
|
|
157
212
|
|
|
@@ -175,18 +230,46 @@ function MultiSelectInputInner<T = any>(
|
|
|
175
230
|
};
|
|
176
231
|
|
|
177
232
|
get();
|
|
178
|
-
}, [debouncedValue,
|
|
179
|
-
|
|
233
|
+
}, [debouncedValue, filtersState, maxFetch, shouldFetch]);
|
|
234
|
+
useLayoutEffect(() => {
|
|
235
|
+
if (dropdownRef.current) {
|
|
236
|
+
updatePosition();
|
|
237
|
+
}
|
|
238
|
+
}, [items, showSelectedOnly]);
|
|
180
239
|
// Update dropdown position
|
|
181
240
|
const updatePosition = () => {
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
241
|
+
const inputEl = containerRef.current;
|
|
242
|
+
const dropdownEl = dropdownRef.current;
|
|
243
|
+
if (!inputEl || !dropdownEl) return;
|
|
244
|
+
|
|
245
|
+
const rect = inputEl.getBoundingClientRect();
|
|
246
|
+
const viewportHeight = window.innerHeight;
|
|
247
|
+
const gap = 4; // distance between input and dropdown
|
|
248
|
+
|
|
249
|
+
// Actual dropdown height based on content, capped at 260px
|
|
250
|
+
const dropdownHeight = Math.min(dropdownEl.offsetHeight || 0, 260);
|
|
251
|
+
|
|
252
|
+
const spaceBelow = viewportHeight - rect.bottom;
|
|
253
|
+
const spaceAbove = rect.top;
|
|
254
|
+
|
|
255
|
+
// Decide direction
|
|
256
|
+
if (spaceBelow < dropdownHeight && spaceAbove > spaceBelow) {
|
|
257
|
+
// Flip above
|
|
258
|
+
setDropDirection("up");
|
|
259
|
+
setPosition({
|
|
260
|
+
top: rect.top + window.scrollY - dropdownHeight - gap,
|
|
261
|
+
left: rect.left + window.scrollX,
|
|
262
|
+
width: rect.width,
|
|
263
|
+
});
|
|
264
|
+
} else {
|
|
265
|
+
// Dropdown below
|
|
266
|
+
setDropDirection("down");
|
|
267
|
+
setPosition({
|
|
268
|
+
top: rect.bottom + window.scrollY + gap,
|
|
269
|
+
left: rect.left + window.scrollX,
|
|
270
|
+
width: rect.width,
|
|
271
|
+
});
|
|
272
|
+
}
|
|
190
273
|
};
|
|
191
274
|
|
|
192
275
|
// Focus handlers
|
|
@@ -196,7 +279,12 @@ function MultiSelectInputInner<T = any>(
|
|
|
196
279
|
const handleFocus = () => {
|
|
197
280
|
setIsFocused(true);
|
|
198
281
|
setShowFilters(false);
|
|
282
|
+
setShowSelectedOnly(false);
|
|
199
283
|
updatePosition();
|
|
284
|
+
// 🟢 First-time fetch trigger
|
|
285
|
+
if (!shouldFetch) {
|
|
286
|
+
setShouldFetch(true);
|
|
287
|
+
}
|
|
200
288
|
};
|
|
201
289
|
el.addEventListener("focus", handleFocus);
|
|
202
290
|
return () => el.removeEventListener("focus", handleFocus);
|
|
@@ -212,6 +300,7 @@ function MultiSelectInputInner<T = any>(
|
|
|
212
300
|
) {
|
|
213
301
|
setIsFocused(false);
|
|
214
302
|
setShowFilters(false);
|
|
303
|
+
setShowSelectedOnly(false);
|
|
215
304
|
}
|
|
216
305
|
};
|
|
217
306
|
document.addEventListener("mousedown", handleClickOutside);
|
|
@@ -278,6 +367,7 @@ function MultiSelectInputInner<T = any>(
|
|
|
278
367
|
setInputValue("");
|
|
279
368
|
setSelectedItems([]);
|
|
280
369
|
onItemsSelect?.([]);
|
|
370
|
+
if (onClear) onClear();
|
|
281
371
|
}}
|
|
282
372
|
className="hover:bg-tertiary/10 hover:text-tertiary size-[38px] p-3 cursor-pointer text-primary/60 rounded transition-colors"
|
|
283
373
|
/>
|
|
@@ -287,6 +377,8 @@ function MultiSelectInputInner<T = any>(
|
|
|
287
377
|
const dropdown =
|
|
288
378
|
(isFocused || showFilters) &&
|
|
289
379
|
Dropdown(
|
|
380
|
+
showSelectedOnly,
|
|
381
|
+
dropDirection,
|
|
290
382
|
position,
|
|
291
383
|
isFetching,
|
|
292
384
|
text,
|
|
@@ -295,10 +387,10 @@ function MultiSelectInputInner<T = any>(
|
|
|
295
387
|
showFilters,
|
|
296
388
|
handleFilterChange,
|
|
297
389
|
items,
|
|
390
|
+
setMaxFetch,
|
|
298
391
|
renderItem,
|
|
299
392
|
dropdownRef,
|
|
300
393
|
maxFetch,
|
|
301
|
-
setMaxFetch,
|
|
302
394
|
STORAGE_KEY,
|
|
303
395
|
onFiltersChange,
|
|
304
396
|
handleItemClick,
|
|
@@ -310,18 +402,51 @@ function MultiSelectInputInner<T = any>(
|
|
|
310
402
|
onItemsSelect
|
|
311
403
|
);
|
|
312
404
|
|
|
405
|
+
const selectedItemsIcon = selectedItems.length > 0 && (
|
|
406
|
+
<div
|
|
407
|
+
onClick={() => {
|
|
408
|
+
setShowFilters(false); // Hide filters panel
|
|
409
|
+
setIsFocused(true); // Open dropdown
|
|
410
|
+
setShowSelectedOnly(true); // Show only selected items
|
|
411
|
+
updatePosition(); // Recalculate dropdown position
|
|
412
|
+
}}
|
|
413
|
+
className="flex items-center hover:bg-tertiary/10 hover:text-tertiary cursor-pointer text-primary/60 rounded transition-colors"
|
|
414
|
+
>
|
|
415
|
+
<List className="size-[38px] p-3" />
|
|
416
|
+
<span className="text-sm px-1">{selectedItems.length}</span>
|
|
417
|
+
</div>
|
|
418
|
+
);
|
|
419
|
+
|
|
313
420
|
return (
|
|
314
|
-
<div
|
|
315
|
-
|
|
421
|
+
<div
|
|
422
|
+
ref={wrapperRef}
|
|
423
|
+
className={readOnly ? "pointer-events-none cursor-not-allowed" : ""}
|
|
424
|
+
>
|
|
425
|
+
<div
|
|
426
|
+
ref={containerRef}
|
|
427
|
+
className={cn("w-full relative", rootDivClassName)}
|
|
428
|
+
>
|
|
316
429
|
<Input
|
|
317
430
|
ref={ref || inputRef}
|
|
318
431
|
{...props}
|
|
432
|
+
readOnly={readOnly}
|
|
433
|
+
requiredHint={text.required}
|
|
434
|
+
label={text.label}
|
|
319
435
|
value={inputValue}
|
|
436
|
+
errorMessage={errorMessage}
|
|
320
437
|
onChange={inputOnChange}
|
|
438
|
+
startContent={startContent}
|
|
321
439
|
endContent={
|
|
322
440
|
<div className="flex items-center gap-1 relative ltr:-right-1 rtl:-left-1">
|
|
323
|
-
{
|
|
324
|
-
|
|
441
|
+
{!showFilters && isFetching ? (
|
|
442
|
+
<LoaderCircle className="size-[38px] p-3 animate-spin" />
|
|
443
|
+
) : (
|
|
444
|
+
<>
|
|
445
|
+
{selectedItemsIcon}
|
|
446
|
+
{isFocused && endIcon}
|
|
447
|
+
</>
|
|
448
|
+
)}
|
|
449
|
+
{(filters.length !== 0 || showMaxFetch) && (
|
|
325
450
|
<ListFilter
|
|
326
451
|
onClick={() => {
|
|
327
452
|
updatePosition();
|
|
@@ -353,6 +478,8 @@ export default MultiSelectInputForward;
|
|
|
353
478
|
|
|
354
479
|
// ---------------- Dropdown ----------------
|
|
355
480
|
const Dropdown = <T,>(
|
|
481
|
+
showSelectedOnly: boolean,
|
|
482
|
+
dropDirection: string,
|
|
356
483
|
position: { top: number; left: number; width: number },
|
|
357
484
|
isFetching: boolean,
|
|
358
485
|
text: {
|
|
@@ -366,10 +493,10 @@ const Dropdown = <T,>(
|
|
|
366
493
|
showFilters: boolean | undefined,
|
|
367
494
|
handleFilterChange: (key: string, value: boolean) => void,
|
|
368
495
|
items: T[],
|
|
496
|
+
setMaxFetch: React.Dispatch<React.SetStateAction<number>>,
|
|
369
497
|
renderItem?: (item: T, selected?: boolean) => React.ReactNode,
|
|
370
498
|
dropdownRef?: React.Ref<HTMLDivElement>,
|
|
371
499
|
maxFetch?: number | "",
|
|
372
|
-
setMaxFetch?: React.Dispatch<React.SetStateAction<number | "">>,
|
|
373
500
|
STORAGE_KEY?: string,
|
|
374
501
|
onFiltersChange?: (filtersState: Record<string, boolean>) => void,
|
|
375
502
|
handleItemClick?: (item: T) => void,
|
|
@@ -380,17 +507,21 @@ const Dropdown = <T,>(
|
|
|
380
507
|
setInputValue?: React.Dispatch<React.SetStateAction<string>>,
|
|
381
508
|
onItemsSelect?: (selected: T | T[]) => void
|
|
382
509
|
) =>
|
|
510
|
+
!isFetching &&
|
|
383
511
|
createPortal(
|
|
384
512
|
<div
|
|
385
513
|
ref={dropdownRef}
|
|
386
|
-
className=
|
|
514
|
+
className={cn(
|
|
515
|
+
"absolute z-50 border border-border ltr:text-xs ltr:sm:text-sm rtl:text-sm rtl:font-semibold bg-card shadow-lg pb-2",
|
|
516
|
+
dropDirection === "down" ? "rounded-b" : "rounded-t"
|
|
517
|
+
)}
|
|
387
518
|
style={{ top: position.top, left: position.left, width: position.width }}
|
|
388
519
|
>
|
|
389
520
|
{/* Filters Panel */}
|
|
390
|
-
{
|
|
391
|
-
<div className="pb-3 px-3 flex flex-col gap-2
|
|
392
|
-
{filters.map((f) => (
|
|
393
|
-
<label key={f.key} className="flex items-center gap-2">
|
|
521
|
+
{
|
|
522
|
+
<div className="pb-3 px-3 flex flex-col gap-2">
|
|
523
|
+
{filters.map((f, index) => (
|
|
524
|
+
<label key={f.key + index} className="flex items-center gap-2">
|
|
394
525
|
<input
|
|
395
526
|
type="checkbox"
|
|
396
527
|
checked={filtersState[f.key]}
|
|
@@ -401,24 +532,26 @@ const Dropdown = <T,>(
|
|
|
401
532
|
</label>
|
|
402
533
|
))}
|
|
403
534
|
|
|
404
|
-
{
|
|
535
|
+
{(showFilters || filters.length > 0) && (
|
|
405
536
|
<input
|
|
406
537
|
type="number"
|
|
407
538
|
min={1}
|
|
408
539
|
value={maxFetch}
|
|
409
540
|
onChange={(e) => {
|
|
410
|
-
const value =
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
541
|
+
const value = Number(e.target.value);
|
|
542
|
+
if (value) {
|
|
543
|
+
setMaxFetch(value);
|
|
544
|
+
if (STORAGE_KEY)
|
|
545
|
+
localStorage.setItem(
|
|
546
|
+
`${STORAGE_KEY}_MAX_FETCH`,
|
|
547
|
+
JSON.stringify(value)
|
|
548
|
+
);
|
|
549
|
+
}
|
|
417
550
|
}}
|
|
418
551
|
className={cn(
|
|
419
|
-
"selection:bg-primary selection:text-primary-foreground dark:bg-input/30
|
|
552
|
+
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 flex w-full min-w-0 rounded-sm border px-3 text-base transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-70",
|
|
420
553
|
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
|
421
|
-
"appearance-none placeholder:text-primary/60 ltr:text-sm rtl:text-sm rtl:font-semibold focus-visible:ring-0 focus-visible:shadow-sm focus-visible:ring-offset-0 transition-[border] bg-card
|
|
554
|
+
"appearance-none placeholder:text-primary/60 ltr:text-sm rtl:text-sm rtl:font-semibold focus-visible:ring-0 focus-visible:shadow-sm focus-visible:ring-offset-0 transition-[border] bg-card",
|
|
422
555
|
"focus-visible:border-tertiary/60",
|
|
423
556
|
"[&::-webkit-outer-spin-button]:appearance-none",
|
|
424
557
|
"[&::-webkit-inner-spin-button]:appearance-none",
|
|
@@ -429,7 +562,7 @@ const Dropdown = <T,>(
|
|
|
429
562
|
)}
|
|
430
563
|
|
|
431
564
|
{/* Clear Filters + Selected Items */}
|
|
432
|
-
{STORAGE_KEY &&
|
|
565
|
+
{STORAGE_KEY && showFilters && (
|
|
433
566
|
<button
|
|
434
567
|
onClick={() => {
|
|
435
568
|
// Clear filters
|
|
@@ -443,7 +576,7 @@ const Dropdown = <T,>(
|
|
|
443
576
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(cleared));
|
|
444
577
|
|
|
445
578
|
// Clear maxFetch
|
|
446
|
-
setMaxFetch(
|
|
579
|
+
setMaxFetch(30);
|
|
447
580
|
localStorage.removeItem(`${STORAGE_KEY}_MAX_FETCH`);
|
|
448
581
|
|
|
449
582
|
// Clear selected items and input
|
|
@@ -460,18 +593,33 @@ const Dropdown = <T,>(
|
|
|
460
593
|
</button>
|
|
461
594
|
)}
|
|
462
595
|
</div>
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
{!showFilters && isFetching && <CircleLoader label={text.fetch} />}
|
|
466
|
-
|
|
596
|
+
}
|
|
467
597
|
{!showFilters && !isFetching && (
|
|
468
598
|
<div className="max-h-60 overflow-auto">
|
|
469
|
-
{
|
|
599
|
+
{showSelectedOnly ? (
|
|
600
|
+
selectedItems &&
|
|
601
|
+
selectedItems.length > 0 &&
|
|
602
|
+
selectedItems.map((item, index) => {
|
|
603
|
+
const keyVal = itemKey ? (item as any)[itemKey] : index;
|
|
604
|
+
return renderItem ? (
|
|
605
|
+
renderItem(item, true)
|
|
606
|
+
) : (
|
|
607
|
+
<div
|
|
608
|
+
key={keyVal}
|
|
609
|
+
className="px-3 flex items-center gap-x-1 py-1 cursor-pointer"
|
|
610
|
+
onClick={() => handleItemClick?.(item)}
|
|
611
|
+
>
|
|
612
|
+
<Check className="size-4" />
|
|
613
|
+
{(item as any)[searchBy ?? "name"]}
|
|
614
|
+
</div>
|
|
615
|
+
);
|
|
616
|
+
})
|
|
617
|
+
) : items.length > 0 ? (
|
|
470
618
|
items.map((item, index) => {
|
|
471
619
|
const keyVal = itemKey ? (item as any)[itemKey] : index;
|
|
472
620
|
const isSelected =
|
|
473
621
|
selectedItems?.some(
|
|
474
|
-
(i) => itemKey && (i as any)[itemKey]
|
|
622
|
+
(i) => itemKey && (i as any)[itemKey] == keyVal
|
|
475
623
|
) ?? false;
|
|
476
624
|
|
|
477
625
|
const displayValue = Array.isArray(searchBy)
|
|
@@ -484,8 +632,8 @@ const Dropdown = <T,>(
|
|
|
484
632
|
<div
|
|
485
633
|
key={keyVal}
|
|
486
634
|
className={cn(
|
|
487
|
-
"px-3 flex items-center gap-x-1 py-1 hover:bg-
|
|
488
|
-
isSelected && "bg-
|
|
635
|
+
"px-3 flex items-center gap-x-1 py-1 hover:bg-primary/5 cursor-pointer",
|
|
636
|
+
isSelected && "bg-primary/5"
|
|
489
637
|
)}
|
|
490
638
|
onClick={() => handleItemClick?.(item)}
|
|
491
639
|
>
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import Input from "../input";
|
|
2
|
-
import { InputProps } from "../input/input";
|
|
1
|
+
import Input, { InputProps } from "../input/input";
|
|
3
2
|
import { Check, X } from "lucide-react";
|
|
4
3
|
import React, { useMemo, useState } from "react";
|
|
5
4
|
type PasswordInputText = {
|
|
@@ -17,16 +16,36 @@ export interface PasswordInputProps extends InputProps {
|
|
|
17
16
|
text: PasswordInputText;
|
|
18
17
|
}
|
|
19
18
|
const PasswordInput = React.forwardRef<HTMLInputElement, PasswordInputProps>(
|
|
20
|
-
(props, ref
|
|
21
|
-
const {
|
|
22
|
-
const
|
|
23
|
-
|
|
19
|
+
(props, ref) => {
|
|
20
|
+
const { classNames, value, text, onChange, ...rest } = props;
|
|
21
|
+
const { rootDivClassName } = classNames || {};
|
|
22
|
+
|
|
23
|
+
// Internal state only if parent does NOT control value
|
|
24
|
+
const [password, setPassword] = useState(value ?? "");
|
|
25
|
+
|
|
26
|
+
// Use parent-controlled value if provided, otherwise internal state
|
|
27
|
+
const currentPassword = value !== undefined ? value : password;
|
|
28
|
+
|
|
29
|
+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
30
|
+
if (value === undefined) {
|
|
31
|
+
setPassword(e.target.value);
|
|
32
|
+
}
|
|
33
|
+
onChange?.(e);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const strength = useMemo(
|
|
37
|
+
() =>
|
|
38
|
+
checkStrength(
|
|
39
|
+
typeof currentPassword == "string" ? currentPassword : "",
|
|
40
|
+
text
|
|
41
|
+
),
|
|
42
|
+
[currentPassword, text]
|
|
24
43
|
);
|
|
25
|
-
const strength = checkStrength(value, text);
|
|
26
44
|
|
|
27
|
-
const strengthScore = useMemo(
|
|
28
|
-
|
|
29
|
-
|
|
45
|
+
const strengthScore = useMemo(
|
|
46
|
+
() => passwordStrengthScore(strength),
|
|
47
|
+
[strength]
|
|
48
|
+
);
|
|
30
49
|
|
|
31
50
|
const getStrengthColor = (score: number) => {
|
|
32
51
|
if (score === 0) return "bg-border";
|
|
@@ -42,21 +61,18 @@ const PasswordInput = React.forwardRef<HTMLInputElement, PasswordInputProps>(
|
|
|
42
61
|
if (score === 3) return text.medium_password;
|
|
43
62
|
return text.strong_password;
|
|
44
63
|
};
|
|
64
|
+
|
|
45
65
|
return (
|
|
46
|
-
<div className={`w-full ${
|
|
66
|
+
<div className={`w-full ${rootDivClassName ?? ""}`}>
|
|
47
67
|
<Input
|
|
48
|
-
value={
|
|
68
|
+
value={currentPassword}
|
|
49
69
|
ref={ref}
|
|
50
|
-
onChange={
|
|
51
|
-
onChange
|
|
52
|
-
? onChange
|
|
53
|
-
: (event: React.ChangeEvent<HTMLInputElement>) =>
|
|
54
|
-
setValue(event.target.value)
|
|
55
|
-
}
|
|
70
|
+
onChange={handleChange}
|
|
56
71
|
aria-invalid={strengthScore < 4}
|
|
57
72
|
aria-describedby="password-strength"
|
|
58
73
|
{...rest}
|
|
59
74
|
/>
|
|
75
|
+
|
|
60
76
|
{/* Password strength indicator */}
|
|
61
77
|
<div
|
|
62
78
|
className="mb-4 mt-3 h-1 w-full overflow-hidden rounded-full bg-border"
|
|
@@ -71,10 +87,10 @@ const PasswordInput = React.forwardRef<HTMLInputElement, PasswordInputProps>(
|
|
|
71
87
|
strengthScore
|
|
72
88
|
)} transition-all duration-500 ease-out`}
|
|
73
89
|
style={{ width: `${(strengthScore / 4) * 100}%` }}
|
|
74
|
-
|
|
90
|
+
/>
|
|
75
91
|
</div>
|
|
76
92
|
|
|
77
|
-
{/* Password strength
|
|
93
|
+
{/* Password strength text */}
|
|
78
94
|
<p
|
|
79
95
|
id="password-strength"
|
|
80
96
|
className="mb-2 text-start rtl:text-xl-rtl ltr:text-xl-ltr font-medium text-foreground"
|
|
@@ -82,22 +98,14 @@ const PasswordInput = React.forwardRef<HTMLInputElement, PasswordInputProps>(
|
|
|
82
98
|
{`${getStrengthText(strengthScore)}. ${text.must_contain}`}
|
|
83
99
|
</p>
|
|
84
100
|
|
|
85
|
-
{/*
|
|
101
|
+
{/* Requirements */}
|
|
86
102
|
<ul className="space-y-1.5" aria-label="Password requirements">
|
|
87
103
|
{strength.map((req, index) => (
|
|
88
104
|
<li key={index} className="flex items-center gap-2">
|
|
89
105
|
{req.met ? (
|
|
90
|
-
<Check
|
|
91
|
-
size={16}
|
|
92
|
-
className="text-emerald-500"
|
|
93
|
-
aria-hidden="true"
|
|
94
|
-
/>
|
|
106
|
+
<Check size={16} className="text-emerald-500" />
|
|
95
107
|
) : (
|
|
96
|
-
<X
|
|
97
|
-
size={16}
|
|
98
|
-
className="text-muted-foreground/80"
|
|
99
|
-
aria-hidden="true"
|
|
100
|
-
/>
|
|
108
|
+
<X size={16} className="text-muted-foreground/80" />
|
|
101
109
|
)}
|
|
102
110
|
<span
|
|
103
111
|
className={`ltr:text-xs rtl:text-lg-rtl ${
|
|
@@ -107,9 +115,6 @@ const PasswordInput = React.forwardRef<HTMLInputElement, PasswordInputProps>(
|
|
|
107
115
|
}`}
|
|
108
116
|
>
|
|
109
117
|
{req.text}
|
|
110
|
-
<span className="sr-only rtl:text-xl-rtl">
|
|
111
|
-
{req.met ? " - Requirement met" : " - Requirement not met"}
|
|
112
|
-
</span>
|
|
113
118
|
</span>
|
|
114
119
|
</li>
|
|
115
120
|
))}
|