@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.
Files changed (236) hide show
  1. package/__mocks__/canvas.ts +8 -0
  2. package/components.json +21 -0
  3. package/eslint.config.js +4 -0
  4. package/jest-environment.js +37 -0
  5. package/jest.config.ts +47 -0
  6. package/jest.setup.ts +69 -0
  7. package/package.json +124 -0
  8. package/postcss.config.mjs +6 -0
  9. package/src/aria/index.tsx +1 -0
  10. package/src/aria/number-field.tsx +41 -0
  11. package/src/components/.gitkeep +0 -0
  12. package/src/components/accordion.tsx +66 -0
  13. package/src/components/alert-dialog.tsx +157 -0
  14. package/src/components/alert.tsx +70 -0
  15. package/src/components/aspect-ratio.tsx +11 -0
  16. package/src/components/avatar.tsx +53 -0
  17. package/src/components/badge.tsx +67 -0
  18. package/src/components/breadcrumb.tsx +109 -0
  19. package/src/components/button-group.tsx +83 -0
  20. package/src/components/button.tsx +68 -0
  21. package/src/components/calendar.tsx +219 -0
  22. package/src/components/card.tsx +92 -0
  23. package/src/components/carousel.tsx +241 -0
  24. package/src/components/chart.tsx +363 -0
  25. package/src/components/checkbox.tsx +32 -0
  26. package/src/components/collapsible.tsx +33 -0
  27. package/src/components/command.tsx +184 -0
  28. package/src/components/context-menu.tsx +252 -0
  29. package/src/components/dialog.tsx +144 -0
  30. package/src/components/drawer.tsx +135 -0
  31. package/src/components/dropdown-menu.tsx +258 -0
  32. package/src/components/empty.tsx +100 -0
  33. package/src/components/field.tsx +248 -0
  34. package/src/components/form.tsx +169 -0
  35. package/src/components/hover-card.tsx +44 -0
  36. package/src/components/input-group.tsx +170 -0
  37. package/src/components/input-otp.tsx +77 -0
  38. package/src/components/input.tsx +21 -0
  39. package/src/components/item.tsx +193 -0
  40. package/src/components/kbd.tsx +28 -0
  41. package/src/components/label.tsx +24 -0
  42. package/src/components/menubar.tsx +276 -0
  43. package/src/components/navigation-menu.tsx +168 -0
  44. package/src/components/pagination.tsx +130 -0
  45. package/src/components/popover.tsx +88 -0
  46. package/src/components/progress.tsx +31 -0
  47. package/src/components/radio-group.tsx +45 -0
  48. package/src/components/resizable.tsx +56 -0
  49. package/src/components/scroll-area.tsx +58 -0
  50. package/src/components/select.tsx +189 -0
  51. package/src/components/separator.tsx +28 -0
  52. package/src/components/sheet.tsx +140 -0
  53. package/src/components/sidebar.tsx +862 -0
  54. package/src/components/skeleton.tsx +13 -0
  55. package/src/components/slider.tsx +63 -0
  56. package/src/components/sonner.tsx +40 -0
  57. package/src/components/spinner.tsx +16 -0
  58. package/src/components/stepper.tsx +291 -0
  59. package/src/components/switch.tsx +31 -0
  60. package/src/components/table.tsx +133 -0
  61. package/src/components/tabs.tsx +66 -0
  62. package/src/components/textarea.tsx +18 -0
  63. package/src/components/toggle-group.tsx +83 -0
  64. package/src/components/toggle.tsx +47 -0
  65. package/src/components/tooltip.tsx +66 -0
  66. package/src/custom/action-button.tsx +48 -0
  67. package/src/custom/async-select.tsx +287 -0
  68. package/src/custom/awesome-not-found.tsx +116 -0
  69. package/src/custom/charts/area-chart.tsx +147 -0
  70. package/src/custom/charts/bar-chart.tsx +233 -0
  71. package/src/custom/charts/chart-card.tsx +103 -0
  72. package/src/custom/charts/index.tsx +16 -0
  73. package/src/custom/charts/pie-chart.tsx +168 -0
  74. package/src/custom/charts/radar-chart.tsx +126 -0
  75. package/src/custom/checkbox-tree.tsx +100 -0
  76. package/src/custom/combobox.tsx +296 -0
  77. package/src/custom/confirm-dialog.tsx +102 -0
  78. package/src/custom/country-selector.tsx +204 -0
  79. package/src/custom/date-picker/calendar-rac.tsx +109 -0
  80. package/src/custom/date-picker/datefield-rac.tsx +84 -0
  81. package/src/custom/date-picker/index.tsx +273 -0
  82. package/src/custom/date-picker/types/index.ts +4 -0
  83. package/src/custom/date-picker/utils/index.ts +42 -0
  84. package/src/custom/date-picker-old.tsx +50 -0
  85. package/src/custom/date-tooltip.tsx +98 -0
  86. package/src/custom/document-scanner/consts.ts +5 -0
  87. package/src/custom/document-scanner/corner-adjustment/action-buttons.tsx +33 -0
  88. package/src/custom/document-scanner/corner-adjustment/corner-handle.tsx +43 -0
  89. package/src/custom/document-scanner/corner-adjustment/hooks/use-corner-drag.ts +85 -0
  90. package/src/custom/document-scanner/corner-adjustment/index.tsx +125 -0
  91. package/src/custom/document-scanner/corner-adjustment/types.ts +53 -0
  92. package/src/custom/document-scanner/corner-adjustment/utils/clip-path.ts +22 -0
  93. package/src/custom/document-scanner/corner-adjustment/zoom-magnifier.tsx +115 -0
  94. package/src/custom/document-scanner/hooks/use-document-capture.ts +81 -0
  95. package/src/custom/document-scanner/hooks/use-document-scanner.ts +80 -0
  96. package/src/custom/document-scanner/hooks/use-perspective-crop.ts +38 -0
  97. package/src/custom/document-scanner/index.tsx +255 -0
  98. package/src/custom/document-scanner/lib.ts +407 -0
  99. package/src/custom/document-scanner/types.ts +205 -0
  100. package/src/custom/document-scanner/utils/perspective-correction.ts +139 -0
  101. package/src/custom/document-viewer/controllers.tsx +98 -0
  102. package/src/custom/document-viewer/index.tsx +43 -0
  103. package/src/custom/document-viewer/renderers/image.tsx +37 -0
  104. package/src/custom/document-viewer/renderers/index.tsx +2 -0
  105. package/src/custom/document-viewer/renderers/pdf.tsx +105 -0
  106. package/src/custom/email-input/domains.json +159 -0
  107. package/src/custom/email-input/email.tsx +229 -0
  108. package/src/custom/email-input/index.tsx +4 -0
  109. package/src/custom/email-input/types.ts +104 -0
  110. package/src/custom/file-uploader.tsx +541 -0
  111. package/src/custom/filter-component/fields/async-select.tsx +33 -0
  112. package/src/custom/filter-component/fields/date.tsx +60 -0
  113. package/src/custom/filter-component/fields/multi-select.tsx +30 -0
  114. package/src/custom/filter-component/index.tsx +217 -0
  115. package/src/custom/image-canvas.tsx +260 -0
  116. package/src/custom/json-editor.tsx +22 -0
  117. package/src/custom/master-data-grid/components/dialogs/column-settings-dialog.tsx +100 -0
  118. package/src/custom/master-data-grid/components/dialogs/index.ts +1 -0
  119. package/src/custom/master-data-grid/components/filters/client-filter.tsx +368 -0
  120. package/src/custom/master-data-grid/components/filters/filter-input.tsx +256 -0
  121. package/src/custom/master-data-grid/components/filters/index.ts +3 -0
  122. package/src/custom/master-data-grid/components/filters/inline-column-filter.tsx +233 -0
  123. package/src/custom/master-data-grid/components/filters/multi-filter-dialog.tsx +90 -0
  124. package/src/custom/master-data-grid/components/filters/server-filter.tsx +255 -0
  125. package/src/custom/master-data-grid/components/master-data-grid.tsx +472 -0
  126. package/src/custom/master-data-grid/components/pagination/index.ts +1 -0
  127. package/src/custom/master-data-grid/components/pagination/pagination.tsx +178 -0
  128. package/src/custom/master-data-grid/components/table/cell-renderer.tsx +634 -0
  129. package/src/custom/master-data-grid/components/table/header-cell.tsx +162 -0
  130. package/src/custom/master-data-grid/components/table/index.ts +4 -0
  131. package/src/custom/master-data-grid/components/table/table-body-renderer.tsx +113 -0
  132. package/src/custom/master-data-grid/components/table/virtual-body.tsx +138 -0
  133. package/src/custom/master-data-grid/components/toolbar/index.ts +1 -0
  134. package/src/custom/master-data-grid/components/toolbar/toolbar.tsx +314 -0
  135. package/src/custom/master-data-grid/hooks/index.ts +3 -0
  136. package/src/custom/master-data-grid/hooks/use-columns.tsx +332 -0
  137. package/src/custom/master-data-grid/hooks/use-editing.ts +106 -0
  138. package/src/custom/master-data-grid/hooks/use-table-state-reducer.ts +157 -0
  139. package/src/custom/master-data-grid/hooks/use-table-state.ts +31 -0
  140. package/src/custom/master-data-grid/index.ts +16 -0
  141. package/src/custom/master-data-grid/types.ts +466 -0
  142. package/src/custom/master-data-grid/utils/column-generator.tsx +306 -0
  143. package/src/custom/master-data-grid/utils/export-utils.ts +67 -0
  144. package/src/custom/master-data-grid/utils/filter-fns.ts +290 -0
  145. package/src/custom/master-data-grid/utils/index.ts +8 -0
  146. package/src/custom/master-data-grid/utils/pinning-utils.ts +88 -0
  147. package/src/custom/master-data-grid/utils/translation-utils.ts +42 -0
  148. package/src/custom/multi-select.tsx +432 -0
  149. package/src/custom/password-input.tsx +194 -0
  150. package/src/custom/phone-input.tsx +172 -0
  151. package/src/custom/schema-form/custom/index.tsx +1 -0
  152. package/src/custom/schema-form/custom/label.tsx +53 -0
  153. package/src/custom/schema-form/fields/base-input-field.tsx +82 -0
  154. package/src/custom/schema-form/fields/field.tsx +67 -0
  155. package/src/custom/schema-form/fields/index.tsx +5 -0
  156. package/src/custom/schema-form/fields/object.tsx +12 -0
  157. package/src/custom/schema-form/fields/table-array/array-field-item.tsx +90 -0
  158. package/src/custom/schema-form/fields/table-array/array-field-template.tsx +115 -0
  159. package/src/custom/schema-form/index.tsx +259 -0
  160. package/src/custom/schema-form/templates/description.tsx +20 -0
  161. package/src/custom/schema-form/templates/index.tsx +2 -0
  162. package/src/custom/schema-form/templates/submit.tsx +32 -0
  163. package/src/custom/schema-form/types.ts +64 -0
  164. package/src/custom/schema-form/utils/index.ts +4 -0
  165. package/src/custom/schema-form/utils/schema-dependency.ts +655 -0
  166. package/src/custom/schema-form/utils/schemas.ts +289 -0
  167. package/src/custom/schema-form/utils/validation.ts +23 -0
  168. package/src/custom/schema-form/widgets/boolean.tsx +77 -0
  169. package/src/custom/schema-form/widgets/combobox.tsx +274 -0
  170. package/src/custom/schema-form/widgets/date.tsx +59 -0
  171. package/src/custom/schema-form/widgets/email.tsx +34 -0
  172. package/src/custom/schema-form/widgets/index.tsx +10 -0
  173. package/src/custom/schema-form/widgets/password.tsx +40 -0
  174. package/src/custom/schema-form/widgets/phone.tsx +40 -0
  175. package/src/custom/schema-form/widgets/select.tsx +105 -0
  176. package/src/custom/schema-form/widgets/selectable.tsx +25 -0
  177. package/src/custom/schema-form/widgets/string-array.tsx +296 -0
  178. package/src/custom/schema-form/widgets/url.tsx +56 -0
  179. package/src/custom/section-layout-v2.tsx +212 -0
  180. package/src/custom/select-tabs.tsx +109 -0
  181. package/src/custom/selectable.tsx +316 -0
  182. package/src/custom/stepper.tsx +236 -0
  183. package/src/custom/tab-layout.tsx +213 -0
  184. package/src/custom/tanstack-table/fields/index.tsx +12 -0
  185. package/src/custom/tanstack-table/fields/tanstack-table-action-dialogs.tsx +89 -0
  186. package/src/custom/tanstack-table/fields/tanstack-table-column-header.tsx +66 -0
  187. package/src/custom/tanstack-table/fields/tanstack-table-filter-date.tsx +180 -0
  188. package/src/custom/tanstack-table/fields/tanstack-table-filter-faceted.tsx +158 -0
  189. package/src/custom/tanstack-table/fields/tanstack-table-filter-text.tsx +76 -0
  190. package/src/custom/tanstack-table/fields/tanstack-table-pagination.tsx +136 -0
  191. package/src/custom/tanstack-table/fields/tanstack-table-plain-table.tsx +142 -0
  192. package/src/custom/tanstack-table/fields/tanstack-table-row-actions-confirmation.tsx +77 -0
  193. package/src/custom/tanstack-table/fields/tanstack-table-row-actions-custom-dialog.tsx +87 -0
  194. package/src/custom/tanstack-table/fields/tanstack-table-row-actions.tsx +151 -0
  195. package/src/custom/tanstack-table/fields/tanstack-table-table-actions-custom-dialog.tsx +88 -0
  196. package/src/custom/tanstack-table/fields/tanstack-table-table-actions-schemaform-dialog.tsx +47 -0
  197. package/src/custom/tanstack-table/fields/tanstack-table-toolbar.tsx +143 -0
  198. package/src/custom/tanstack-table/fields/tanstack-table-view-options.tsx +171 -0
  199. package/src/custom/tanstack-table/index.tsx +244 -0
  200. package/src/custom/tanstack-table/types/index.ts +328 -0
  201. package/src/custom/tanstack-table/utils/cell-with-actions.tsx +21 -0
  202. package/src/custom/tanstack-table/utils/column-names.ts +26 -0
  203. package/src/custom/tanstack-table/utils/columns-by-row-data.tsx +312 -0
  204. package/src/custom/tanstack-table/utils/editable-columns-by-row-data.tsx +219 -0
  205. package/src/custom/tanstack-table/utils/faceted-boolean-options.tsx +22 -0
  206. package/src/custom/tanstack-table/utils/index.tsx +10 -0
  207. package/src/custom/tanstack-table/utils/pinning-styles.ts +57 -0
  208. package/src/custom/tanstack-table/utils/table.tsx +83 -0
  209. package/src/custom/tanstack-table/utils/test-conditions.ts +17 -0
  210. package/src/custom/timeline.tsx +208 -0
  211. package/src/custom/tree.tsx +200 -0
  212. package/src/custom/tscanify/browser.ts +66 -0
  213. package/src/custom/tscanify/index.ts +51 -0
  214. package/src/custom/tscanify/tscanify-browser.ts +522 -0
  215. package/src/custom/tscanify/tscanify.ts +262 -0
  216. package/src/custom/tscanify/types.ts +22 -0
  217. package/src/custom/webcam.tsx +737 -0
  218. package/src/hooks/.gitkeep +0 -0
  219. package/src/hooks/use-callback-ref.ts +27 -0
  220. package/src/hooks/use-controllable-state.ts +67 -0
  221. package/src/hooks/use-debounce.ts +19 -0
  222. package/src/hooks/use-is-visible.ts +23 -0
  223. package/src/hooks/use-media-query.ts +21 -0
  224. package/src/hooks/use-mobile.ts +21 -0
  225. package/src/hooks/use-on-window-resize.ts +15 -0
  226. package/src/hooks/use-scroll.tsx +22 -0
  227. package/src/lib/utils.ts +61 -0
  228. package/src/lib/zod.ts +2 -0
  229. package/src/styles/core.css +57 -0
  230. package/src/styles/globals.css +130 -0
  231. package/src/test/email-input.test.tsx +217 -0
  232. package/src/test/password-input.test.tsx +92 -0
  233. package/src/test/select-tabs.test.tsx +302 -0
  234. package/src/test/selectable.test.tsx +1093 -0
  235. package/tsconfig.json +13 -0
  236. package/tsconfig.lint.json +8 -0
