@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,212 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, {
|
|
4
|
+
createContext,
|
|
5
|
+
ElementType,
|
|
6
|
+
useContext,
|
|
7
|
+
useEffect,
|
|
8
|
+
useMemo,
|
|
9
|
+
useState,
|
|
10
|
+
} from "react";
|
|
11
|
+
import { Button } from "@repo/ayasofyazilim-ui/components/button";
|
|
12
|
+
import { Skeleton } from "@repo/ayasofyazilim-ui/components/skeleton";
|
|
13
|
+
import { cn } from "@repo/ayasofyazilim-ui/lib/utils";
|
|
14
|
+
|
|
15
|
+
export interface ISection {
|
|
16
|
+
children?: React.ReactNode;
|
|
17
|
+
className?: string;
|
|
18
|
+
disabled?: boolean;
|
|
19
|
+
id: string;
|
|
20
|
+
link?: string;
|
|
21
|
+
name: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ISectionLayoutNavbarProps {
|
|
25
|
+
activeSectionId: string;
|
|
26
|
+
linkElement?: ElementType;
|
|
27
|
+
onSectionChange?: (sectionId: string) => void;
|
|
28
|
+
sections: Array<ISection>;
|
|
29
|
+
vertical?: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Renders a navigation bar for a section layout.
|
|
34
|
+
*
|
|
35
|
+
* @param {Array<ISection>} props.sections - The sections to be rendered in the navigation bar.
|
|
36
|
+
* @param {string} props.activeSectionId - The ID of the active section.
|
|
37
|
+
* @param {(sectionId: string) => void} [props.onSectionChange] - The function to be called when a section is clicked.
|
|
38
|
+
* @param {React.ElementType} [props.linkElement] - The element to be used for the section links. (default: Button)
|
|
39
|
+
* @param {boolean} [props.vertical] - Whether the navigation bar should be rendered vertically.
|
|
40
|
+
* @return {React.ReactNode} The rendered navigation bar.
|
|
41
|
+
*/
|
|
42
|
+
export function SectionLayoutNavbar({
|
|
43
|
+
sections,
|
|
44
|
+
activeSectionId,
|
|
45
|
+
onSectionChange,
|
|
46
|
+
linkElement,
|
|
47
|
+
vertical,
|
|
48
|
+
}: ISectionLayoutNavbarProps) {
|
|
49
|
+
const LinkElement = linkElement || Button;
|
|
50
|
+
return (
|
|
51
|
+
<nav
|
|
52
|
+
className={cn(
|
|
53
|
+
"flex gap-4 text-sm text-center md:text-left p-3 ",
|
|
54
|
+
vertical
|
|
55
|
+
? "flex-col border-b md:border-b-0 md:border-r overflow-auto min-w-full md:min-w-60 items-center md:items-start"
|
|
56
|
+
: "flex-col md:flex-row border-b"
|
|
57
|
+
)}
|
|
58
|
+
>
|
|
59
|
+
{sections.map((section) => (
|
|
60
|
+
<LinkElement
|
|
61
|
+
className={cn(
|
|
62
|
+
activeSectionId === section.id
|
|
63
|
+
? "font-semibold text-primary hover:no-underline m-0 p-0 h-auto justify-start"
|
|
64
|
+
: "font-normal text-muted-foreground hover:no-underline m-0 p-0 h-auto justify-start",
|
|
65
|
+
section.disabled
|
|
66
|
+
? "cursor-not-allowed opacity-50"
|
|
67
|
+
: "cursor-pointer",
|
|
68
|
+
section.className
|
|
69
|
+
)}
|
|
70
|
+
data-active={activeSectionId === section.id}
|
|
71
|
+
href={section.link || "#"}
|
|
72
|
+
onClick={() => {
|
|
73
|
+
if (section.disabled) return;
|
|
74
|
+
if (!linkElement && onSectionChange) {
|
|
75
|
+
onSectionChange(section.id);
|
|
76
|
+
}
|
|
77
|
+
}}
|
|
78
|
+
key={section.id}
|
|
79
|
+
tabIndex={section.disabled ? -1 : 0}
|
|
80
|
+
variant="link"
|
|
81
|
+
>
|
|
82
|
+
{section.children && section.children}
|
|
83
|
+
{!section.children && section.name}
|
|
84
|
+
</LinkElement>
|
|
85
|
+
))}
|
|
86
|
+
</nav>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface ISectionContentProps {
|
|
91
|
+
children: React.ReactNode;
|
|
92
|
+
className?: string;
|
|
93
|
+
sectionId: string;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Renders the content of a section layout based on the active section ID. It must be inside of SectionLayout component.
|
|
97
|
+
*
|
|
98
|
+
* @param {string} sectionId - The ID of the section to render.
|
|
99
|
+
* @param {React.ReactNode} children - The content to be rendered inside the section layout.
|
|
100
|
+
* @param {string} className - Additional CSS classes for styling.
|
|
101
|
+
* @return {JSX.Element | null} The rendered section layout content or null if the section is not active.
|
|
102
|
+
*/
|
|
103
|
+
export function SectionLayoutContent({
|
|
104
|
+
sectionId,
|
|
105
|
+
children,
|
|
106
|
+
className,
|
|
107
|
+
}: ISectionContentProps) {
|
|
108
|
+
const context = useContext(SectionLayoutContext);
|
|
109
|
+
const { activeSectionId } = context;
|
|
110
|
+
if (activeSectionId !== sectionId) return null;
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<div
|
|
114
|
+
id={`section-${sectionId}`}
|
|
115
|
+
className={cn("w-full p-5 overflow-auto h-full flex-1", className)}
|
|
116
|
+
>
|
|
117
|
+
{children}
|
|
118
|
+
</div>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const SectionLayoutContext = createContext({
|
|
123
|
+
activeSectionId: "",
|
|
124
|
+
});
|
|
125
|
+
export interface ISectionLayoutProps {
|
|
126
|
+
children: React.ReactNode;
|
|
127
|
+
defaultActiveSectionId?: string;
|
|
128
|
+
linkElement?: ElementType;
|
|
129
|
+
sections: Array<ISection>;
|
|
130
|
+
vertical?: boolean;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Renders a section layout component with a navigation bar and content area.
|
|
135
|
+
*
|
|
136
|
+
* @param {React.ReactNode} children - The content to be rendered inside the section layout.
|
|
137
|
+
* @param {Array<ISection>} sections - The sections to be rendered in the navigation bar.
|
|
138
|
+
* @param {string} [defaultActiveSectionId] - The ID of the section to be active by default.
|
|
139
|
+
* @param {ElementType} [linkElement] - The element to be used for the section links. (default: Button)
|
|
140
|
+
* @param {boolean} [vertical] - Whether the layout should be rendered vertically.
|
|
141
|
+
* @return {JSX.Element} The rendered section layout component.
|
|
142
|
+
*/
|
|
143
|
+
export function SectionLayout({
|
|
144
|
+
children,
|
|
145
|
+
sections,
|
|
146
|
+
defaultActiveSectionId,
|
|
147
|
+
linkElement,
|
|
148
|
+
vertical,
|
|
149
|
+
}: ISectionLayoutProps) {
|
|
150
|
+
const [activeSectionId, setActiveSectionId] = useState(
|
|
151
|
+
defaultActiveSectionId || sections?.[0]?.id
|
|
152
|
+
);
|
|
153
|
+
useEffect(() => {
|
|
154
|
+
if (linkElement && defaultActiveSectionId) {
|
|
155
|
+
setActiveSectionId(defaultActiveSectionId);
|
|
156
|
+
}
|
|
157
|
+
}, [defaultActiveSectionId]);
|
|
158
|
+
const contextValue = useMemo(
|
|
159
|
+
() => ({ activeSectionId: activeSectionId || "" }),
|
|
160
|
+
[activeSectionId]
|
|
161
|
+
);
|
|
162
|
+
return (
|
|
163
|
+
<SectionLayoutContext.Provider value={contextValue}>
|
|
164
|
+
<div
|
|
165
|
+
className={
|
|
166
|
+
vertical
|
|
167
|
+
? "flex flex-wrap md:flex-nowrap rounded-lg h-full overflow-hidden mb-5"
|
|
168
|
+
: "rounded-lg h-full overflow-hidden flex flex-col"
|
|
169
|
+
}
|
|
170
|
+
>
|
|
171
|
+
<SectionLayoutNavbar
|
|
172
|
+
sections={sections}
|
|
173
|
+
activeSectionId={activeSectionId || ""}
|
|
174
|
+
onSectionChange={setActiveSectionId}
|
|
175
|
+
linkElement={linkElement}
|
|
176
|
+
vertical={vertical}
|
|
177
|
+
/>
|
|
178
|
+
{children}
|
|
179
|
+
</div>
|
|
180
|
+
</SectionLayoutContext.Provider>
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export const SectionLayoutSkeleton = ({ vertical }: { vertical?: boolean }) => (
|
|
185
|
+
<div
|
|
186
|
+
className={
|
|
187
|
+
vertical
|
|
188
|
+
? "flex flex-wrap md:flex-nowrap rounded-lg h-full overflow-hidden"
|
|
189
|
+
: "rounded-lg h-full overflow-hidden flex flex-col"
|
|
190
|
+
}
|
|
191
|
+
>
|
|
192
|
+
<nav
|
|
193
|
+
className={cn(
|
|
194
|
+
"flex gap-4 text-sm text-center md:text-left p-5 ",
|
|
195
|
+
vertical
|
|
196
|
+
? "flex-col border-b md:border-b-0 md:border-r min-w-full md:min-w-60 items-center md:items-start"
|
|
197
|
+
: "flex-col md:flex-row border-b"
|
|
198
|
+
)}
|
|
199
|
+
>
|
|
200
|
+
<Skeleton className="h-6 w-full bg-gray-200" />
|
|
201
|
+
<Skeleton className="h-6 w-full bg-gray-200" />
|
|
202
|
+
<Skeleton className="h-6 w-full bg-gray-200" />
|
|
203
|
+
<Skeleton className="h-6 w-full bg-gray-200" />
|
|
204
|
+
<Skeleton className="h-6 w-full bg-gray-200" />
|
|
205
|
+
</nav>
|
|
206
|
+
<div className="w-full p-5 overflow-auto h-full flex-1">
|
|
207
|
+
<Skeleton className="h-40 w-full bg-gray-200 mb-2" />
|
|
208
|
+
<Skeleton className="h-40 w-full bg-gray-200 mb-2" />
|
|
209
|
+
<Skeleton className="h-40 w-full bg-gray-200 mb-2" />
|
|
210
|
+
</div>
|
|
211
|
+
</div>
|
|
212
|
+
);
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Circle, CircleCheckBig } from "lucide-react";
|
|
4
|
+
import React, {
|
|
5
|
+
createContext,
|
|
6
|
+
JSX,
|
|
7
|
+
useContext,
|
|
8
|
+
useMemo,
|
|
9
|
+
useState,
|
|
10
|
+
} from "react";
|
|
11
|
+
import { Button } from "@repo/ayasofyazilim-ui/components/button";
|
|
12
|
+
import { cn } from "@repo/ayasofyazilim-ui/lib/utils";
|
|
13
|
+
|
|
14
|
+
const variants = {
|
|
15
|
+
default:
|
|
16
|
+
"flex flex-row justify-between border-2 border-gray-300 gap-5 text-gray-700/80 flex-1",
|
|
17
|
+
active:
|
|
18
|
+
"flex flex-row justify-between border-2 border-primary/80 gap-5 text-primary/80 hover:text-primary flex-1",
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
interface ISelectTabsContentProps {
|
|
22
|
+
children: JSX.Element | string;
|
|
23
|
+
value: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Renders the content of a select tab.
|
|
27
|
+
*
|
|
28
|
+
* @param {ISelectTabsContentProps} props - The properties for the select tab content.
|
|
29
|
+
* @param {string} props.value - The value of the tab.
|
|
30
|
+
* @param {JSX.Element | string} props.children - The content of the tab.
|
|
31
|
+
* @return {JSX.Element} The rendered select tab content.
|
|
32
|
+
*/
|
|
33
|
+
export function SelectTabsContent({
|
|
34
|
+
value,
|
|
35
|
+
children,
|
|
36
|
+
}: ISelectTabsContentProps) {
|
|
37
|
+
const { activeTab, onChange } = useContext(SelectTabsContext);
|
|
38
|
+
return (
|
|
39
|
+
<Button
|
|
40
|
+
key={value}
|
|
41
|
+
className={activeTab === value ? variants.active : variants.default}
|
|
42
|
+
variant="ghost"
|
|
43
|
+
onClick={() => onChange(value)}
|
|
44
|
+
>
|
|
45
|
+
{children}
|
|
46
|
+
{activeTab === value ? (
|
|
47
|
+
<CircleCheckBig size={16} />
|
|
48
|
+
) : (
|
|
49
|
+
<Circle size={16} />
|
|
50
|
+
)}
|
|
51
|
+
</Button>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
interface IContextProps {
|
|
55
|
+
activeTab: string;
|
|
56
|
+
onChange: (value: string) => void;
|
|
57
|
+
}
|
|
58
|
+
const SelectTabsContext = createContext<IContextProps>({
|
|
59
|
+
activeTab: "",
|
|
60
|
+
onChange: () => {},
|
|
61
|
+
});
|
|
62
|
+
interface ISelectTabsProps {
|
|
63
|
+
children?: React.ReactNode;
|
|
64
|
+
deselect?: boolean;
|
|
65
|
+
disabled?: boolean;
|
|
66
|
+
onValueChange?: (value: string) => void;
|
|
67
|
+
value?: string;
|
|
68
|
+
className?: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Renders a set of selectable tabs.
|
|
73
|
+
*
|
|
74
|
+
* @param {ISelectTabsProps} props - The properties for the select tabs.
|
|
75
|
+
* @param {boolean} props.deselect - Whether the tabs can have a null value.
|
|
76
|
+
* @param {React.ReactNode} props.children - The content of the tabs.
|
|
77
|
+
* @param {string } [props.value] - The initial active tab value.
|
|
78
|
+
* @param {(newValue: string) => void} [props.onValueChange] - The callback function triggered when the active tab value changes.
|
|
79
|
+
* @param {boolean} [props.disabled] - Whether the tabs are disabled.
|
|
80
|
+
* @return {JSX.Element} The rendered select tabs.
|
|
81
|
+
*/
|
|
82
|
+
export default function SelectTabs({
|
|
83
|
+
deselect,
|
|
84
|
+
children,
|
|
85
|
+
value = "",
|
|
86
|
+
onValueChange,
|
|
87
|
+
disabled,
|
|
88
|
+
className,
|
|
89
|
+
}: ISelectTabsProps) {
|
|
90
|
+
const [activeTab, setActiveTab] = useState(value);
|
|
91
|
+
const contextValue = useMemo(() => ({ activeTab, onChange }), [activeTab]);
|
|
92
|
+
function onChange(newValue: string) {
|
|
93
|
+
if (disabled) return;
|
|
94
|
+
|
|
95
|
+
if (newValue === activeTab && deselect) {
|
|
96
|
+
setActiveTab("");
|
|
97
|
+
onValueChange?.("");
|
|
98
|
+
} else if (newValue !== activeTab) {
|
|
99
|
+
setActiveTab(newValue);
|
|
100
|
+
onValueChange?.(newValue);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<SelectTabsContext.Provider value={contextValue}>
|
|
106
|
+
<div className={cn("w-full grid gap-3", className)}>{children}</div>
|
|
107
|
+
</SelectTabsContext.Provider>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Check, ChevronDown, ChevronsUpDown, XIcon } from "lucide-react";
|
|
4
|
+
|
|
5
|
+
import { Fragment, useCallback, useEffect, useMemo, useState } from "react";
|
|
6
|
+
import { Badge } from "../components/badge";
|
|
7
|
+
import { Button } from "../components/button";
|
|
8
|
+
import {
|
|
9
|
+
Collapsible,
|
|
10
|
+
CollapsibleContent,
|
|
11
|
+
CollapsibleTrigger,
|
|
12
|
+
} from "../components/collapsible";
|
|
13
|
+
import {
|
|
14
|
+
Command,
|
|
15
|
+
CommandEmpty,
|
|
16
|
+
CommandGroup,
|
|
17
|
+
CommandInput,
|
|
18
|
+
CommandItem,
|
|
19
|
+
CommandList,
|
|
20
|
+
} from "../components/command";
|
|
21
|
+
import { Popover, PopoverContent, PopoverTrigger } from "../components/popover";
|
|
22
|
+
import { Skeleton } from "../components/skeleton";
|
|
23
|
+
import { useDebounce } from "../hooks/use-debounce";
|
|
24
|
+
import { cn } from "../lib/utils";
|
|
25
|
+
|
|
26
|
+
export type SelectableProps<T> = {
|
|
27
|
+
id?: string;
|
|
28
|
+
className?: string;
|
|
29
|
+
disabled?: boolean;
|
|
30
|
+
options?: T[];
|
|
31
|
+
defaultValue?: T[];
|
|
32
|
+
getKey: (option: T) => string;
|
|
33
|
+
getLabel: (option: T) => string;
|
|
34
|
+
getGroup?: (option: T) => string;
|
|
35
|
+
getDisabled?: (option: T) => boolean;
|
|
36
|
+
onSearch?: (search: string) => Promise<T[]>;
|
|
37
|
+
onChange: (value: T[]) => void;
|
|
38
|
+
singular?: boolean;
|
|
39
|
+
singleLine?: boolean;
|
|
40
|
+
selectedText?: string;
|
|
41
|
+
noResult?: string;
|
|
42
|
+
searchPlaceholderText?: string;
|
|
43
|
+
makeAChoiceText?: string;
|
|
44
|
+
typeToSearchText?: string;
|
|
45
|
+
renderOption?: (option: T) => React.ReactNode;
|
|
46
|
+
renderTrigger?: ({
|
|
47
|
+
children,
|
|
48
|
+
disabled,
|
|
49
|
+
}: {
|
|
50
|
+
children: React.ReactNode;
|
|
51
|
+
disabled: boolean;
|
|
52
|
+
}) => React.ReactNode;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export function Selectable<T>({
|
|
56
|
+
id,
|
|
57
|
+
className,
|
|
58
|
+
options,
|
|
59
|
+
defaultValue,
|
|
60
|
+
getKey,
|
|
61
|
+
getLabel,
|
|
62
|
+
getGroup,
|
|
63
|
+
getDisabled,
|
|
64
|
+
onSearch,
|
|
65
|
+
onChange,
|
|
66
|
+
singular = false,
|
|
67
|
+
singleLine,
|
|
68
|
+
selectedText = "Selected",
|
|
69
|
+
noResult = "Nothing to show.",
|
|
70
|
+
searchPlaceholderText = "Search...",
|
|
71
|
+
makeAChoiceText = "Make a choice...",
|
|
72
|
+
typeToSearchText = "Type to search...",
|
|
73
|
+
disabled = false,
|
|
74
|
+
renderOption,
|
|
75
|
+
renderTrigger: Trigger,
|
|
76
|
+
}: SelectableProps<T>) {
|
|
77
|
+
const [open, setOpen] = useState(false);
|
|
78
|
+
|
|
79
|
+
const [searchInput, setSearchInput] = useState("");
|
|
80
|
+
const searchValue = useDebounce(searchInput, 500);
|
|
81
|
+
const [searching, setSearching] = useState(false);
|
|
82
|
+
const [searchResults, setSearchResults] = useState<T[] | undefined>([]);
|
|
83
|
+
|
|
84
|
+
const [selectedOptions, setSelectedOptions] = useState<T[] | undefined>(
|
|
85
|
+
defaultValue
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
const selectableOptions = useMemo(() => {
|
|
89
|
+
const sourceOptions = onSearch && searchValue ? searchResults : options;
|
|
90
|
+
const selectableOptions =
|
|
91
|
+
sourceOptions?.filter(
|
|
92
|
+
(option) =>
|
|
93
|
+
!selectedOptions?.some(
|
|
94
|
+
(selected) => getKey(selected) === getKey(option)
|
|
95
|
+
)
|
|
96
|
+
) || [];
|
|
97
|
+
|
|
98
|
+
if (!getGroup)
|
|
99
|
+
return { groupedOptions: { "": selectableOptions }, sortedTypes: [""] };
|
|
100
|
+
const groupedOptions: Record<string, T[]> = {};
|
|
101
|
+
selectableOptions.forEach((option) => {
|
|
102
|
+
const type = getGroup(option);
|
|
103
|
+
if (!groupedOptions[type]) {
|
|
104
|
+
groupedOptions[type] = [];
|
|
105
|
+
}
|
|
106
|
+
groupedOptions[type].push(option);
|
|
107
|
+
});
|
|
108
|
+
const sortedTypes = Object.keys(groupedOptions).sort();
|
|
109
|
+
|
|
110
|
+
return { groupedOptions, sortedTypes };
|
|
111
|
+
}, [selectedOptions, searchResults, options, searchValue, getKey, getGroup]);
|
|
112
|
+
|
|
113
|
+
const toggleSelection = useCallback(
|
|
114
|
+
(option: T, isSelected: boolean) => {
|
|
115
|
+
// Prevent selection of disabled options
|
|
116
|
+
if (getDisabled?.(option)) return;
|
|
117
|
+
|
|
118
|
+
if (singular) {
|
|
119
|
+
const newSelection = isSelected ? [option] : [];
|
|
120
|
+
setSelectedOptions(newSelection);
|
|
121
|
+
onChange(newSelection);
|
|
122
|
+
setOpen(false);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
setSelectedOptions((prev) => {
|
|
126
|
+
const newSelection = isSelected
|
|
127
|
+
? [...(prev || []), option]
|
|
128
|
+
: prev?.filter((item) => getKey(item) !== getKey(option)) || [];
|
|
129
|
+
onChange(newSelection);
|
|
130
|
+
return newSelection;
|
|
131
|
+
});
|
|
132
|
+
},
|
|
133
|
+
[singular, getKey, getDisabled, onChange]
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
const handleSearchChange = useCallback((search: string) => {
|
|
137
|
+
setSearching(!!search);
|
|
138
|
+
setSearchInput(search);
|
|
139
|
+
}, []);
|
|
140
|
+
|
|
141
|
+
useEffect(() => {
|
|
142
|
+
if (!onSearch) {
|
|
143
|
+
setSearching(false);
|
|
144
|
+
setSearchResults([]);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (searchValue !== searchInput) return;
|
|
148
|
+
|
|
149
|
+
setSearching(true);
|
|
150
|
+
setSearchResults([]);
|
|
151
|
+
onSearch(searchValue)
|
|
152
|
+
.then((fetchedOptions) => {
|
|
153
|
+
setSearchResults(fetchedOptions);
|
|
154
|
+
})
|
|
155
|
+
.finally(() => {
|
|
156
|
+
setSearching(false);
|
|
157
|
+
});
|
|
158
|
+
}, [onSearch, searchValue, searchInput]);
|
|
159
|
+
|
|
160
|
+
const onPopoverOpenChange = useCallback(
|
|
161
|
+
(isOpen: boolean) => {
|
|
162
|
+
if (disabled) return;
|
|
163
|
+
setOpen(isOpen);
|
|
164
|
+
setSearchInput("");
|
|
165
|
+
},
|
|
166
|
+
[disabled]
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
const TriggerContent = (
|
|
170
|
+
<>
|
|
171
|
+
{selectedOptions?.length ? (
|
|
172
|
+
<div className={"flex flex-wrap gap-1 overflow-hidden w-full"}>
|
|
173
|
+
{!singleLine &&
|
|
174
|
+
selectedOptions.map((option) => {
|
|
175
|
+
const key = getKey(option);
|
|
176
|
+
const label = getLabel(option);
|
|
177
|
+
if (singular) {
|
|
178
|
+
return <Fragment key={key}>{label}</Fragment>;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return (
|
|
182
|
+
<Badge
|
|
183
|
+
key={key}
|
|
184
|
+
variant={"secondary"}
|
|
185
|
+
className="hover:animate-pulse"
|
|
186
|
+
onClick={(e) => {
|
|
187
|
+
if (disabled) return;
|
|
188
|
+
e.stopPropagation();
|
|
189
|
+
toggleSelection(option, false);
|
|
190
|
+
}}
|
|
191
|
+
>
|
|
192
|
+
{label}
|
|
193
|
+
<XIcon className="size-4" />
|
|
194
|
+
</Badge>
|
|
195
|
+
);
|
|
196
|
+
})}
|
|
197
|
+
{singleLine && (
|
|
198
|
+
<span className="truncate">
|
|
199
|
+
{selectedOptions.map((option) => getLabel(option)).join(", ")}
|
|
200
|
+
</span>
|
|
201
|
+
)}
|
|
202
|
+
</div>
|
|
203
|
+
) : (
|
|
204
|
+
makeAChoiceText
|
|
205
|
+
)}
|
|
206
|
+
<ChevronsUpDown className="opacity-50" />
|
|
207
|
+
</>
|
|
208
|
+
);
|
|
209
|
+
return (
|
|
210
|
+
<Popover open={open} onOpenChange={onPopoverOpenChange}>
|
|
211
|
+
<PopoverTrigger
|
|
212
|
+
id={id}
|
|
213
|
+
className={cn(
|
|
214
|
+
"w-full justify-between h-max hover:bg-accent/30",
|
|
215
|
+
className
|
|
216
|
+
)}
|
|
217
|
+
disabled={disabled}
|
|
218
|
+
asChild
|
|
219
|
+
>
|
|
220
|
+
{Trigger ? (
|
|
221
|
+
<span>
|
|
222
|
+
<Trigger disabled={disabled}>{TriggerContent}</Trigger>
|
|
223
|
+
</span>
|
|
224
|
+
) : (
|
|
225
|
+
<Button variant="outline" disabled={disabled}>
|
|
226
|
+
{TriggerContent}
|
|
227
|
+
</Button>
|
|
228
|
+
)}
|
|
229
|
+
</PopoverTrigger>
|
|
230
|
+
<PopoverContent className="p-0">
|
|
231
|
+
<Command>
|
|
232
|
+
<CommandInput
|
|
233
|
+
placeholder={searchPlaceholderText}
|
|
234
|
+
className="h-9"
|
|
235
|
+
onValueChange={(search) => handleSearchChange(search)}
|
|
236
|
+
/>
|
|
237
|
+
<CommandList>
|
|
238
|
+
{searching && (
|
|
239
|
+
<div className="p-1 text-sm">
|
|
240
|
+
<Skeleton className="h-7 w-full mb-1" />
|
|
241
|
+
<Skeleton className="h-7 w-full mb-1" />
|
|
242
|
+
<Skeleton className="h-7 w-full mb-1" />
|
|
243
|
+
</div>
|
|
244
|
+
)}
|
|
245
|
+
{!searching && !!selectedOptions?.length && (
|
|
246
|
+
<Collapsible className="border-b">
|
|
247
|
+
<CollapsibleTrigger asChild>
|
|
248
|
+
<CommandGroup
|
|
249
|
+
className="[&[data-state=open]_svg]:-rotate-90 cursor-pointer"
|
|
250
|
+
heading={
|
|
251
|
+
<div className="flex justify-between">
|
|
252
|
+
{selectedText}
|
|
253
|
+
<ChevronDown className="size-4 transition-all" />
|
|
254
|
+
</div>
|
|
255
|
+
}
|
|
256
|
+
/>
|
|
257
|
+
</CollapsibleTrigger>
|
|
258
|
+
<CollapsibleContent className="px-1">
|
|
259
|
+
{selectedOptions.map((option) => {
|
|
260
|
+
const key = getKey(option);
|
|
261
|
+
const isDisabled = getDisabled?.(option) ?? false;
|
|
262
|
+
return (
|
|
263
|
+
<CommandItem
|
|
264
|
+
id={"option-" + key}
|
|
265
|
+
key={key}
|
|
266
|
+
value={key}
|
|
267
|
+
disabled={isDisabled}
|
|
268
|
+
onSelect={() => toggleSelection(option, false)}
|
|
269
|
+
>
|
|
270
|
+
{renderOption ? (
|
|
271
|
+
renderOption(option)
|
|
272
|
+
) : (
|
|
273
|
+
<>
|
|
274
|
+
{getLabel(option)}
|
|
275
|
+
<Check className="ml-auto opacity-100" />
|
|
276
|
+
</>
|
|
277
|
+
)}
|
|
278
|
+
</CommandItem>
|
|
279
|
+
);
|
|
280
|
+
})}
|
|
281
|
+
</CollapsibleContent>
|
|
282
|
+
</Collapsible>
|
|
283
|
+
)}
|
|
284
|
+
{!searching && (
|
|
285
|
+
<CommandEmpty id="no-result-text">
|
|
286
|
+
{onSearch && !options?.length && !searchValue
|
|
287
|
+
? typeToSearchText
|
|
288
|
+
: noResult}
|
|
289
|
+
</CommandEmpty>
|
|
290
|
+
)}
|
|
291
|
+
{!searching &&
|
|
292
|
+
selectableOptions.sortedTypes?.map((group) => (
|
|
293
|
+
<CommandGroup key={group} heading={group}>
|
|
294
|
+
{selectableOptions.groupedOptions[group]?.map((option) => {
|
|
295
|
+
const key = getKey(option);
|
|
296
|
+
const isDisabled = getDisabled?.(option) ?? false;
|
|
297
|
+
return (
|
|
298
|
+
<CommandItem
|
|
299
|
+
id={"option-" + key}
|
|
300
|
+
key={key}
|
|
301
|
+
value={getLabel(option)}
|
|
302
|
+
disabled={isDisabled}
|
|
303
|
+
onSelect={() => toggleSelection(option, true)}
|
|
304
|
+
>
|
|
305
|
+
{getLabel(option)}
|
|
306
|
+
</CommandItem>
|
|
307
|
+
);
|
|
308
|
+
})}
|
|
309
|
+
</CommandGroup>
|
|
310
|
+
))}
|
|
311
|
+
</CommandList>
|
|
312
|
+
</Command>
|
|
313
|
+
</PopoverContent>
|
|
314
|
+
</Popover>
|
|
315
|
+
);
|
|
316
|
+
}
|