@ayasofyazilim/ui 0.0.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/__mocks__/canvas.ts +8 -0
- package/components.json +21 -0
- package/eslint.config.js +4 -0
- package/jest-environment.js +37 -0
- package/jest.config.ts +47 -0
- package/jest.setup.ts +69 -0
- package/package.json +124 -0
- package/postcss.config.mjs +6 -0
- package/src/aria/index.tsx +1 -0
- package/src/aria/number-field.tsx +41 -0
- package/src/components/.gitkeep +0 -0
- package/src/components/accordion.tsx +66 -0
- package/src/components/alert-dialog.tsx +157 -0
- package/src/components/alert.tsx +70 -0
- package/src/components/aspect-ratio.tsx +11 -0
- package/src/components/avatar.tsx +53 -0
- package/src/components/badge.tsx +67 -0
- package/src/components/breadcrumb.tsx +109 -0
- package/src/components/button-group.tsx +83 -0
- package/src/components/button.tsx +68 -0
- package/src/components/calendar.tsx +219 -0
- package/src/components/card.tsx +92 -0
- package/src/components/carousel.tsx +241 -0
- package/src/components/chart.tsx +363 -0
- package/src/components/checkbox.tsx +32 -0
- package/src/components/collapsible.tsx +33 -0
- package/src/components/command.tsx +184 -0
- package/src/components/context-menu.tsx +252 -0
- package/src/components/dialog.tsx +144 -0
- package/src/components/drawer.tsx +135 -0
- package/src/components/dropdown-menu.tsx +258 -0
- package/src/components/empty.tsx +100 -0
- package/src/components/field.tsx +248 -0
- package/src/components/form.tsx +169 -0
- package/src/components/hover-card.tsx +44 -0
- package/src/components/input-group.tsx +170 -0
- package/src/components/input-otp.tsx +77 -0
- package/src/components/input.tsx +21 -0
- package/src/components/item.tsx +193 -0
- package/src/components/kbd.tsx +28 -0
- package/src/components/label.tsx +24 -0
- package/src/components/menubar.tsx +276 -0
- package/src/components/navigation-menu.tsx +168 -0
- package/src/components/pagination.tsx +130 -0
- package/src/components/popover.tsx +88 -0
- package/src/components/progress.tsx +31 -0
- package/src/components/radio-group.tsx +45 -0
- package/src/components/resizable.tsx +56 -0
- package/src/components/scroll-area.tsx +58 -0
- package/src/components/select.tsx +189 -0
- package/src/components/separator.tsx +28 -0
- package/src/components/sheet.tsx +140 -0
- package/src/components/sidebar.tsx +862 -0
- package/src/components/skeleton.tsx +13 -0
- package/src/components/slider.tsx +63 -0
- package/src/components/sonner.tsx +40 -0
- package/src/components/spinner.tsx +16 -0
- package/src/components/stepper.tsx +291 -0
- package/src/components/switch.tsx +31 -0
- package/src/components/table.tsx +133 -0
- package/src/components/tabs.tsx +66 -0
- package/src/components/textarea.tsx +18 -0
- package/src/components/toggle-group.tsx +83 -0
- package/src/components/toggle.tsx +47 -0
- package/src/components/tooltip.tsx +66 -0
- package/src/custom/action-button.tsx +48 -0
- package/src/custom/async-select.tsx +287 -0
- package/src/custom/awesome-not-found.tsx +116 -0
- package/src/custom/charts/area-chart.tsx +147 -0
- package/src/custom/charts/bar-chart.tsx +233 -0
- package/src/custom/charts/chart-card.tsx +103 -0
- package/src/custom/charts/index.tsx +16 -0
- package/src/custom/charts/pie-chart.tsx +168 -0
- package/src/custom/charts/radar-chart.tsx +126 -0
- package/src/custom/checkbox-tree.tsx +100 -0
- package/src/custom/combobox.tsx +296 -0
- package/src/custom/confirm-dialog.tsx +102 -0
- package/src/custom/country-selector.tsx +204 -0
- package/src/custom/date-picker/calendar-rac.tsx +109 -0
- package/src/custom/date-picker/datefield-rac.tsx +84 -0
- package/src/custom/date-picker/index.tsx +273 -0
- package/src/custom/date-picker/types/index.ts +4 -0
- package/src/custom/date-picker/utils/index.ts +42 -0
- package/src/custom/date-picker-old.tsx +50 -0
- package/src/custom/date-tooltip.tsx +98 -0
- package/src/custom/document-scanner/consts.ts +5 -0
- package/src/custom/document-scanner/corner-adjustment/action-buttons.tsx +33 -0
- package/src/custom/document-scanner/corner-adjustment/corner-handle.tsx +43 -0
- package/src/custom/document-scanner/corner-adjustment/hooks/use-corner-drag.ts +85 -0
- package/src/custom/document-scanner/corner-adjustment/index.tsx +125 -0
- package/src/custom/document-scanner/corner-adjustment/types.ts +53 -0
- package/src/custom/document-scanner/corner-adjustment/utils/clip-path.ts +22 -0
- package/src/custom/document-scanner/corner-adjustment/zoom-magnifier.tsx +115 -0
- package/src/custom/document-scanner/hooks/use-document-capture.ts +81 -0
- package/src/custom/document-scanner/hooks/use-document-scanner.ts +80 -0
- package/src/custom/document-scanner/hooks/use-perspective-crop.ts +38 -0
- package/src/custom/document-scanner/index.tsx +255 -0
- package/src/custom/document-scanner/lib.ts +407 -0
- package/src/custom/document-scanner/types.ts +205 -0
- package/src/custom/document-scanner/utils/perspective-correction.ts +139 -0
- package/src/custom/document-viewer/controllers.tsx +98 -0
- package/src/custom/document-viewer/index.tsx +43 -0
- package/src/custom/document-viewer/renderers/image.tsx +37 -0
- package/src/custom/document-viewer/renderers/index.tsx +2 -0
- package/src/custom/document-viewer/renderers/pdf.tsx +105 -0
- package/src/custom/email-input/domains.json +159 -0
- package/src/custom/email-input/email.tsx +229 -0
- package/src/custom/email-input/index.tsx +4 -0
- package/src/custom/email-input/types.ts +104 -0
- package/src/custom/file-uploader.tsx +541 -0
- package/src/custom/filter-component/fields/async-select.tsx +33 -0
- package/src/custom/filter-component/fields/date.tsx +60 -0
- package/src/custom/filter-component/fields/multi-select.tsx +30 -0
- package/src/custom/filter-component/index.tsx +217 -0
- package/src/custom/image-canvas.tsx +260 -0
- package/src/custom/json-editor.tsx +22 -0
- package/src/custom/master-data-grid/components/dialogs/column-settings-dialog.tsx +100 -0
- package/src/custom/master-data-grid/components/dialogs/index.ts +1 -0
- package/src/custom/master-data-grid/components/filters/client-filter.tsx +368 -0
- package/src/custom/master-data-grid/components/filters/filter-input.tsx +256 -0
- package/src/custom/master-data-grid/components/filters/index.ts +3 -0
- package/src/custom/master-data-grid/components/filters/inline-column-filter.tsx +233 -0
- package/src/custom/master-data-grid/components/filters/multi-filter-dialog.tsx +90 -0
- package/src/custom/master-data-grid/components/filters/server-filter.tsx +255 -0
- package/src/custom/master-data-grid/components/master-data-grid.tsx +472 -0
- package/src/custom/master-data-grid/components/pagination/index.ts +1 -0
- package/src/custom/master-data-grid/components/pagination/pagination.tsx +178 -0
- package/src/custom/master-data-grid/components/table/cell-renderer.tsx +634 -0
- package/src/custom/master-data-grid/components/table/header-cell.tsx +162 -0
- package/src/custom/master-data-grid/components/table/index.ts +4 -0
- package/src/custom/master-data-grid/components/table/table-body-renderer.tsx +113 -0
- package/src/custom/master-data-grid/components/table/virtual-body.tsx +138 -0
- package/src/custom/master-data-grid/components/toolbar/index.ts +1 -0
- package/src/custom/master-data-grid/components/toolbar/toolbar.tsx +314 -0
- package/src/custom/master-data-grid/hooks/index.ts +3 -0
- package/src/custom/master-data-grid/hooks/use-columns.tsx +332 -0
- package/src/custom/master-data-grid/hooks/use-editing.ts +106 -0
- package/src/custom/master-data-grid/hooks/use-table-state-reducer.ts +157 -0
- package/src/custom/master-data-grid/hooks/use-table-state.ts +31 -0
- package/src/custom/master-data-grid/index.ts +16 -0
- package/src/custom/master-data-grid/types.ts +466 -0
- package/src/custom/master-data-grid/utils/column-generator.tsx +306 -0
- package/src/custom/master-data-grid/utils/export-utils.ts +67 -0
- package/src/custom/master-data-grid/utils/filter-fns.ts +290 -0
- package/src/custom/master-data-grid/utils/index.ts +8 -0
- package/src/custom/master-data-grid/utils/pinning-utils.ts +88 -0
- package/src/custom/master-data-grid/utils/translation-utils.ts +42 -0
- package/src/custom/multi-select.tsx +432 -0
- package/src/custom/password-input.tsx +194 -0
- package/src/custom/phone-input.tsx +172 -0
- package/src/custom/schema-form/custom/index.tsx +1 -0
- package/src/custom/schema-form/custom/label.tsx +53 -0
- package/src/custom/schema-form/fields/base-input-field.tsx +82 -0
- package/src/custom/schema-form/fields/field.tsx +67 -0
- package/src/custom/schema-form/fields/index.tsx +5 -0
- package/src/custom/schema-form/fields/object.tsx +12 -0
- package/src/custom/schema-form/fields/table-array/array-field-item.tsx +90 -0
- package/src/custom/schema-form/fields/table-array/array-field-template.tsx +115 -0
- package/src/custom/schema-form/index.tsx +259 -0
- package/src/custom/schema-form/templates/description.tsx +20 -0
- package/src/custom/schema-form/templates/index.tsx +2 -0
- package/src/custom/schema-form/templates/submit.tsx +32 -0
- package/src/custom/schema-form/types.ts +64 -0
- package/src/custom/schema-form/utils/index.ts +4 -0
- package/src/custom/schema-form/utils/schema-dependency.ts +655 -0
- package/src/custom/schema-form/utils/schemas.ts +289 -0
- package/src/custom/schema-form/utils/validation.ts +23 -0
- package/src/custom/schema-form/widgets/boolean.tsx +77 -0
- package/src/custom/schema-form/widgets/combobox.tsx +274 -0
- package/src/custom/schema-form/widgets/date.tsx +59 -0
- package/src/custom/schema-form/widgets/email.tsx +34 -0
- package/src/custom/schema-form/widgets/index.tsx +10 -0
- package/src/custom/schema-form/widgets/password.tsx +40 -0
- package/src/custom/schema-form/widgets/phone.tsx +40 -0
- package/src/custom/schema-form/widgets/select.tsx +105 -0
- package/src/custom/schema-form/widgets/selectable.tsx +25 -0
- package/src/custom/schema-form/widgets/string-array.tsx +296 -0
- package/src/custom/schema-form/widgets/url.tsx +56 -0
- package/src/custom/section-layout-v2.tsx +212 -0
- package/src/custom/select-tabs.tsx +109 -0
- package/src/custom/selectable.tsx +316 -0
- package/src/custom/stepper.tsx +236 -0
- package/src/custom/tab-layout.tsx +213 -0
- package/src/custom/tanstack-table/fields/index.tsx +12 -0
- package/src/custom/tanstack-table/fields/tanstack-table-action-dialogs.tsx +89 -0
- package/src/custom/tanstack-table/fields/tanstack-table-column-header.tsx +66 -0
- package/src/custom/tanstack-table/fields/tanstack-table-filter-date.tsx +180 -0
- package/src/custom/tanstack-table/fields/tanstack-table-filter-faceted.tsx +158 -0
- package/src/custom/tanstack-table/fields/tanstack-table-filter-text.tsx +76 -0
- package/src/custom/tanstack-table/fields/tanstack-table-pagination.tsx +136 -0
- package/src/custom/tanstack-table/fields/tanstack-table-plain-table.tsx +142 -0
- package/src/custom/tanstack-table/fields/tanstack-table-row-actions-confirmation.tsx +77 -0
- package/src/custom/tanstack-table/fields/tanstack-table-row-actions-custom-dialog.tsx +87 -0
- package/src/custom/tanstack-table/fields/tanstack-table-row-actions.tsx +151 -0
- package/src/custom/tanstack-table/fields/tanstack-table-table-actions-custom-dialog.tsx +88 -0
- package/src/custom/tanstack-table/fields/tanstack-table-table-actions-schemaform-dialog.tsx +47 -0
- package/src/custom/tanstack-table/fields/tanstack-table-toolbar.tsx +143 -0
- package/src/custom/tanstack-table/fields/tanstack-table-view-options.tsx +171 -0
- package/src/custom/tanstack-table/index.tsx +244 -0
- package/src/custom/tanstack-table/types/index.ts +328 -0
- package/src/custom/tanstack-table/utils/cell-with-actions.tsx +21 -0
- package/src/custom/tanstack-table/utils/column-names.ts +26 -0
- package/src/custom/tanstack-table/utils/columns-by-row-data.tsx +312 -0
- package/src/custom/tanstack-table/utils/editable-columns-by-row-data.tsx +219 -0
- package/src/custom/tanstack-table/utils/faceted-boolean-options.tsx +22 -0
- package/src/custom/tanstack-table/utils/index.tsx +10 -0
- package/src/custom/tanstack-table/utils/pinning-styles.ts +57 -0
- package/src/custom/tanstack-table/utils/table.tsx +83 -0
- package/src/custom/tanstack-table/utils/test-conditions.ts +17 -0
- package/src/custom/timeline.tsx +208 -0
- package/src/custom/tree.tsx +200 -0
- package/src/custom/tscanify/browser.ts +66 -0
- package/src/custom/tscanify/index.ts +51 -0
- package/src/custom/tscanify/tscanify-browser.ts +522 -0
- package/src/custom/tscanify/tscanify.ts +262 -0
- package/src/custom/tscanify/types.ts +22 -0
- package/src/custom/webcam.tsx +737 -0
- package/src/hooks/.gitkeep +0 -0
- package/src/hooks/use-callback-ref.ts +27 -0
- package/src/hooks/use-controllable-state.ts +67 -0
- package/src/hooks/use-debounce.ts +19 -0
- package/src/hooks/use-is-visible.ts +23 -0
- package/src/hooks/use-media-query.ts +21 -0
- package/src/hooks/use-mobile.ts +21 -0
- package/src/hooks/use-on-window-resize.ts +15 -0
- package/src/hooks/use-scroll.tsx +22 -0
- package/src/lib/utils.ts +61 -0
- package/src/lib/zod.ts +2 -0
- package/src/styles/core.css +57 -0
- package/src/styles/globals.css +130 -0
- package/src/test/email-input.test.tsx +217 -0
- package/src/test/password-input.test.tsx +92 -0
- package/src/test/select-tabs.test.tsx +302 -0
- package/src/test/selectable.test.tsx +1093 -0
- package/tsconfig.json +13 -0
- package/tsconfig.lint.json +8 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { Header, Cell } from "@tanstack/react-table";
|
|
2
|
+
import type { CSSProperties } from "react";
|
|
3
|
+
|
|
4
|
+
export function getPinningHeaderStyles<TData>(
|
|
5
|
+
header: Header<TData, unknown>
|
|
6
|
+
): CSSProperties {
|
|
7
|
+
const pinned = header.column.getIsPinned();
|
|
8
|
+
const isActions = header.column.id === "actions";
|
|
9
|
+
return {
|
|
10
|
+
left: pinned === "left" ? `${header.column.getStart("left")}px` : undefined,
|
|
11
|
+
right:
|
|
12
|
+
pinned === "right" ? `${header.column.getAfter("right")}px` : undefined,
|
|
13
|
+
position: pinned ? "sticky" : "relative",
|
|
14
|
+
zIndex: pinned ? 1 : 0,
|
|
15
|
+
maxWidth: isActions ? "40px" : undefined,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function getPinningHeaderClassNames<TData>(
|
|
20
|
+
header: Header<TData, unknown>
|
|
21
|
+
): string {
|
|
22
|
+
const pinned = header.column.getIsPinned();
|
|
23
|
+
const classes: string[] = [];
|
|
24
|
+
|
|
25
|
+
if (pinned) {
|
|
26
|
+
classes.push("bg-background");
|
|
27
|
+
}
|
|
28
|
+
if (pinned === "left") {
|
|
29
|
+
classes.push("shadow-[2px_0_4px_-2px_rgba(0,0,0,0.1)]");
|
|
30
|
+
}
|
|
31
|
+
if (pinned === "right") {
|
|
32
|
+
classes.push("shadow-[-2px_0_4px_-2px_rgba(0,0,0,0.1)]");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return classes.join(" ");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function getPinningCellStyles<TData>(
|
|
39
|
+
cell: Cell<TData, unknown>
|
|
40
|
+
): CSSProperties {
|
|
41
|
+
const pinned = cell.column.getIsPinned();
|
|
42
|
+
const isActions = cell.column.id === "actions";
|
|
43
|
+
if (isActions) {
|
|
44
|
+
return {
|
|
45
|
+
width: "40px",
|
|
46
|
+
minWidth: "40px",
|
|
47
|
+
maxWidth: "40px",
|
|
48
|
+
left: pinned === "left" ? `${cell.column.getStart("left")}px` : undefined,
|
|
49
|
+
right:
|
|
50
|
+
pinned === "right" ? `${cell.column.getAfter("right")}px` : undefined,
|
|
51
|
+
position: pinned ? "sticky" : "relative",
|
|
52
|
+
zIndex: pinned ? 1 : 0,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
width: cell.column.getSize(),
|
|
57
|
+
minWidth: pinned
|
|
58
|
+
? cell.column.getSize()
|
|
59
|
+
: cell.column.columnDef.minSize || cell.column.getSize(),
|
|
60
|
+
maxWidth: pinned
|
|
61
|
+
? cell.column.columnDef.maxSize
|
|
62
|
+
: cell.column.columnDef.maxSize || cell.column.getSize(),
|
|
63
|
+
left: pinned === "left" ? `${cell.column.getStart("left")}px` : undefined,
|
|
64
|
+
right:
|
|
65
|
+
pinned === "right" ? `${cell.column.getAfter("right")}px` : undefined,
|
|
66
|
+
position: pinned ? "sticky" : "relative",
|
|
67
|
+
zIndex: pinned ? 1 : 0,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function getPinningCellClassNames<TData>(
|
|
72
|
+
cell: Cell<TData, unknown>
|
|
73
|
+
): string {
|
|
74
|
+
const pinned = cell.column.getIsPinned();
|
|
75
|
+
const classes: string[] = [];
|
|
76
|
+
|
|
77
|
+
if (pinned) {
|
|
78
|
+
classes.push("bg-background", "overflow-hidden", "text-ellipsis");
|
|
79
|
+
}
|
|
80
|
+
if (pinned === "left") {
|
|
81
|
+
classes.push("shadow-[2px_0_4px_-2px_rgba(0,0,0,0.1)]");
|
|
82
|
+
}
|
|
83
|
+
if (pinned === "right") {
|
|
84
|
+
classes.push("shadow-[-2px_0_4px_-2px_rgba(0,0,0,0.1)]");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return classes.join(" ");
|
|
88
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { Column } from "@tanstack/react-table";
|
|
2
|
+
import type { MasterDataGridResources } from "../types";
|
|
3
|
+
|
|
4
|
+
export function getTranslations(
|
|
5
|
+
key: string,
|
|
6
|
+
t?: MasterDataGridResources,
|
|
7
|
+
replacements?: Record<string, string>
|
|
8
|
+
): string {
|
|
9
|
+
let text = t?.[key] ?? key;
|
|
10
|
+
|
|
11
|
+
if (replacements) {
|
|
12
|
+
Object.entries(replacements).forEach(([placeholder, value]) => {
|
|
13
|
+
text = text.replace(`{${placeholder}}`, value);
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return text;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function getColumnName<TData>(
|
|
21
|
+
column: Column<TData> | string,
|
|
22
|
+
t?: MasterDataGridResources,
|
|
23
|
+
fallback?: string
|
|
24
|
+
): string {
|
|
25
|
+
if (typeof column === "string") {
|
|
26
|
+
const translationKey = `column.${column}`;
|
|
27
|
+
const translated = getTranslations(translationKey, t);
|
|
28
|
+
if (translated !== translationKey) {
|
|
29
|
+
return translated;
|
|
30
|
+
}
|
|
31
|
+
return fallback ?? column;
|
|
32
|
+
}
|
|
33
|
+
const translationKey = `column.${column.id}`;
|
|
34
|
+
if (t?.[translationKey]) {
|
|
35
|
+
return t[translationKey];
|
|
36
|
+
}
|
|
37
|
+
const header = column.columnDef.header;
|
|
38
|
+
if (typeof header === "string") {
|
|
39
|
+
return header;
|
|
40
|
+
}
|
|
41
|
+
return column.id;
|
|
42
|
+
}
|
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/components/multi-select.tsx
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
6
|
+
import {
|
|
7
|
+
CheckIcon,
|
|
8
|
+
XCircle,
|
|
9
|
+
ChevronDown,
|
|
10
|
+
XIcon,
|
|
11
|
+
WandSparkles,
|
|
12
|
+
} from "lucide-react";
|
|
13
|
+
|
|
14
|
+
import { cn } from "@repo/ayasofyazilim-ui/lib/utils";
|
|
15
|
+
import { Separator } from "@repo/ayasofyazilim-ui/components/separator";
|
|
16
|
+
import { Button } from "@repo/ayasofyazilim-ui/components/button";
|
|
17
|
+
import { Badge } from "@repo/ayasofyazilim-ui/components/badge";
|
|
18
|
+
import {
|
|
19
|
+
Popover,
|
|
20
|
+
PopoverContent,
|
|
21
|
+
PopoverTrigger,
|
|
22
|
+
} from "@repo/ayasofyazilim-ui/components/popover";
|
|
23
|
+
import {
|
|
24
|
+
Command,
|
|
25
|
+
CommandEmpty,
|
|
26
|
+
CommandGroup,
|
|
27
|
+
CommandInput,
|
|
28
|
+
CommandItem,
|
|
29
|
+
CommandList,
|
|
30
|
+
CommandSeparator,
|
|
31
|
+
} from "@repo/ayasofyazilim-ui/components/command";
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Variants for the multi-select component to handle different styles.
|
|
35
|
+
* Uses class-variance-authority (cva) to define different styles based on "variant" prop.
|
|
36
|
+
*/
|
|
37
|
+
const multiSelectVariants = cva(
|
|
38
|
+
"m-1 transition ease-in-out delay-150 hover:-translate-y-1 hover:scale-110 duration-300",
|
|
39
|
+
{
|
|
40
|
+
variants: {
|
|
41
|
+
variant: {
|
|
42
|
+
default:
|
|
43
|
+
"border-foreground/10 text-foreground bg-card hover:bg-card/80",
|
|
44
|
+
secondary:
|
|
45
|
+
"border-foreground/10 bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
|
46
|
+
destructive:
|
|
47
|
+
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
|
|
48
|
+
inverted: "inverted",
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
defaultVariants: {
|
|
52
|
+
variant: "default",
|
|
53
|
+
},
|
|
54
|
+
}
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Props for MultiSelect component
|
|
59
|
+
*/
|
|
60
|
+
export interface MultiSelectProps
|
|
61
|
+
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
62
|
+
VariantProps<typeof multiSelectVariants> {
|
|
63
|
+
/**
|
|
64
|
+
* Animation duration in seconds for the visual effects (e.g., bouncing badges).
|
|
65
|
+
* Optional, defaults to 0 (no animation).
|
|
66
|
+
*/
|
|
67
|
+
animation?: number;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* If true, renders the multi-select component as a child of another component.
|
|
71
|
+
* Optional, defaults to false.
|
|
72
|
+
*/
|
|
73
|
+
asChild?: boolean;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Additional class names to apply custom styles to the multi-select component.
|
|
77
|
+
* Optional, can be used to add custom styles.
|
|
78
|
+
*/
|
|
79
|
+
className?: string;
|
|
80
|
+
|
|
81
|
+
/** The default selected values when the component mounts. */
|
|
82
|
+
defaultValue?: string[];
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Maximum number of items to display. Extra selected items will be summarized.
|
|
86
|
+
* Optional, defaults to 3.
|
|
87
|
+
*/
|
|
88
|
+
maxCount?: number;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The modality of the popover. When set to true, interaction with outside elements
|
|
92
|
+
* will be disabled and only popover content will be visible to screen readers.
|
|
93
|
+
* Optional, defaults to false.
|
|
94
|
+
*/
|
|
95
|
+
modalPopover?: boolean;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Callback function triggered when the selected values change.
|
|
99
|
+
* Receives an array of the new selected values.
|
|
100
|
+
*/
|
|
101
|
+
onValueChange?: (value: string[]) => void;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* An array of option objects to be displayed in the multi-select component.
|
|
105
|
+
* Each option object has a label, value, and an optional icon.
|
|
106
|
+
*/
|
|
107
|
+
options: {
|
|
108
|
+
/** Optional icon component to display alongside the option. */
|
|
109
|
+
icon?: React.ComponentType<{ className?: string }>;
|
|
110
|
+
/** The text to display for the option. */
|
|
111
|
+
label: string;
|
|
112
|
+
/** The unique value associated with the option. */
|
|
113
|
+
value: string;
|
|
114
|
+
/** Optional badge component to display alongside the option. */
|
|
115
|
+
children?: React.ReactNode;
|
|
116
|
+
disabled?: boolean;
|
|
117
|
+
}[];
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Placeholder text to be displayed when no values are selected.
|
|
121
|
+
* Optional, defaults to "Select options".
|
|
122
|
+
*/
|
|
123
|
+
placeholder?: string;
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* selectAllLabel text to be displayed for select all option.
|
|
127
|
+
* Optional, defaults to "Select all".
|
|
128
|
+
*/
|
|
129
|
+
selectAllLabel?: string;
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* If true, the multi-select component will be disabled.
|
|
133
|
+
* Optional, defaults to false.
|
|
134
|
+
*/
|
|
135
|
+
disabled?: boolean;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export const MultiSelect = React.forwardRef<
|
|
139
|
+
HTMLButtonElement,
|
|
140
|
+
MultiSelectProps
|
|
141
|
+
>(
|
|
142
|
+
(
|
|
143
|
+
{
|
|
144
|
+
options,
|
|
145
|
+
onValueChange,
|
|
146
|
+
variant,
|
|
147
|
+
defaultValue = [],
|
|
148
|
+
placeholder = "Select options",
|
|
149
|
+
selectAllLabel = "Select all",
|
|
150
|
+
animation = 0,
|
|
151
|
+
maxCount = 3,
|
|
152
|
+
modalPopover = false,
|
|
153
|
+
className,
|
|
154
|
+
disabled,
|
|
155
|
+
...props
|
|
156
|
+
},
|
|
157
|
+
ref
|
|
158
|
+
) => {
|
|
159
|
+
const [selectedValues, setSelectedValues] =
|
|
160
|
+
React.useState<string[]>(defaultValue);
|
|
161
|
+
const previousDefaultValue = React.useRef<string[]>(defaultValue);
|
|
162
|
+
React.useEffect(() => {
|
|
163
|
+
if (
|
|
164
|
+
JSON.stringify(previousDefaultValue.current) !==
|
|
165
|
+
JSON.stringify(defaultValue)
|
|
166
|
+
) {
|
|
167
|
+
setSelectedValues(defaultValue);
|
|
168
|
+
previousDefaultValue.current = defaultValue;
|
|
169
|
+
}
|
|
170
|
+
}, [defaultValue]);
|
|
171
|
+
const [isPopoverOpen, setIsPopoverOpen] = React.useState(false);
|
|
172
|
+
const [isAnimating, setIsAnimating] = React.useState(false);
|
|
173
|
+
|
|
174
|
+
const handleInputKeyDown = (
|
|
175
|
+
event: React.KeyboardEvent<HTMLInputElement>
|
|
176
|
+
) => {
|
|
177
|
+
if (event.key === "Enter") {
|
|
178
|
+
setIsPopoverOpen(true);
|
|
179
|
+
} else if (event.key === "Backspace" && !event.currentTarget.value) {
|
|
180
|
+
const newSelectedValues = [...selectedValues];
|
|
181
|
+
newSelectedValues.pop();
|
|
182
|
+
setSelectedValues(newSelectedValues);
|
|
183
|
+
if (onValueChange) onValueChange(newSelectedValues);
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const toggleOption = (option: string) => {
|
|
188
|
+
const newSelectedValues = selectedValues.includes(option)
|
|
189
|
+
? selectedValues.filter((value) => value !== option)
|
|
190
|
+
: [...selectedValues, option];
|
|
191
|
+
setSelectedValues(newSelectedValues);
|
|
192
|
+
if (onValueChange) onValueChange(newSelectedValues);
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const handleClear = () => {
|
|
196
|
+
setSelectedValues(
|
|
197
|
+
selectedValues.filter(
|
|
198
|
+
(value) => !options.map((o) => !o.disabled && o.value).includes(value)
|
|
199
|
+
)
|
|
200
|
+
);
|
|
201
|
+
if (onValueChange) onValueChange([]);
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const handleTogglePopover = () => {
|
|
205
|
+
setIsPopoverOpen((prev) => !prev);
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const clearExtraOptions = () => {
|
|
209
|
+
const newSelectedValues = selectedValues.slice(0, maxCount);
|
|
210
|
+
setSelectedValues(newSelectedValues);
|
|
211
|
+
if (onValueChange) onValueChange(newSelectedValues);
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const toggleAll = () => {
|
|
215
|
+
if (selectedValues.length === options.length) {
|
|
216
|
+
handleClear();
|
|
217
|
+
} else {
|
|
218
|
+
const allValues = options.map((option) => option.value);
|
|
219
|
+
setSelectedValues(allValues);
|
|
220
|
+
if (onValueChange) onValueChange(allValues);
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
return (
|
|
225
|
+
<Popover
|
|
226
|
+
open={isPopoverOpen}
|
|
227
|
+
onOpenChange={setIsPopoverOpen}
|
|
228
|
+
modal={modalPopover}
|
|
229
|
+
>
|
|
230
|
+
<PopoverTrigger asChild className="w-full">
|
|
231
|
+
<Button
|
|
232
|
+
ref={ref}
|
|
233
|
+
data-testid={props.id}
|
|
234
|
+
type="button"
|
|
235
|
+
disabled={disabled}
|
|
236
|
+
onClick={handleTogglePopover}
|
|
237
|
+
className={cn(
|
|
238
|
+
"flex w-full p-1 rounded-md border min-h-9 h-auto items-center justify-between bg-inherit hover:bg-inherit",
|
|
239
|
+
className
|
|
240
|
+
)}
|
|
241
|
+
>
|
|
242
|
+
{selectedValues.length > 0 ? (
|
|
243
|
+
<div className="flex justify-between items-center w-full">
|
|
244
|
+
<div className="flex flex-wrap items-center">
|
|
245
|
+
{selectedValues.slice(0, maxCount).map((value) => {
|
|
246
|
+
const option = options.find((o) => o.value === value);
|
|
247
|
+
const IconComponent = option?.icon;
|
|
248
|
+
return (
|
|
249
|
+
<Badge
|
|
250
|
+
key={value}
|
|
251
|
+
className={cn(
|
|
252
|
+
isAnimating ? "animate-bounce" : "",
|
|
253
|
+
multiSelectVariants({ variant })
|
|
254
|
+
)}
|
|
255
|
+
style={{ animationDuration: `${animation}s` }}
|
|
256
|
+
>
|
|
257
|
+
{IconComponent && (
|
|
258
|
+
<IconComponent className="h-4 w-4 mr-2" />
|
|
259
|
+
)}
|
|
260
|
+
{option?.label}
|
|
261
|
+
{!option?.disabled && (
|
|
262
|
+
<XCircle
|
|
263
|
+
className="ml-2 h-4 w-4 cursor-pointer"
|
|
264
|
+
onClick={(event) => {
|
|
265
|
+
event.stopPropagation();
|
|
266
|
+
toggleOption(value);
|
|
267
|
+
}}
|
|
268
|
+
/>
|
|
269
|
+
)}
|
|
270
|
+
</Badge>
|
|
271
|
+
);
|
|
272
|
+
})}
|
|
273
|
+
{selectedValues.length > maxCount && (
|
|
274
|
+
<Badge
|
|
275
|
+
className={cn(
|
|
276
|
+
"bg-transparent text-foreground border-foreground/1 hover:bg-transparent",
|
|
277
|
+
isAnimating ? "animate-bounce" : "",
|
|
278
|
+
multiSelectVariants({ variant })
|
|
279
|
+
)}
|
|
280
|
+
style={{ animationDuration: `${animation}s` }}
|
|
281
|
+
>
|
|
282
|
+
{`+ ${selectedValues.length - maxCount} more`}
|
|
283
|
+
<XCircle
|
|
284
|
+
className="ml-2 h-4 w-4 cursor-pointer"
|
|
285
|
+
onClick={(event) => {
|
|
286
|
+
event.stopPropagation();
|
|
287
|
+
clearExtraOptions();
|
|
288
|
+
}}
|
|
289
|
+
/>
|
|
290
|
+
</Badge>
|
|
291
|
+
)}
|
|
292
|
+
</div>
|
|
293
|
+
<div className="flex items-center justify-between">
|
|
294
|
+
{options.filter((x) => x.disabled).length !==
|
|
295
|
+
options.length && (
|
|
296
|
+
<XIcon
|
|
297
|
+
className="h-4 mx-2 cursor-pointer text-muted-foreground"
|
|
298
|
+
onClick={(event) => {
|
|
299
|
+
event.stopPropagation();
|
|
300
|
+
handleClear();
|
|
301
|
+
}}
|
|
302
|
+
/>
|
|
303
|
+
)}
|
|
304
|
+
<Separator
|
|
305
|
+
orientation="vertical"
|
|
306
|
+
className="flex min-h-6 h-full"
|
|
307
|
+
/>
|
|
308
|
+
<ChevronDown className="h-4 mx-2 cursor-pointer text-muted-foreground" />
|
|
309
|
+
</div>
|
|
310
|
+
</div>
|
|
311
|
+
) : (
|
|
312
|
+
<div className="flex items-center justify-between w-full mx-auto">
|
|
313
|
+
<span className="text-sm text-black mx-3 font-normal">
|
|
314
|
+
{placeholder}
|
|
315
|
+
</span>
|
|
316
|
+
<ChevronDown className="h-4 cursor-pointer text-muted-foreground mx-2" />
|
|
317
|
+
</div>
|
|
318
|
+
)}
|
|
319
|
+
</Button>
|
|
320
|
+
</PopoverTrigger>
|
|
321
|
+
<PopoverContent
|
|
322
|
+
className="p-0"
|
|
323
|
+
onEscapeKeyDown={() => setIsPopoverOpen(false)}
|
|
324
|
+
>
|
|
325
|
+
<Command className="w-full">
|
|
326
|
+
<CommandInput
|
|
327
|
+
data-testid={`${props.id}_search`}
|
|
328
|
+
placeholder="Search..."
|
|
329
|
+
onKeyDown={handleInputKeyDown}
|
|
330
|
+
/>
|
|
331
|
+
<CommandList>
|
|
332
|
+
<CommandEmpty>No results found.</CommandEmpty>
|
|
333
|
+
<CommandGroup>
|
|
334
|
+
<CommandItem
|
|
335
|
+
data-testid={`${props.id}_select_all`}
|
|
336
|
+
key="all"
|
|
337
|
+
onSelect={toggleAll}
|
|
338
|
+
className="cursor-pointer"
|
|
339
|
+
disabled={
|
|
340
|
+
options.filter((x) => x.disabled).length === options.length
|
|
341
|
+
}
|
|
342
|
+
>
|
|
343
|
+
<div
|
|
344
|
+
className={cn(
|
|
345
|
+
"mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary",
|
|
346
|
+
selectedValues.length === options.length
|
|
347
|
+
? "bg-primary text-primary-foreground"
|
|
348
|
+
: "opacity-50 [&_svg]:invisible"
|
|
349
|
+
)}
|
|
350
|
+
>
|
|
351
|
+
<CheckIcon className="h-4 w-4" />
|
|
352
|
+
</div>
|
|
353
|
+
<span>({selectAllLabel})</span>
|
|
354
|
+
</CommandItem>
|
|
355
|
+
{options.map((option) => {
|
|
356
|
+
const isSelected = selectedValues.includes(option.value);
|
|
357
|
+
return (
|
|
358
|
+
<CommandItem
|
|
359
|
+
data-testid={`${props.id}_${option.value}`}
|
|
360
|
+
key={option.value}
|
|
361
|
+
onSelect={() => toggleOption(option.value)}
|
|
362
|
+
className="cursor-pointer"
|
|
363
|
+
disabled={option.disabled}
|
|
364
|
+
>
|
|
365
|
+
<div
|
|
366
|
+
className={cn(
|
|
367
|
+
"mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary",
|
|
368
|
+
isSelected
|
|
369
|
+
? "bg-primary text-primary-foreground"
|
|
370
|
+
: "opacity-50 [&_svg]:invisible"
|
|
371
|
+
)}
|
|
372
|
+
>
|
|
373
|
+
<CheckIcon className="h-4 w-4" />
|
|
374
|
+
</div>
|
|
375
|
+
{option.icon && (
|
|
376
|
+
<option.icon className="mr-2 h-4 w-4 text-muted-foreground" />
|
|
377
|
+
)}
|
|
378
|
+
<span>{option.label}</span>
|
|
379
|
+
{option.children}
|
|
380
|
+
</CommandItem>
|
|
381
|
+
);
|
|
382
|
+
})}
|
|
383
|
+
</CommandGroup>
|
|
384
|
+
<CommandSeparator />
|
|
385
|
+
<CommandGroup>
|
|
386
|
+
<div className="flex items-center justify-between">
|
|
387
|
+
{selectedValues.length > 0 && (
|
|
388
|
+
<>
|
|
389
|
+
<CommandItem
|
|
390
|
+
data-testid={`${props.id}_clear`}
|
|
391
|
+
onSelect={handleClear}
|
|
392
|
+
className="flex-1 justify-center cursor-pointer"
|
|
393
|
+
disabled={
|
|
394
|
+
options.filter((x) => x.disabled).length ===
|
|
395
|
+
options.length
|
|
396
|
+
}
|
|
397
|
+
>
|
|
398
|
+
Clear
|
|
399
|
+
</CommandItem>
|
|
400
|
+
<Separator
|
|
401
|
+
orientation="vertical"
|
|
402
|
+
className="flex min-h-6 h-full"
|
|
403
|
+
/>
|
|
404
|
+
</>
|
|
405
|
+
)}
|
|
406
|
+
<CommandItem
|
|
407
|
+
data-testid={`${props.id}_close`}
|
|
408
|
+
onSelect={() => setIsPopoverOpen(false)}
|
|
409
|
+
className="flex-1 justify-center cursor-pointer max-w-full"
|
|
410
|
+
>
|
|
411
|
+
Close
|
|
412
|
+
</CommandItem>
|
|
413
|
+
</div>
|
|
414
|
+
</CommandGroup>
|
|
415
|
+
</CommandList>
|
|
416
|
+
</Command>
|
|
417
|
+
</PopoverContent>
|
|
418
|
+
{animation > 0 && selectedValues.length > 0 && (
|
|
419
|
+
<WandSparkles
|
|
420
|
+
className={cn(
|
|
421
|
+
"cursor-pointer my-2 text-foreground bg-background w-3 h-3",
|
|
422
|
+
isAnimating ? "" : "text-muted-foreground"
|
|
423
|
+
)}
|
|
424
|
+
onClick={() => setIsAnimating(!isAnimating)}
|
|
425
|
+
/>
|
|
426
|
+
)}
|
|
427
|
+
</Popover>
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
);
|
|
431
|
+
|
|
432
|
+
MultiSelect.displayName = "MultiSelect";
|