@@ -0,0 +1,42 @@
1
+ import {
2
+ CalendarDate,
3
+ DateValue,
4
+ parseDate,
5
+ parseTime,
6
+ Time,
7
+ } from "@internationalized/date";
8
+
9
+ export function createDate({
10
+ date,
11
+ // timezone,
12
+ offset,
13
+ }: {
14
+ date: Date | undefined;
15
+ // timezone: string;
16
+ offset: number;
17
+ }): CalendarDate | DateValue | undefined {
18
+ if (date)
19
+ return parseDate(
20
+ new Date(date.getTime() - offset).toJSON().split("T").at(0) || ""
21
+ );
22
+ return undefined;
23
+ }
24
+
25
+ export function createTime({
26
+ date,
27
+ offset,
28
+ }: {
29
+ date: Date | undefined;
30
+ offset: number;
31
+ }): Time {
32
+ if (date)
33
+ return parseTime(
34
+ new Date(date.getTime() - offset)
35
+ .toJSON()
36
+ .split("T")
37
+ .at(1)
38
+ ?.replace("Z", "") || ""
39
+ );
40
+
41
+ return parseTime("00:00");
42
+ }
@@ -0,0 +1,50 @@
1
+ "use client";
2
+ import { format } from "date-fns";
3
+ import { Calendar as CalendarIcon } from "lucide-react";
4
+
5
+ import { cn } from "@repo/ayasofyazilim-ui/lib/utils";
6
+ import { Button } from "@repo/ayasofyazilim-ui/components/button";
7
+ import { Calendar } from "@repo/ayasofyazilim-ui/components/calendar";
8
+ import {
9
+ Popover,
10
+ PopoverContent,
11
+ PopoverTrigger,
12
+ } from "@repo/ayasofyazilim-ui/components/popover";
13
+ import { forwardRef } from "react";
14
+
15
+ export const DatePicker = forwardRef<
16
+ HTMLDivElement,
17
+ {
18
+ date?: Date;
19
+ setDate: (date?: Date) => void;
20
+ disabled?: boolean;
21
+ className?: string;
22
+ }
23
+ >(function DatePickerCmp({ date, setDate, disabled, className }, ref) {
24
+ return (
25
+ <Popover>
26
+ <PopoverTrigger asChild>
27
+ <Button
28
+ disabled={disabled}
29
+ variant={"outline"}
30
+ className={cn(
31
+ "w-full justify-start text-left font-normal",
32
+ !date && "text-muted-foreground",
33
+ className
34
+ )}
35
+ >
36
+ <CalendarIcon className="mr-2 h-4 w-4" />
37
+ {date ? format(date, "PPP") : <span>Pick a date</span>}
38
+ </Button>
39
+ </PopoverTrigger>
40
+ <PopoverContent className="w-auto p-0" ref={ref}>
41
+ <Calendar
42
+ mode="single"
43
+ selected={date}
44
+ onSelect={setDate}
45
+ initialFocus
46
+ />
47
+ </PopoverContent>
48
+ </Popover>
49
+ );
50
+ });
@@ -0,0 +1,98 @@
1
+ "use client";
2
+
3
+ import React from "react";
4
+ import { ClockIcon } from "lucide-react";
5
+ import {
6
+ Tooltip,
7
+ TooltipContent,
8
+ TooltipTrigger,
9
+ } from "@repo/ayasofyazilim-ui/components/tooltip";
10
+
11
+ export type Localization = { locale: string; timeZone: string; lang: string };
12
+
13
+ function DateTooltip({
14
+ icon = <ClockIcon className="w-4 h-4" />,
15
+ date,
16
+ dateOptions = {
17
+ day: "2-digit",
18
+ month: "long",
19
+ year: "numeric",
20
+ hour: "2-digit",
21
+ minute: "2-digit",
22
+ hour12: false,
23
+ },
24
+ localization,
25
+ }: {
26
+ icon?: React.ReactNode;
27
+ date: string | Date;
28
+ dateOptions?: Intl.DateTimeFormatOptions;
29
+ localization: Localization;
30
+ }) {
31
+ const tenantDate = formatToLocalizedDate({
32
+ localization,
33
+ date,
34
+ dateOptions,
35
+ timeZone: localization.timeZone,
36
+ });
37
+ const utcDate = formatToLocalizedDate({
38
+ localization,
39
+ date,
40
+ dateOptions,
41
+ timeZone: "UTC",
42
+ });
43
+ const userDate = formatToLocalizedDate({ localization, date, dateOptions });
44
+ return (
45
+ <Tooltip>
46
+ <TooltipTrigger
47
+ className="flex items-center gap-1 underline decoration-dotted underline-offset-2"
48
+ asChild
49
+ >
50
+ <div>
51
+ {icon} {tenantDate}
52
+ </div>
53
+ </TooltipTrigger>
54
+ <TooltipContent className="bg-gray-100 text-gray-900 border border-gray-300 z-100 [&_svg]:fill-gray-100">
55
+ <p className="flex justify-between">
56
+ <span className="font-semibold mr-2">UTC:</span>
57
+ {utcDate}
58
+ </p>
59
+ <p className="flex justify-between">
60
+ <span className="font-semibold mr-2">Tenant:</span>
61
+ {tenantDate}
62
+ </p>
63
+ <p className="flex justify-between">
64
+ <span className="font-semibold mr-2">You:</span>
65
+ {userDate}
66
+ </p>
67
+ </TooltipContent>
68
+ </Tooltip>
69
+ );
70
+ }
71
+
72
+ export default DateTooltip;
73
+
74
+ export function formatToLocalizedDate({
75
+ localization,
76
+ date,
77
+ dateOptions = {
78
+ day: "2-digit",
79
+ month: "long",
80
+ year: "numeric",
81
+ hour: "2-digit",
82
+ minute: "2-digit",
83
+ hour12: false,
84
+ },
85
+ timeZone,
86
+ }: {
87
+ localization: Localization;
88
+ date: string | Date;
89
+ dateOptions?: Intl.DateTimeFormatOptions;
90
+ timeZone?: string;
91
+ }) {
92
+ const _date = typeof date === "string" ? new Date(date) : date;
93
+ const dateString = _date.toLocaleDateString(localization.lang, {
94
+ ...dateOptions,
95
+ timeZone,
96
+ });
97
+ return dateString;
98
+ }
@@ -0,0 +1,5 @@
1
+ export const DEFAULT_VIDEO_DIMENSIONS = { width: 1080, height: 1080 };
2
+ export const DEFAULT_MIN_DOCUMENT_SIZE = 0.1;
3
+ export const DEFAULT_MAX_DOCUMENT_SIZE = 0.98;
4
+ export const DEFAULT_IMAGE_QUALITY = 0.95;
5
+ export const DEFAULT_CAPTURE_INTERVAL = 2;
@@ -0,0 +1,33 @@
1
+ import { memo } from "react";
2
+ import { ActionButtonsProps } from "./types";
3
+ import { Button } from "@repo/ayasofyazilim-ui/components/button";
4
+
5
+ export const ActionButtons = memo<ActionButtonsProps>(
6
+ ({
7
+ allowCrop,
8
+ allowRetry,
9
+ cropButtonText,
10
+ retryButtonText,
11
+ onCrop,
12
+ onRetry,
13
+ }) => {
14
+ if (!allowCrop && !allowRetry) return null;
15
+
16
+ return (
17
+ <div className="absolute bottom-2 right-2 flex gap-2">
18
+ {allowRetry && (
19
+ <Button type="button" variant="outline" onClick={onRetry} size="sm">
20
+ {retryButtonText}
21
+ </Button>
22
+ )}
23
+ {allowCrop && (
24
+ <Button onClick={onCrop} size="sm">
25
+ {cropButtonText}
26
+ </Button>
27
+ )}
28
+ </div>
29
+ );
30
+ }
31
+ );
32
+
33
+ ActionButtons.displayName = "ActionButtons";
@@ -0,0 +1,43 @@
1
+ import { memo } from "react";
2
+ import { cn } from "@repo/ayasofyazilim-ui/lib/utils";
3
+ import { CornerHandleProps } from "./types";
4
+
5
+ export const CornerHandle = memo<CornerHandleProps>(
6
+ ({
7
+ cornerKey,
8
+ corner,
9
+ videoDimensions,
10
+ isDragging,
11
+ cornerColor,
12
+ cornerTouchAreaSize,
13
+ onDragStart,
14
+ }) => {
15
+ const percentX = (corner.x / videoDimensions.width) * 100;
16
+ const percentY = (corner.y / videoDimensions.height) * 100;
17
+
18
+ return (
19
+ <div
20
+ role="button"
21
+ tabIndex={0}
22
+ className={cn(
23
+ "block border rounded-full min-w-5 min-h-5 absolute cursor-grab active:cursor-grabbing select-none",
24
+ isDragging ? "bg-blue-500/80 scale-125" : cornerColor,
25
+ isDragging ? "z-20" : "z-10"
26
+ )}
27
+ style={{
28
+ left: `${percentX}%`,
29
+ top: `${percentY}%`,
30
+ transform: `translate(-50%, -50%)`,
31
+ width: `${cornerTouchAreaSize}px`,
32
+ height: `${cornerTouchAreaSize}px`,
33
+ touchAction: "none",
34
+ }}
35
+ onPointerDown={(e) => onDragStart(cornerKey, e)}
36
+ >
37
+ <span className="sr-only">{`Drag ${cornerKey} corner`}</span>
38
+ </div>
39
+ );
40
+ }
41
+ );
42
+
43
+ CornerHandle.displayName = "CornerHandle";
@@ -0,0 +1,85 @@
1
+ import { useCallback, useState, PointerEvent } from "react";
2
+ import { DocumentCorners } from "../../types";
3
+ import { DragState } from "../types";
4
+
5
+ export function useCornerDrag(
6
+ detectedCorners: DocumentCorners,
7
+ videoDimensions: { width: number; height: number },
8
+ onCornersChange: (corners: DocumentCorners) => void
9
+ ) {
10
+ const [dragState, setDragState] = useState<DragState>({
11
+ isDragging: null,
12
+ offset: { x: 0, y: 0 },
13
+ });
14
+
15
+ const handleCornerDragStart = useCallback(
16
+ (cornerKey: string, event: PointerEvent) => {
17
+ event.preventDefault();
18
+
19
+ const rect = event.currentTarget.getBoundingClientRect();
20
+ const containerRect = event.currentTarget
21
+ .closest(".corner-adjustment-container")
22
+ ?.getBoundingClientRect();
23
+
24
+ if (containerRect) {
25
+ const newDragState = {
26
+ isDragging: cornerKey,
27
+ offset: {
28
+ x: event.clientX - rect.left - rect.width / 2,
29
+ y: event.clientY - rect.top - rect.height / 2,
30
+ },
31
+ };
32
+ setDragState(newDragState);
33
+ }
34
+ },
35
+ []
36
+ );
37
+
38
+ const handleCornerDrag = useCallback(
39
+ (event: PointerEvent) => {
40
+ if (!dragState.isDragging || !detectedCorners) {
41
+ return;
42
+ }
43
+
44
+ event.preventDefault();
45
+ const containerRect = event.currentTarget.getBoundingClientRect();
46
+
47
+ // Calculate new position relative to container
48
+ const newX =
49
+ ((event.clientX - containerRect.left - dragState.offset.x) /
50
+ containerRect.width) *
51
+ videoDimensions.width;
52
+ const newY =
53
+ ((event.clientY - containerRect.top - dragState.offset.y) /
54
+ containerRect.height) *
55
+ videoDimensions.height;
56
+
57
+ // Clamp to image bounds
58
+ const clampedX = Math.max(0, Math.min(videoDimensions.width, newX));
59
+ const clampedY = Math.max(0, Math.min(videoDimensions.height, newY));
60
+
61
+ // Update corners
62
+ const updatedCorners = {
63
+ ...detectedCorners,
64
+ [dragState.isDragging]: { x: clampedX, y: clampedY },
65
+ };
66
+
67
+ onCornersChange(updatedCorners);
68
+ },
69
+ [dragState, detectedCorners, videoDimensions, onCornersChange]
70
+ );
71
+
72
+ const handleCornerDragEnd = useCallback(() => {
73
+ setDragState({
74
+ isDragging: null,
75
+ offset: { x: 0, y: 0 },
76
+ });
77
+ }, []);
78
+
79
+ return {
80
+ dragState,
81
+ handleCornerDragStart,
82
+ handleCornerDrag,
83
+ handleCornerDragEnd,
84
+ };
85
+ }
@@ -0,0 +1,125 @@
1
+ import { memo, useMemo, useRef, useLayoutEffect, useState } from "react";
2
+ import { ActionButtons } from "./action-buttons";
3
+ import { CornerHandle } from "./corner-handle";
4
+ import { ZoomMagnifier } from "./zoom-magnifier";
5
+ import { useCornerDrag } from "./hooks/use-corner-drag";
6
+ import { generateClipPath } from "./utils/clip-path";
7
+ import { CornerAdjustmentProps } from "../types";
8
+
9
+ export const CornerAdjustment = memo<CornerAdjustmentProps>(
10
+ ({
11
+ capturedImage,
12
+ detectedCorners,
13
+ videoDimensions,
14
+ onCornersChange,
15
+ onCrop,
16
+ onRetry,
17
+ cornerColor = "bg-white/80",
18
+ cornerTouchAreaSize = 20,
19
+ allowCrop = true,
20
+ allowRetry = true,
21
+ cropButtonText = "Crop",
22
+ retryButtonText = "Scan Again",
23
+ showMagnifier = true,
24
+ magnifierSize = 100,
25
+ zoomLevel = 2,
26
+ }) => {
27
+ const containerRef = useRef<HTMLDivElement>(null);
28
+ const [containerDimensions, setContainerDimensions] = useState({
29
+ width: 0,
30
+ height: 0,
31
+ });
32
+
33
+ const {
34
+ dragState,
35
+ handleCornerDragStart,
36
+ handleCornerDrag,
37
+ handleCornerDragEnd,
38
+ } = useCornerDrag(detectedCorners, videoDimensions, onCornersChange);
39
+
40
+ // Update container dimensions
41
+ useLayoutEffect(() => {
42
+ if (containerRef.current) {
43
+ const rect = containerRef.current.getBoundingClientRect();
44
+ setContainerDimensions({ width: rect.width, height: rect.height });
45
+ }
46
+ }, []);
47
+
48
+ const clipPath = useMemo(
49
+ () => generateClipPath(detectedCorners, videoDimensions),
50
+ [detectedCorners, videoDimensions]
51
+ );
52
+
53
+ const cornerEntries = useMemo(
54
+ () => Object.entries(detectedCorners),
55
+ [detectedCorners]
56
+ );
57
+
58
+ return (
59
+ <div
60
+ ref={containerRef}
61
+ className="corner-adjustment-container size-full flex items-center justify-center relative bg-red-500"
62
+ onPointerMove={handleCornerDrag}
63
+ onPointerUp={handleCornerDragEnd}
64
+ onPointerLeave={handleCornerDragEnd}
65
+ >
66
+ {/* Original image */}
67
+ <img
68
+ src={capturedImage}
69
+ alt="Original"
70
+ className="h-auto w-full object-contain absolute top-0 left-0 opacity-50"
71
+ draggable={false}
72
+ />
73
+ {/* Clipped image */}
74
+ <img
75
+ src={capturedImage}
76
+ alt="Document preview"
77
+ className="h-full w-full object-contain"
78
+ style={{ clipPath }}
79
+ draggable={false}
80
+ />
81
+
82
+ {/* Zoom Magnifier */}
83
+ {showMagnifier && (
84
+ <ZoomMagnifier
85
+ capturedImage={capturedImage}
86
+ detectedCorners={detectedCorners}
87
+ videoDimensions={videoDimensions}
88
+ containerDimensions={containerDimensions}
89
+ draggedCorner={dragState.isDragging}
90
+ zoomLevel={zoomLevel}
91
+ magnifierSize={magnifierSize}
92
+ />
93
+ )}
94
+
95
+ {/* Corner handles */}
96
+ {cornerEntries.map(([cornerKey, corner]) => (
97
+ <CornerHandle
98
+ key={cornerKey}
99
+ cornerKey={cornerKey}
100
+ corner={corner}
101
+ videoDimensions={videoDimensions}
102
+ isDragging={dragState.isDragging === cornerKey}
103
+ cornerColor={cornerColor}
104
+ cornerTouchAreaSize={cornerTouchAreaSize}
105
+ onDragStart={handleCornerDragStart}
106
+ />
107
+ ))}
108
+
109
+ {/* Action buttons */}
110
+ {!dragState.isDragging && (
111
+ <ActionButtons
112
+ allowCrop={allowCrop}
113
+ allowRetry={allowRetry}
114
+ cropButtonText={cropButtonText}
115
+ retryButtonText={retryButtonText}
116
+ onCrop={onCrop}
117
+ onRetry={onRetry}
118
+ />
119
+ )}
120
+ </div>
121
+ );
122
+ }
123
+ );
124
+
125
+ CornerAdjustment.displayName = "CornerAdjustment";
@@ -0,0 +1,53 @@
1
+ import { PointerEvent } from "react";
2
+ import { DocumentCorners } from "../types";
3
+
4
+ export interface CornerAdjustmentProps {
5
+ capturedImage: string;
6
+ detectedCorners: DocumentCorners;
7
+ videoDimensions: { width: number; height: number };
8
+
9
+ onCornersChange: (corners: DocumentCorners) => void;
10
+ onCrop: () => void;
11
+ onRetry: () => void;
12
+
13
+ // Styling props
14
+ cornerColor?: string;
15
+ cornerTouchAreaSize?: number;
16
+
17
+ // Feature toggles
18
+ allowCrop?: boolean;
19
+ allowRetry?: boolean;
20
+
21
+ // Text customization
22
+ cropButtonText?: string;
23
+ retryButtonText?: string;
24
+
25
+ // Magnifier settings
26
+ showMagnifier?: boolean;
27
+ magnifierSize?: number;
28
+ zoomLevel?: number;
29
+ }
30
+
31
+ export interface DragState {
32
+ isDragging: string | null;
33
+ offset: { x: number; y: number };
34
+ }
35
+
36
+ export interface CornerHandleProps {
37
+ cornerKey: string;
38
+ corner: { x: number; y: number };
39
+ videoDimensions: { width: number; height: number };
40
+ isDragging: boolean;
41
+ cornerColor: string;
42
+ cornerTouchAreaSize: number;
43
+ onDragStart: (cornerKey: string, event: PointerEvent) => void;
44
+ }
45
+
46
+ export interface ActionButtonsProps {
47
+ allowCrop: boolean;
48
+ allowRetry: boolean;
49
+ cropButtonText: string;
50
+ retryButtonText: string;
51
+ onCrop: () => void;
52
+ onRetry: () => void;
53
+ }
@@ -0,0 +1,22 @@
1
+ import { DocumentCorners } from "../../types";
2
+
3
+ export function generateClipPath(
4
+ corners: DocumentCorners,
5
+ videoDimensions: { width: number; height: number }
6
+ ): string {
7
+ const { topLeftCorner, topRightCorner, bottomRightCorner, bottomLeftCorner } =
8
+ corners;
9
+
10
+ // Convert pixel coordinates to percentages
11
+ const toPercent = (x: number, y: number) => ({
12
+ x: (x / videoDimensions.width) * 100,
13
+ y: (y / videoDimensions.height) * 100,
14
+ });
15
+
16
+ const tl = toPercent(topLeftCorner.x, topLeftCorner.y);
17
+ const tr = toPercent(topRightCorner.x, topRightCorner.y);
18
+ const br = toPercent(bottomRightCorner.x, bottomRightCorner.y);
19
+ const bl = toPercent(bottomLeftCorner.x, bottomLeftCorner.y);
20
+
21
+ return `polygon(${tl.x}% ${tl.y}%, ${tr.x}% ${tr.y}%, ${br.x}% ${br.y}%, ${bl.x}% ${bl.y}%)`;
22
+ }
@@ -0,0 +1,115 @@
1
+ import { memo, useMemo } from "react";
2
+ import { DocumentCorners } from "../types";
3
+
4
+ interface ZoomMagnifierProps {
5
+ capturedImage: string;
6
+ detectedCorners: DocumentCorners;
7
+ videoDimensions: { width: number; height: number };
8
+ containerDimensions: { width: number; height: number };
9
+ draggedCorner: string | null;
10
+ zoomLevel?: number;
11
+ magnifierSize?: number;
12
+ }
13
+
14
+ export const ZoomMagnifier = memo<ZoomMagnifierProps>(
15
+ ({
16
+ capturedImage,
17
+ detectedCorners,
18
+ videoDimensions,
19
+ containerDimensions,
20
+ draggedCorner,
21
+ zoomLevel = 3,
22
+ magnifierSize = 100,
23
+ }) => {
24
+ const position = useMemo(() => {
25
+ if (!draggedCorner) return null;
26
+
27
+ const margin = 8;
28
+ let left = 0;
29
+ let top = 0;
30
+
31
+ // Position opposite to the corner being dragged
32
+ switch (draggedCorner) {
33
+ case "topLeftCorner":
34
+ left = containerDimensions.width - magnifierSize - margin;
35
+ top = margin;
36
+ break;
37
+ case "topRightCorner":
38
+ left = margin;
39
+ top = margin;
40
+ break;
41
+ case "bottomLeftCorner":
42
+ left = containerDimensions.width - magnifierSize - margin;
43
+ top = containerDimensions.height - magnifierSize - margin;
44
+ break;
45
+ case "bottomRightCorner":
46
+ left = margin;
47
+ top = containerDimensions.height - magnifierSize - margin;
48
+ break;
49
+ default:
50
+ return null;
51
+ }
52
+
53
+ return { left, top };
54
+ }, [draggedCorner, containerDimensions, magnifierSize]);
55
+
56
+ const backgroundStyle = useMemo(() => {
57
+ if (!position || !draggedCorner) return {};
58
+
59
+ // Get the active corner position
60
+ const activeCorner =
61
+ detectedCorners[draggedCorner as keyof DocumentCorners];
62
+ if (!activeCorner) return {};
63
+
64
+ // Convert corner position from video dimensions to container dimensions
65
+ const cornerX =
66
+ (activeCorner.x / videoDimensions.width) * containerDimensions.width;
67
+ const cornerY =
68
+ (activeCorner.y / videoDimensions.height) * containerDimensions.height;
69
+
70
+ // Calculate the background position to center the zoom on active corner
71
+ const bgX = -cornerX * zoomLevel + magnifierSize / 2;
72
+ const bgY = -cornerY * zoomLevel + magnifierSize / 2;
73
+
74
+ return {
75
+ backgroundImage: `url(${capturedImage})`,
76
+ backgroundSize: `${containerDimensions.width * zoomLevel}px ${containerDimensions.height * zoomLevel}px`,
77
+ backgroundPosition: `${bgX}px ${bgY}px`,
78
+ backgroundRepeat: "no-repeat",
79
+ };
80
+ }, [
81
+ capturedImage,
82
+ detectedCorners,
83
+ draggedCorner,
84
+ videoDimensions,
85
+ containerDimensions,
86
+ zoomLevel,
87
+ magnifierSize,
88
+ position,
89
+ ]);
90
+
91
+ if (!position || !draggedCorner) return null;
92
+
93
+ return (
94
+ <div
95
+ className="absolute border-2 border-white rounded-md shadow-lg pointer-events-none z-30"
96
+ style={{
97
+ left: position.left,
98
+ top: position.top,
99
+ width: magnifierSize,
100
+ height: magnifierSize,
101
+ ...backgroundStyle,
102
+ }}
103
+ >
104
+ {/* Crosshair overlay */}
105
+ <div className="absolute inset-0 flex items-center justify-center pointer-events-none">
106
+ <div className="absolute inset-0 flex items-center justify-center pointer-events-none">
107
+ <div className="size-[5px] rounded-full border border-primary bg-white absolute" />
108
+ </div>
109
+ </div>
110
+ </div>
111
+ );
112
+ }
113
+ );
114
+
115
+ ZoomMagnifier.displayName = "ZoomMagnifier";