@webiny/admin-ui 6.4.11 → 6.6.0-alpha.1

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 (317) hide show
  1. package/Accordion/Accordion.d.ts +7 -6
  2. package/Accordion/components/AccordionContent.js +4 -1
  3. package/Accordion/components/AccordionContent.js.map +1 -1
  4. package/Accordion/components/AccordionItem.d.ts +8 -6
  5. package/Accordion/components/AccordionItem.js.map +1 -1
  6. package/Accordion/components/AccordionItemAction.d.ts +7 -6
  7. package/Accordion/components/AccordionItemDragHandle.d.ts +2 -1
  8. package/Accordion/components/AccordionItemDragHandle.js +2 -1
  9. package/Accordion/components/AccordionItemDragHandle.js.map +1 -1
  10. package/Accordion/components/AccordionItemIcon.d.ts +7 -6
  11. package/Accordion/components/AccordionItemSeparator.d.ts +7 -6
  12. package/Accordion/components/AccordionTrigger.js +4 -2
  13. package/Accordion/components/AccordionTrigger.js.map +1 -1
  14. package/Alert/Alert.d.ts +21 -19
  15. package/AutoComplete/AutoComplete.d.ts +7 -6
  16. package/AutoComplete/primitives/AutoCompletePrimitive.d.ts +7 -6
  17. package/Avatar/Avatar.d.ts +9 -8
  18. package/Avatar/components/AvatarFallback.d.ts +7 -6
  19. package/Avatar/components/AvatarImage.d.ts +7 -6
  20. package/BetaBadge/BetaBadge.d.ts +6 -0
  21. package/BetaBadge/BetaBadge.js +7 -0
  22. package/BetaBadge/BetaBadge.js.map +1 -0
  23. package/Breadcrumbs/Breadcrumbs.d.ts +54 -0
  24. package/Breadcrumbs/Breadcrumbs.js +66 -0
  25. package/Breadcrumbs/Breadcrumbs.js.map +1 -0
  26. package/Breadcrumbs/Breadcrumbs.stories.d.ts +9 -0
  27. package/Breadcrumbs/Breadcrumbs.stories.js +71 -0
  28. package/Breadcrumbs/Breadcrumbs.stories.js.map +1 -0
  29. package/Breadcrumbs/index.d.ts +1 -0
  30. package/Breadcrumbs/index.js +1 -0
  31. package/Button/Button.d.ts +10 -9
  32. package/Button/CopyButton.d.ts +7 -6
  33. package/Button/IconButton.d.ts +10 -9
  34. package/Calendar/Calendar.d.ts +7 -6
  35. package/Calendar/calendarClassNames.js +1 -1
  36. package/Calendar/calendarClassNames.js.map +1 -1
  37. package/Card/Card.d.ts +8 -22
  38. package/Card/components/CancelAction.d.ts +8 -6
  39. package/Card/components/CardContent.d.ts +6 -6
  40. package/Card/components/CardTitle.d.ts +2 -2
  41. package/Card/components/ConfirmAction.d.ts +8 -6
  42. package/Card/components/Icon.d.ts +7 -6
  43. package/Checkbox/Checkbox.d.ts +7 -6
  44. package/Checkbox/primitives/CheckboxPrimitive.d.ts +17 -12
  45. package/CheckboxGroup/CheckboxGroup.d.ts +7 -6
  46. package/CheckboxGroup/primitives/CheckboxGroupPrimitive.d.ts +20 -12
  47. package/CodeEditor/CodeEditor.d.ts +7 -6
  48. package/CodeEditor/CodeEditorPrimitive.d.ts +7 -6
  49. package/CodeEditor/CodeEditorPrimitive.js +3 -1
  50. package/CodeEditor/CodeEditorPrimitive.js.map +1 -1
  51. package/ColorPicker/ColorPicker.d.ts +7 -6
  52. package/ColorPicker/primitives/ColorPickerPrimitive.d.ts +7 -6
  53. package/Command/Command.d.ts +10 -8
  54. package/DataList/DataListModal.d.ts +6 -3
  55. package/DataTable/DataTable.d.ts +23 -12
  56. package/DataTable/DataTable.js +12 -4
  57. package/DataTable/DataTable.js.map +1 -1
  58. package/DataTable/components/ColumnsVisibility.d.ts +5 -4
  59. package/DataTable/components/ColumnsVisibility.js +6 -3
  60. package/DataTable/components/ColumnsVisibility.js.map +1 -1
  61. package/DatePicker/DatePicker.d.ts +7 -6
  62. package/DatePicker/primitives/DatePickerPrimitive.d.ts +7 -6
  63. package/DatePicker/primitives/DateTimePicker.d.ts +1 -1
  64. package/DatePicker/primitives/DateTimePicker.js +15 -3
  65. package/DatePicker/primitives/DateTimePicker.js.map +1 -1
  66. package/DatePicker/primitives/components/DatePickerTrigger.js +1 -1
  67. package/DatePicker/primitives/components/DatePickerTrigger.js.map +1 -1
  68. package/DatePicker/utils/types.d.ts +2 -0
  69. package/Dialog/Dialog.d.ts +8 -22
  70. package/Dialog/components/CancelAction.d.ts +8 -6
  71. package/Dialog/components/ConfirmAction.d.ts +8 -6
  72. package/Dialog/components/DialogContent.d.ts +1 -1
  73. package/Dialog/components/DialogFooter.d.ts +1 -1
  74. package/Dialog/components/DialogTitle.d.ts +1 -1
  75. package/Dialog/components/Icon.d.ts +7 -6
  76. package/Drawer/Drawer.d.ts +8 -22
  77. package/Drawer/Drawer.stories.js +1 -0
  78. package/Drawer/Drawer.stories.js.map +1 -1
  79. package/Drawer/components/CancelButton.d.ts +8 -6
  80. package/Drawer/components/ConfirmButton.d.ts +8 -6
  81. package/Drawer/components/DrawerContent.d.ts +1 -1
  82. package/Drawer/components/Icon.d.ts +7 -6
  83. package/DropdownMenu/components/DropdownMenuItem.d.ts +3 -2
  84. package/DropdownMenu/components/DropdownMenuItem.js +9 -2
  85. package/DropdownMenu/components/DropdownMenuItem.js.map +1 -1
  86. package/DropdownMenu/components/DropdownMenuLink.d.ts +9 -8
  87. package/DropdownMenu/components/DropdownMenuSeparator.d.ts +7 -6
  88. package/EditableTitle/EditableTitle.d.ts +7 -6
  89. package/EmptyState/EmptyState.d.ts +43 -0
  90. package/EmptyState/EmptyState.js +93 -0
  91. package/EmptyState/EmptyState.js.map +1 -0
  92. package/EmptyState/EmptyState.stories.d.ts +13 -0
  93. package/EmptyState/EmptyState.stories.js +136 -0
  94. package/EmptyState/EmptyState.stories.js.map +1 -0
  95. package/EmptyState/illustrations/ContentIllustration.d.ts +2 -0
  96. package/EmptyState/illustrations/ContentIllustration.js +100 -0
  97. package/EmptyState/illustrations/ContentIllustration.js.map +1 -0
  98. package/EmptyState/illustrations/LayoutIllustration.d.ts +2 -0
  99. package/EmptyState/illustrations/LayoutIllustration.js +93 -0
  100. package/EmptyState/illustrations/LayoutIllustration.js.map +1 -0
  101. package/EmptyState/illustrations/ListingIllustration.d.ts +2 -0
  102. package/EmptyState/illustrations/ListingIllustration.js +92 -0
  103. package/EmptyState/illustrations/ListingIllustration.js.map +1 -0
  104. package/EmptyState/illustrations/SelectIllustration.d.ts +2 -0
  105. package/EmptyState/illustrations/SelectIllustration.js +100 -0
  106. package/EmptyState/illustrations/SelectIllustration.js.map +1 -0
  107. package/EmptyState/illustrations/TableIllustration.d.ts +2 -0
  108. package/EmptyState/illustrations/TableIllustration.js +233 -0
  109. package/EmptyState/illustrations/TableIllustration.js.map +1 -0
  110. package/EmptyState/illustrations/UploadIllustration.d.ts +2 -0
  111. package/EmptyState/illustrations/UploadIllustration.js +77 -0
  112. package/EmptyState/illustrations/UploadIllustration.js.map +1 -0
  113. package/EmptyState/illustrations/index.d.ts +6 -0
  114. package/EmptyState/illustrations/index.js +6 -0
  115. package/EmptyState/index.d.ts +1 -0
  116. package/EmptyState/index.js +1 -0
  117. package/FilePicker/FilePicker.d.ts +19 -18
  118. package/FilePicker/FilePicker.stories.d.ts +0 -6
  119. package/FilePicker/FilePicker.stories.js +1 -24
  120. package/FilePicker/FilePicker.stories.js.map +1 -1
  121. package/FilePicker/primitives/FilePickerPrimitive.d.ts +20 -19
  122. package/FilePicker/primitives/components/Trigger.d.ts +1 -1
  123. package/FilePicker/primitives/components/previews/ImagePreview/ImagePreview.d.ts +19 -7
  124. package/FilePicker/primitives/components/previews/ImagePreview/ImagePreview.js +15 -6
  125. package/FilePicker/primitives/components/previews/ImagePreview/ImagePreview.js.map +1 -1
  126. package/FilePicker/primitives/components/previews/ItemActions.js +1 -1
  127. package/FilePicker/primitives/components/previews/ItemActions.js.map +1 -1
  128. package/FilePicker/primitives/components/previews/ItemDescription.js +1 -1
  129. package/FilePicker/primitives/components/previews/ItemDescription.js.map +1 -1
  130. package/FilePicker/primitives/components/previews/RichItemPreview/RichItemPreview.d.ts +12 -6
  131. package/FilePicker/primitives/components/previews/RichItemPreview/RichItemPreview.js +13 -52
  132. package/FilePicker/primitives/components/previews/RichItemPreview/RichItemPreview.js.map +1 -1
  133. package/FilePicker/primitives/components/previews/RichItemPreview/RichItemThumbnail.d.ts +8 -4
  134. package/FilePicker/primitives/components/previews/RichItemPreview/RichItemThumbnail.js +66 -8
  135. package/FilePicker/primitives/components/previews/RichItemPreview/RichItemThumbnail.js.map +1 -1
  136. package/FilePicker/primitives/components/previews/TextOnlyPreview/TextOnlyPreview.d.ts +7 -6
  137. package/FilePicker/primitives/components/previews/index.d.ts +0 -1
  138. package/FilePicker/primitives/components/previews/index.js +0 -1
  139. package/FilePicker/primitives/components/previews/variants.d.ts +1 -1
  140. package/FillViewport/FillViewport.d.ts +21 -18
  141. package/FormComponent/Description.d.ts +7 -6
  142. package/FormComponent/ErrorMessage.d.ts +7 -6
  143. package/FormComponent/Label.d.ts +7 -6
  144. package/FormComponent/Note.d.ts +7 -6
  145. package/Grid/Grid.d.ts +24 -22
  146. package/HeaderBar/HeaderBar.d.ts +7 -6
  147. package/Heading/Heading.d.ts +8 -7
  148. package/Icon/Icon.d.ts +9 -8
  149. package/IconPicker/IconPicker.d.ts +7 -6
  150. package/IconPicker/primitives/IconPickerPrimitive.d.ts +1 -1
  151. package/IconPicker/primitives/components/IconPickerTrigger.d.ts +1 -1
  152. package/ImageEditor/AspectRatioPreview.d.ts +16 -0
  153. package/ImageEditor/AspectRatioPreview.js +26 -0
  154. package/ImageEditor/AspectRatioPreview.js.map +1 -0
  155. package/ImageEditor/CropHotspotEditor.d.ts +22 -0
  156. package/ImageEditor/CropHotspotEditor.js +402 -0
  157. package/ImageEditor/CropHotspotEditor.js.map +1 -0
  158. package/ImageEditor/ImageEditor.d.ts +18 -0
  159. package/ImageEditor/ImageEditor.js +151 -0
  160. package/ImageEditor/ImageEditor.js.map +1 -0
  161. package/ImageEditor/ImageEditor.stories.d.ts +6 -0
  162. package/ImageEditor/ImageEditor.stories.js +42 -0
  163. package/ImageEditor/ImageEditor.stories.js.map +1 -0
  164. package/ImageEditor/MetaFields.d.ts +12 -0
  165. package/ImageEditor/MetaFields.js +20 -0
  166. package/ImageEditor/MetaFields.js.map +1 -0
  167. package/ImageEditor/SectionLabel.d.ts +8 -0
  168. package/ImageEditor/SectionLabel.js +7 -0
  169. package/ImageEditor/SectionLabel.js.map +1 -0
  170. package/ImageEditor/geometry.d.ts +46 -0
  171. package/ImageEditor/geometry.js +126 -0
  172. package/ImageEditor/geometry.js.map +1 -0
  173. package/ImageEditor/index.d.ts +3 -0
  174. package/ImageEditor/index.js +3 -0
  175. package/ImageEditor/types.d.ts +52 -0
  176. package/ImageEditor/types.js +35 -0
  177. package/ImageEditor/types.js.map +1 -0
  178. package/Input/Input.d.ts +7 -6
  179. package/Input/InputPrimitive.d.ts +9 -8
  180. package/Label/Label.d.ts +8 -7
  181. package/Label/components/LabelValue.d.ts +1 -1
  182. package/Link/Link.d.ts +3 -3
  183. package/Link/Link.js +1 -1
  184. package/Link/Link.js.map +1 -1
  185. package/List/List.d.ts +8 -7
  186. package/List/components/ListItem.d.ts +7 -6
  187. package/List/components/ListItemAction.d.ts +7 -6
  188. package/List/components/ListItemHandle.d.ts +7 -6
  189. package/List/components/ListItemIcon.d.ts +7 -6
  190. package/List/components/ListItemSeparator.d.ts +7 -6
  191. package/Loader/Loader.d.ts +9 -8
  192. package/Loader/OverlayLoader.d.ts +7 -6
  193. package/Markdown/Markdown.d.ts +47 -0
  194. package/Markdown/Markdown.js +17 -0
  195. package/Markdown/Markdown.js.map +1 -0
  196. package/Markdown/index.d.ts +1 -0
  197. package/Markdown/index.js +1 -0
  198. package/MultiAutoComplete/MultiAutoComplete.d.ts +7 -6
  199. package/MultiAutoComplete/primitives/MultiAutoCompletePrimitive.d.ts +7 -6
  200. package/MultiAutoComplete/primitives/presenters/MultiAutoCompletePresenterWithFreeInput.d.ts +3 -3
  201. package/MultiAutoComplete/primitives/useMultiAutoComplete.d.ts +3 -3
  202. package/MultiFilePicker/MultiFilePicker.d.ts +19 -18
  203. package/MultiFilePicker/primitives/MultiFilePickerPrimitive.d.ts +19 -18
  204. package/MultiSelect/MultiSelect.d.ts +7 -6
  205. package/MultiSelect/primitives/MultiSelectPrimitive.d.ts +10 -6
  206. package/MultiSelect/primitives/MultiSelectPrimitive.js +1 -1
  207. package/MultiSelect/primitives/MultiSelectPrimitive.js.map +1 -1
  208. package/Popover/Popover.d.ts +7 -6
  209. package/Popover/primitives/PopoverPrimitive.d.ts +7 -7
  210. package/ProgressBar/ProgressBar.d.ts +7 -6
  211. package/RadioGroup/RadioGroup.d.ts +7 -6
  212. package/RadioGroup/primitives/Radio.d.ts +7 -6
  213. package/RadioGroup/primitives/RadioGroupPrimitive.d.ts +5 -1
  214. package/RangeSlider/RangeSlider.d.ts +7 -6
  215. package/SegmentedControl/SegmentedControl.d.ts +8 -7
  216. package/SegmentedControl/SegmentedControlTabs.d.ts +5 -2
  217. package/SegmentedControl/primitives/SegmentedControlPrimitive.d.ts +1 -1
  218. package/Select/Select.d.ts +7 -6
  219. package/Select/primitives/components/SelectContent.js +1 -1
  220. package/Select/primitives/components/SelectContent.js.map +1 -1
  221. package/Select/primitives/components/SelectTrigger.d.ts +2 -2
  222. package/Separator/Separator.d.ts +9 -8
  223. package/Sidebar/Sidebar.d.ts +7 -6
  224. package/Sidebar/components/SidebarContent.js +1 -1
  225. package/Sidebar/components/SidebarContent.js.map +1 -1
  226. package/Sidebar/components/SidebarIcon.d.ts +7 -6
  227. package/Sidebar/components/SidebarProvider.d.ts +4 -4
  228. package/Sidebar/components/SidebarRoot.js +1 -1
  229. package/Sidebar/components/SidebarRoot.js.map +1 -1
  230. package/Sidebar/components/items/SidebarMenuGroup.d.ts +9 -8
  231. package/Sidebar/components/items/SidebarMenuItem.d.ts +11 -10
  232. package/Sidebar/components/items/SidebarMenuItemIcon.d.ts +7 -6
  233. package/Sidebar/components/items/SidebarMenuLink.d.ts +11 -10
  234. package/Sidebar/components/items/SidebarMenuProvider.d.ts +4 -4
  235. package/Sidebar/components/items/SidebarMenuRootItem.d.ts +9 -7
  236. package/Sidebar/components/items/SidebarMenuSubButton.js +5 -8
  237. package/Sidebar/components/items/SidebarMenuSubButton.js.map +1 -1
  238. package/Sidebar/components/items/SidebarMenuSubItem.js +3 -3
  239. package/Sidebar/components/items/SidebarMenuSubItem.js.map +1 -1
  240. package/Skeleton/Skeleton.d.ts +9 -7
  241. package/Skeleton/Skeleton.js +10 -4
  242. package/Skeleton/Skeleton.js.map +1 -1
  243. package/Skeleton/Skeleton.stories.d.ts +2 -0
  244. package/Skeleton/Skeleton.stories.js +28 -2
  245. package/Skeleton/Skeleton.stories.js.map +1 -1
  246. package/Slider/Slider.d.ts +7 -6
  247. package/SteppedProgress/SteppedProgress.d.ts +7 -6
  248. package/SteppedProgress/SteppedProgressItem.d.ts +7 -6
  249. package/Switch/Switch.d.ts +7 -6
  250. package/Switch/primitives/SwitchPrimitive.d.ts +10 -6
  251. package/Table/Table.d.ts +12 -10
  252. package/Table/components/Direction.d.ts +1 -1
  253. package/Tabs/Tabs.d.ts +8 -6
  254. package/Tabs/Tabs.js +5 -2
  255. package/Tabs/Tabs.js.map +1 -1
  256. package/Tabs/components/Content.d.ts +1 -1
  257. package/Tabs/components/List.d.ts +2 -2
  258. package/Tabs/components/Tab.d.ts +7 -6
  259. package/Tabs/components/Trigger.d.ts +3 -2
  260. package/Tabs/components/Trigger.js +8 -3
  261. package/Tabs/components/Trigger.js.map +1 -1
  262. package/Tag/Tag.d.ts +8 -7
  263. package/Tags/Tags.d.ts +7 -6
  264. package/Tags/primitives/TagsPrimitive.d.ts +7 -6
  265. package/Text/Text.d.ts +8 -7
  266. package/Textarea/Textarea.d.ts +7 -6
  267. package/Textarea/TextareaPrimitive.d.ts +9 -8
  268. package/Toast/Toast.d.ts +7 -6
  269. package/Toast/components/ToastActions.d.ts +7 -6
  270. package/Toast/components/ToastDescription.d.ts +7 -6
  271. package/Toast/components/ToastTitle.d.ts +7 -6
  272. package/Toast/useToast.stories.js.map +1 -1
  273. package/Toggle/Toggle.d.ts +7 -6
  274. package/Toggle/primitives/TogglePrimitive.d.ts +13 -9
  275. package/ToggleGroup/ToggleGroup.d.ts +7 -6
  276. package/ToggleGroup/primitives/ToggleGroupPrimitive.d.ts +13 -9
  277. package/Tooltip/Tooltip.d.ts +7 -6
  278. package/Tree/Tree.d.ts +7 -6
  279. package/Tree/components/Item.d.ts +7 -6
  280. package/Tree/components/ItemCollapseTrigger.d.ts +7 -6
  281. package/Tree/components/ItemContent.d.ts +8 -6
  282. package/Tree/components/ItemDragHandle.d.ts +7 -6
  283. package/Tree/components/ItemIcon.d.ts +7 -6
  284. package/Tree/components/ItemPlaceholder.d.ts +7 -6
  285. package/Tree/components/ItemSubIndentation.d.ts +7 -6
  286. package/Tree/useTree.js +2 -6
  287. package/Tree/useTree.js.map +1 -1
  288. package/Widget/Widget.d.ts +8 -22
  289. package/Widget/components/Icon.d.ts +7 -6
  290. package/Widget/components/WidgetAction.d.ts +8 -6
  291. package/Widget/components/WidgetContent.d.ts +5 -5
  292. package/exports/admin/ui.d.ts +2 -0
  293. package/exports/admin/ui.js +2 -0
  294. package/index.d.ts +5 -0
  295. package/index.js +5 -0
  296. package/package.json +29 -29
  297. package/theme.css +22 -1
  298. package/utils.js +52 -34
  299. package/utils.js.map +1 -1
  300. package/FilePicker/primitives/components/previews/RichItemPreview/FileType.d.ts +0 -9
  301. package/FilePicker/primitives/components/previews/RichItemPreview/FileType.js +0 -45
  302. package/FilePicker/primitives/components/previews/RichItemPreview/FileType.js.map +0 -1
  303. package/FilePicker/primitives/components/previews/RichItemPreview/Placeholder.d.ts +0 -8
  304. package/FilePicker/primitives/components/previews/RichItemPreview/Placeholder.js +0 -14
  305. package/FilePicker/primitives/components/previews/RichItemPreview/Placeholder.js.map +0 -1
  306. package/FilePicker/primitives/components/previews/RichItemPreview/Thumbnail.d.ts +0 -11
  307. package/FilePicker/primitives/components/previews/RichItemPreview/Thumbnail.js +0 -20
  308. package/FilePicker/primitives/components/previews/RichItemPreview/Thumbnail.js.map +0 -1
  309. package/FilePicker/primitives/components/previews/RichItemPreview/ThumbnailActions.d.ts +0 -15
  310. package/FilePicker/primitives/components/previews/RichItemPreview/ThumbnailActions.js +0 -46
  311. package/FilePicker/primitives/components/previews/RichItemPreview/ThumbnailActions.js.map +0 -1
  312. package/FilePicker/primitives/components/previews/TruncatedFileName.d.ts +0 -11
  313. package/FilePicker/primitives/components/previews/TruncatedFileName.js +0 -20
  314. package/FilePicker/primitives/components/previews/TruncatedFileName.js.map +0 -1
  315. package/FilePicker/primitives/components/previews/fileName.d.ts +0 -12
  316. package/FilePicker/primitives/components/previews/fileName.js +0 -26
  317. package/FilePicker/primitives/components/previews/fileName.js.map +0 -1
@@ -0,0 +1,402 @@
1
+ import react, { useCallback, useLayoutEffect, useMemo, useRef, useState } from "react";
2
+ import { Select } from "../Select/index.js";
3
+ import { Text } from "../Text/index.js";
4
+ import { SectionLabel } from "./SectionLabel.js";
5
+ import { DEFAULT_ASPECT_RATIOS } from "./types.js";
6
+ const MIN = 0.05;
7
+ const FREE = "free";
8
+ const inferAspectId = (crop, width, height)=>{
9
+ if (!crop || !width || !height) return FREE;
10
+ const cropWidth = Math.max(0, 1 - (crop.left ?? 0) - (crop.right ?? 0));
11
+ const cropHeight = Math.max(0, 1 - (crop.top ?? 0) - (crop.bottom ?? 0));
12
+ if (cropWidth <= 0 || cropHeight <= 0) return FREE;
13
+ const displayRatio = cropWidth * width / (cropHeight * height);
14
+ const match = DEFAULT_ASPECT_RATIOS.find((ar)=>Math.abs(ar.ratio - displayRatio) < 0.02);
15
+ return match?.id ?? FREE;
16
+ };
17
+ const MAX_CANVAS_WIDTH = 640;
18
+ const MAX_CANVAS_HEIGHT = 460;
19
+ const clamp01 = (n)=>Math.min(1, Math.max(0, n));
20
+ const cropToEdges = (crop)=>({
21
+ left: clamp01(crop?.left ?? 0),
22
+ top: clamp01(crop?.top ?? 0),
23
+ right: 1 - clamp01(crop?.right ?? 0),
24
+ bottom: 1 - clamp01(crop?.bottom ?? 0)
25
+ });
26
+ const edgesToCrop = (e)=>({
27
+ top: e.top,
28
+ left: e.left,
29
+ bottom: 1 - e.bottom,
30
+ right: 1 - e.right
31
+ });
32
+ const HANDLE_EDGES = {
33
+ nw: [
34
+ "top",
35
+ "left"
36
+ ],
37
+ n: [
38
+ "top"
39
+ ],
40
+ ne: [
41
+ "top",
42
+ "right"
43
+ ],
44
+ e: [
45
+ "right"
46
+ ],
47
+ se: [
48
+ "bottom",
49
+ "right"
50
+ ],
51
+ s: [
52
+ "bottom"
53
+ ],
54
+ sw: [
55
+ "bottom",
56
+ "left"
57
+ ],
58
+ w: [
59
+ "left"
60
+ ]
61
+ };
62
+ const CORNER_HANDLES = [
63
+ "nw",
64
+ "ne",
65
+ "se",
66
+ "sw"
67
+ ];
68
+ const EDGE_HANDLES = [
69
+ "n",
70
+ "e",
71
+ "s",
72
+ "w"
73
+ ];
74
+ const clampHotspot = (h, e)=>({
75
+ x: Math.min(e.right, Math.max(e.left, h?.x ?? (e.left + e.right) / 2)),
76
+ y: Math.min(e.bottom, Math.max(e.top, h?.y ?? (e.top + e.bottom) / 2)),
77
+ width: h?.width ?? 1,
78
+ height: h?.height ?? 1
79
+ });
80
+ const CropHotspotEditor = ({ image, crop, hotspot, aspectId, onChangeAspect, onChangeCrop, onChangeHotspot })=>{
81
+ const surfaceRef = useRef(null);
82
+ const dragRef = useRef(null);
83
+ const wrapperRef = useRef(null);
84
+ const [availWidth, setAvailWidth] = useState(0);
85
+ useLayoutEffect(()=>{
86
+ const el = wrapperRef.current;
87
+ if (!el) return;
88
+ const measure = ()=>setAvailWidth(el.getBoundingClientRect().width);
89
+ measure();
90
+ if ("u" < typeof ResizeObserver) return;
91
+ const ro = new ResizeObserver(measure);
92
+ ro.observe(el);
93
+ return ()=>ro.disconnect();
94
+ }, []);
95
+ const imageRatio = (image.width || 1) / (image.height || 1);
96
+ const surfaceStyle = useMemo(()=>{
97
+ if (availWidth <= 0) return {
98
+ width: "100%",
99
+ aspectRatio: `${image.width || 1} / ${image.height || 1}`,
100
+ maxHeight: `${MAX_CANVAS_HEIGHT}px`
101
+ };
102
+ let width = Math.min(availWidth, MAX_CANVAS_WIDTH);
103
+ let height = width / imageRatio;
104
+ if (height > MAX_CANVAS_HEIGHT) {
105
+ height = MAX_CANVAS_HEIGHT;
106
+ width = height * imageRatio;
107
+ }
108
+ return {
109
+ width: `${width}px`,
110
+ height: `${height}px`
111
+ };
112
+ }, [
113
+ availWidth,
114
+ imageRatio,
115
+ image.width,
116
+ image.height
117
+ ]);
118
+ const edges = useMemo(()=>cropToEdges(crop), [
119
+ crop
120
+ ]);
121
+ const ratio = DEFAULT_ASPECT_RATIOS.find((ar)=>ar.id === aspectId)?.ratio ?? null;
122
+ const aspectOptions = useMemo(()=>[
123
+ {
124
+ label: "Free",
125
+ value: FREE
126
+ },
127
+ ...DEFAULT_ASPECT_RATIOS.map((ar)=>({
128
+ label: ar.label,
129
+ value: ar.id
130
+ }))
131
+ ], []);
132
+ const toNorm = useCallback((clientX, clientY)=>{
133
+ const rect = surfaceRef.current.getBoundingClientRect();
134
+ return {
135
+ x: clamp01((clientX - rect.left) / rect.width),
136
+ y: clamp01((clientY - rect.top) / rect.height)
137
+ };
138
+ }, []);
139
+ const commitEdges = useCallback((next)=>{
140
+ onChangeCrop(edgesToCrop(next));
141
+ onChangeHotspot(clampHotspot(hotspot, next));
142
+ }, [
143
+ onChangeCrop,
144
+ onChangeHotspot,
145
+ hotspot
146
+ ]);
147
+ const resizeFree = (handle, p)=>{
148
+ const moves = HANDLE_EDGES[handle];
149
+ const next = {
150
+ ...edges
151
+ };
152
+ if (moves.includes("left")) next.left = Math.min(p.x, next.right - MIN);
153
+ if (moves.includes("right")) next.right = Math.max(p.x, next.left + MIN);
154
+ if (moves.includes("top")) next.top = Math.min(p.y, next.bottom - MIN);
155
+ if (moves.includes("bottom")) next.bottom = Math.max(p.y, next.top + MIN);
156
+ return next;
157
+ };
158
+ const resizeLocked = (handle, p, r)=>{
159
+ const moves = HANDLE_EDGES[handle];
160
+ const rNorm = r / imageRatio;
161
+ const anchorX = moves.includes("left") ? edges.right : edges.left;
162
+ const anchorY = moves.includes("top") ? edges.bottom : edges.top;
163
+ const dirX = moves.includes("left") ? -1 : 1;
164
+ const dirY = moves.includes("top") ? -1 : 1;
165
+ const maxW = dirX > 0 ? 1 - anchorX : anchorX;
166
+ const maxH = dirY > 0 ? 1 - anchorY : anchorY;
167
+ let w = Math.abs(p.x - anchorX);
168
+ let h = w / rNorm;
169
+ if (h > Math.abs(p.y - anchorY)) {
170
+ h = Math.abs(p.y - anchorY);
171
+ w = h * rNorm;
172
+ }
173
+ w = Math.min(w, maxW);
174
+ h = w / rNorm;
175
+ if (h > maxH) {
176
+ h = maxH;
177
+ w = h * rNorm;
178
+ }
179
+ w = Math.max(w, MIN);
180
+ h = Math.max(h, MIN);
181
+ const next = {
182
+ ...edges
183
+ };
184
+ if (dirX > 0) {
185
+ next.left = anchorX;
186
+ next.right = anchorX + w;
187
+ } else {
188
+ next.right = anchorX;
189
+ next.left = anchorX - w;
190
+ }
191
+ if (dirY > 0) {
192
+ next.top = anchorY;
193
+ next.bottom = anchorY + h;
194
+ } else {
195
+ next.bottom = anchorY;
196
+ next.top = anchorY - h;
197
+ }
198
+ return next;
199
+ };
200
+ const moveCrop = (startEdges, dx, dy)=>{
201
+ const w = startEdges.right - startEdges.left;
202
+ const h = startEdges.bottom - startEdges.top;
203
+ const left = Math.min(Math.max(startEdges.left + dx, 0), 1 - w);
204
+ const top = Math.min(Math.max(startEdges.top + dy, 0), 1 - h);
205
+ return {
206
+ left,
207
+ top,
208
+ right: left + w,
209
+ bottom: top + h
210
+ };
211
+ };
212
+ const onPointerMove = useCallback((e)=>{
213
+ const drag = dragRef.current;
214
+ if (!drag) return;
215
+ const p = toNorm(e.clientX, e.clientY);
216
+ if ("focal" === drag.mode) return void onChangeHotspot(clampHotspot({
217
+ x: p.x,
218
+ y: p.y,
219
+ width: 1,
220
+ height: 1
221
+ }, edges));
222
+ if ("move" === drag.mode) {
223
+ const dx = p.x - drag.startPointer.x;
224
+ const dy = p.y - drag.startPointer.y;
225
+ commitEdges(moveCrop(drag.startEdges, dx, dy));
226
+ return;
227
+ }
228
+ const handle = drag.mode.slice(7);
229
+ commitEdges(null === ratio ? resizeFree(handle, p) : resizeLocked(handle, p, ratio));
230
+ }, [
231
+ toNorm,
232
+ onChangeHotspot,
233
+ edges,
234
+ commitEdges,
235
+ ratio
236
+ ]);
237
+ const startMoveCrop = useCallback((e)=>{
238
+ dragRef.current = {
239
+ mode: "move",
240
+ startPointer: toNorm(e.clientX, e.clientY),
241
+ startEdges: edges
242
+ };
243
+ surfaceRef.current?.setPointerCapture(e.pointerId);
244
+ }, [
245
+ toNorm,
246
+ edges
247
+ ]);
248
+ const startFocal = useCallback((e)=>{
249
+ e.stopPropagation();
250
+ dragRef.current = {
251
+ mode: "focal"
252
+ };
253
+ surfaceRef.current?.setPointerCapture(e.pointerId);
254
+ }, []);
255
+ const startResize = useCallback((handle)=>(e)=>{
256
+ e.stopPropagation();
257
+ dragRef.current = {
258
+ mode: `resize:${handle}`
259
+ };
260
+ surfaceRef.current?.setPointerCapture(e.pointerId);
261
+ }, []);
262
+ const endDrag = useCallback((e)=>{
263
+ dragRef.current = null;
264
+ surfaceRef.current?.releasePointerCapture(e.pointerId);
265
+ }, []);
266
+ const onSelectAspect = useCallback((value)=>{
267
+ onChangeAspect(value);
268
+ const r = DEFAULT_ASPECT_RATIOS.find((ar)=>ar.id === value)?.ratio;
269
+ if (value === FREE || !r) return;
270
+ const rNorm = r / imageRatio;
271
+ const cw = edges.right - edges.left;
272
+ const ch = edges.bottom - edges.top;
273
+ let w = cw;
274
+ let h = w / rNorm;
275
+ if (h > ch) {
276
+ h = ch;
277
+ w = h * rNorm;
278
+ }
279
+ const cx = (edges.left + edges.right) / 2;
280
+ const cy = (edges.top + edges.bottom) / 2;
281
+ commitEdges({
282
+ left: clamp01(cx - w / 2),
283
+ right: clamp01(cx + w / 2),
284
+ top: clamp01(cy - h / 2),
285
+ bottom: clamp01(cy + h / 2)
286
+ });
287
+ }, [
288
+ edges,
289
+ commitEdges,
290
+ imageRatio,
291
+ onChangeAspect
292
+ ]);
293
+ const activeHotspot = clampHotspot(hotspot, edges);
294
+ const handles = null === ratio ? [
295
+ ...CORNER_HANDLES,
296
+ ...EDGE_HANDLES
297
+ ] : CORNER_HANDLES;
298
+ const handlePosition = (handle)=>{
299
+ const cx = (edges.left + edges.right) / 2;
300
+ const cy = (edges.top + edges.bottom) / 2;
301
+ const x = handle.includes("w") ? edges.left : handle.includes("e") ? edges.right : cx;
302
+ const y = handle.includes("n") ? edges.top : handle.includes("s") ? edges.bottom : cy;
303
+ return {
304
+ left: `${100 * x}%`,
305
+ top: `${100 * y}%`
306
+ };
307
+ };
308
+ const cursorFor = (handle)=>{
309
+ const map = {
310
+ nw: "nwse-resize",
311
+ se: "nwse-resize",
312
+ ne: "nesw-resize",
313
+ sw: "nesw-resize",
314
+ n: "ns-resize",
315
+ s: "ns-resize",
316
+ e: "ew-resize",
317
+ w: "ew-resize"
318
+ };
319
+ return map[handle];
320
+ };
321
+ return /*#__PURE__*/ react.createElement("div", {
322
+ className: "flex flex-col gap-sm"
323
+ }, /*#__PURE__*/ react.createElement("div", {
324
+ className: "flex items-end justify-between gap-md"
325
+ }, /*#__PURE__*/ react.createElement(SectionLabel, null, "Crop & focal point"), /*#__PURE__*/ react.createElement("div", {
326
+ className: "w-[200px] shrink-0"
327
+ }, /*#__PURE__*/ react.createElement(Select, {
328
+ label: "Crop shape",
329
+ value: aspectId,
330
+ options: aspectOptions,
331
+ onChange: onSelectAspect,
332
+ displayResetAction: false
333
+ }))), /*#__PURE__*/ react.createElement("div", {
334
+ className: "flex justify-center rounded-md bg-neutral-dark p-lg"
335
+ }, /*#__PURE__*/ react.createElement("div", {
336
+ ref: wrapperRef,
337
+ className: "w-full"
338
+ }, /*#__PURE__*/ react.createElement("div", {
339
+ ref: surfaceRef,
340
+ onPointerMove: onPointerMove,
341
+ onPointerUp: endDrag,
342
+ className: "relative mx-auto touch-none select-none overflow-hidden rounded-sm",
343
+ style: surfaceStyle
344
+ }, /*#__PURE__*/ react.createElement("img", {
345
+ src: image.src,
346
+ alt: "",
347
+ draggable: false,
348
+ className: "pointer-events-none absolute inset-0 h-full w-full object-cover"
349
+ }), /*#__PURE__*/ react.createElement("div", {
350
+ onPointerDown: startMoveCrop,
351
+ className: "absolute cursor-move border border-white/90",
352
+ style: {
353
+ left: `${100 * edges.left}%`,
354
+ top: `${100 * edges.top}%`,
355
+ width: `${(edges.right - edges.left) * 100}%`,
356
+ height: `${(edges.bottom - edges.top) * 100}%`,
357
+ boxShadow: "0 0 0 1px rgba(0, 0, 0, 0.55), 0 0 0 9999px rgba(0, 0, 0, 0.45)"
358
+ }
359
+ }, /*#__PURE__*/ react.createElement("div", {
360
+ className: "pointer-events-none absolute inset-0"
361
+ }, /*#__PURE__*/ react.createElement("div", {
362
+ className: "absolute inset-y-0 left-1/3 w-px bg-white/30"
363
+ }), /*#__PURE__*/ react.createElement("div", {
364
+ className: "absolute inset-y-0 left-2/3 w-px bg-white/30"
365
+ }), /*#__PURE__*/ react.createElement("div", {
366
+ className: "absolute inset-x-0 top-1/3 h-px bg-white/30"
367
+ }), /*#__PURE__*/ react.createElement("div", {
368
+ className: "absolute inset-x-0 top-2/3 h-px bg-white/30"
369
+ })), /*#__PURE__*/ react.createElement("div", {
370
+ onPointerDown: startFocal,
371
+ className: "absolute flex h-8 w-8 -translate-x-1/2 -translate-y-1/2 cursor-grab touch-none items-center justify-center active:cursor-grabbing",
372
+ style: {
373
+ left: `${(activeHotspot.x - edges.left) / Math.max(MIN, edges.right - edges.left) * 100}%`,
374
+ top: `${(activeHotspot.y - edges.top) / Math.max(MIN, edges.bottom - edges.top) * 100}%`
375
+ }
376
+ }, /*#__PURE__*/ react.createElement("div", {
377
+ className: "pointer-events-none relative h-6 w-6 rounded-full border-2 border-white bg-white/25 shadow-lg"
378
+ }, /*#__PURE__*/ react.createElement("div", {
379
+ className: "absolute left-1/2 top-1/2 h-1.5 w-1.5 -translate-x-1/2 -translate-y-1/2 rounded-full bg-white"
380
+ })))), handles.map((handle)=>{
381
+ const pos = handlePosition(handle);
382
+ return /*#__PURE__*/ react.createElement("div", {
383
+ key: handle,
384
+ onPointerDown: startResize(handle),
385
+ className: "absolute flex h-6 w-6 -translate-x-1/2 -translate-y-1/2 touch-none items-center justify-center",
386
+ style: {
387
+ left: pos.left,
388
+ top: pos.top,
389
+ cursor: cursorFor(handle)
390
+ }
391
+ }, /*#__PURE__*/ react.createElement("div", {
392
+ className: "pointer-events-none h-3 w-3 rounded-sm border border-neutral-dark bg-white shadow"
393
+ }));
394
+ })))), /*#__PURE__*/ react.createElement(Text, {
395
+ as: "div",
396
+ size: "sm",
397
+ className: "text-neutral-strong"
398
+ }, "Drag the handles to crop, drag inside to reposition, and drag the circle to set the focal point."));
399
+ };
400
+ export { CropHotspotEditor, FREE, inferAspectId };
401
+
402
+ //# sourceMappingURL=CropHotspotEditor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ImageEditor/CropHotspotEditor.js","sources":["../../src/ImageEditor/CropHotspotEditor.tsx"],"sourcesContent":["import React, { useCallback, useLayoutEffect, useMemo, useRef, useState } from \"react\";\nimport { Select } from \"~/Select/index.js\";\nimport { Text } from \"~/Text/index.js\";\nimport { SectionLabel } from \"./SectionLabel.js\";\nimport { DEFAULT_ASPECT_RATIOS } from \"./types.js\";\nimport type { ImageEditorCrop, ImageEditorHotspot, ImageEditorImage } from \"./types.js\";\n\ninterface CropHotspotEditorProps {\n image: ImageEditorImage;\n crop: ImageEditorCrop | undefined;\n hotspot: ImageEditorHotspot | undefined;\n /** Currently selected crop shape id (\"free\" or a preset id). Controlled by the parent. */\n aspectId: string;\n onChangeAspect: (id: string) => void;\n onChangeCrop: (crop: ImageEditorCrop) => void;\n onChangeHotspot: (hotspot: ImageEditorHotspot) => void;\n}\n\n/** Crop rectangle in edge positions (0..1): left/top/right/bottom, left<right, top<bottom. */\ninterface Edges {\n left: number;\n top: number;\n right: number;\n bottom: number;\n}\n\ntype Handle = \"nw\" | \"n\" | \"ne\" | \"e\" | \"se\" | \"s\" | \"sw\" | \"w\";\n\nconst MIN = 0.05;\nexport const FREE = \"free\";\n\n/**\n * Infer which crop-shape preset a saved crop corresponds to, by comparing the\n * crop's on-screen (pixel) aspect ratio against the presets. The crop stores only\n * edge insets — not the shape used to draw it — so on reopen we recover the shape\n * here; a crop drawn with a preset matches it closely, while a free crop won't.\n */\nexport const inferAspectId = (\n crop: ImageEditorCrop | undefined,\n width: number,\n height: number\n): string => {\n if (!crop || !width || !height) {\n return FREE;\n }\n const cropWidth = Math.max(0, 1 - (crop.left ?? 0) - (crop.right ?? 0));\n const cropHeight = Math.max(0, 1 - (crop.top ?? 0) - (crop.bottom ?? 0));\n if (cropWidth <= 0 || cropHeight <= 0) {\n return FREE;\n }\n const displayRatio = (cropWidth * width) / (cropHeight * height);\n const match = DEFAULT_ASPECT_RATIOS.find(ar => Math.abs(ar.ratio - displayRatio) < 0.02);\n return match?.id ?? FREE;\n};\n\n// Canvas fits within this box (px), preserving the image aspect ratio, so tall\n// images don't take up the full screen height.\nconst MAX_CANVAS_WIDTH = 640;\nconst MAX_CANVAS_HEIGHT = 460;\n\nconst clamp01 = (n: number) => Math.min(1, Math.max(0, n));\n\nconst cropToEdges = (crop: ImageEditorCrop | undefined): Edges => ({\n left: clamp01(crop?.left ?? 0),\n top: clamp01(crop?.top ?? 0),\n right: 1 - clamp01(crop?.right ?? 0),\n bottom: 1 - clamp01(crop?.bottom ?? 0)\n});\n\nconst edgesToCrop = (e: Edges): ImageEditorCrop => ({\n top: e.top,\n left: e.left,\n bottom: 1 - e.bottom,\n right: 1 - e.right\n});\n\n// Which edges each handle moves.\nconst HANDLE_EDGES: Record<Handle, Array<keyof Edges>> = {\n nw: [\"top\", \"left\"],\n n: [\"top\"],\n ne: [\"top\", \"right\"],\n e: [\"right\"],\n se: [\"bottom\", \"right\"],\n s: [\"bottom\"],\n sw: [\"bottom\", \"left\"],\n w: [\"left\"]\n};\n\nconst CORNER_HANDLES: Handle[] = [\"nw\", \"ne\", \"se\", \"sw\"];\nconst EDGE_HANDLES: Handle[] = [\"n\", \"e\", \"s\", \"w\"];\n\nconst clampHotspot = (h: ImageEditorHotspot | undefined, e: Edges): ImageEditorHotspot => ({\n x: Math.min(e.right, Math.max(e.left, h?.x ?? (e.left + e.right) / 2)),\n y: Math.min(e.bottom, Math.max(e.top, h?.y ?? (e.top + e.bottom) / 2)),\n width: h?.width ?? 1,\n height: h?.height ?? 1\n});\n\nexport const CropHotspotEditor = ({\n image,\n crop,\n hotspot,\n aspectId,\n onChangeAspect,\n onChangeCrop,\n onChangeHotspot\n}: CropHotspotEditorProps) => {\n const surfaceRef = useRef<HTMLDivElement>(null);\n const dragRef = useRef<\n | { mode: \"focal\" }\n | { mode: `resize:${Handle}` }\n | { mode: \"move\"; startPointer: { x: number; y: number }; startEdges: Edges }\n | null\n >(null);\n\n // Measure the available width so we can fit the canvas into a max box while\n // preserving the image aspect ratio (keeps the box == image aspect, which the\n // pointer coordinate mapping relies on).\n const wrapperRef = useRef<HTMLDivElement>(null);\n const [availWidth, setAvailWidth] = useState(0);\n\n useLayoutEffect(() => {\n const el = wrapperRef.current;\n if (!el) {\n return;\n }\n const measure = () => setAvailWidth(el.getBoundingClientRect().width);\n measure();\n if (typeof ResizeObserver === \"undefined\") {\n return;\n }\n const ro = new ResizeObserver(measure);\n ro.observe(el);\n return () => ro.disconnect();\n }, []);\n\n const imageRatio = (image.width || 1) / (image.height || 1);\n const surfaceStyle = useMemo<React.CSSProperties>(() => {\n if (availWidth <= 0) {\n return {\n width: \"100%\",\n aspectRatio: `${image.width || 1} / ${image.height || 1}`,\n maxHeight: `${MAX_CANVAS_HEIGHT}px`\n };\n }\n let width = Math.min(availWidth, MAX_CANVAS_WIDTH);\n let height = width / imageRatio;\n if (height > MAX_CANVAS_HEIGHT) {\n height = MAX_CANVAS_HEIGHT;\n width = height * imageRatio;\n }\n return { width: `${width}px`, height: `${height}px` };\n }, [availWidth, imageRatio, image.width, image.height]);\n\n const edges = useMemo(() => cropToEdges(crop), [crop]);\n const ratio = DEFAULT_ASPECT_RATIOS.find(ar => ar.id === aspectId)?.ratio ?? null;\n\n const aspectOptions = useMemo(\n () => [\n { label: \"Free\", value: FREE },\n ...DEFAULT_ASPECT_RATIOS.map(ar => ({ label: ar.label, value: ar.id }))\n ],\n []\n );\n\n const toNorm = useCallback((clientX: number, clientY: number) => {\n const rect = surfaceRef.current!.getBoundingClientRect();\n return {\n x: clamp01((clientX - rect.left) / rect.width),\n y: clamp01((clientY - rect.top) / rect.height)\n };\n }, []);\n\n const commitEdges = useCallback(\n (next: Edges) => {\n onChangeCrop(edgesToCrop(next));\n // Keep the focal point inside the (new) crop.\n onChangeHotspot(clampHotspot(hotspot, next));\n },\n [onChangeCrop, onChangeHotspot, hotspot]\n );\n\n const resizeFree = (handle: Handle, p: { x: number; y: number }): Edges => {\n const moves = HANDLE_EDGES[handle];\n const next = { ...edges };\n if (moves.includes(\"left\")) {\n next.left = Math.min(p.x, next.right - MIN);\n }\n if (moves.includes(\"right\")) {\n next.right = Math.max(p.x, next.left + MIN);\n }\n if (moves.includes(\"top\")) {\n next.top = Math.min(p.y, next.bottom - MIN);\n }\n if (moves.includes(\"bottom\")) {\n next.bottom = Math.max(p.y, next.top + MIN);\n }\n return next;\n };\n\n const resizeLocked = (handle: Handle, p: { x: number; y: number }, r: number): Edges => {\n const moves = HANDLE_EDGES[handle];\n // `r` is a display (pixel) aspect ratio, but the crop is stored in normalized\n // image coordinates. Convert so the on-screen crop actually has ratio `r`.\n const rNorm = r / imageRatio;\n // Anchor is the opposite corner (fixed).\n const anchorX = moves.includes(\"left\") ? edges.right : edges.left;\n const anchorY = moves.includes(\"top\") ? edges.bottom : edges.top;\n const dirX = moves.includes(\"left\") ? -1 : 1;\n const dirY = moves.includes(\"top\") ? -1 : 1;\n\n const maxW = dirX > 0 ? 1 - anchorX : anchorX;\n const maxH = dirY > 0 ? 1 - anchorY : anchorY;\n\n let w = Math.abs(p.x - anchorX);\n let h = w / rNorm;\n if (h > Math.abs(p.y - anchorY)) {\n // The vertical drag is the limiting axis.\n h = Math.abs(p.y - anchorY);\n w = h * rNorm;\n }\n // Fit inside the image while keeping the ratio.\n w = Math.min(w, maxW);\n h = w / rNorm;\n if (h > maxH) {\n h = maxH;\n w = h * rNorm;\n }\n w = Math.max(w, MIN);\n h = Math.max(h, MIN);\n\n const next = { ...edges };\n if (dirX > 0) {\n next.left = anchorX;\n next.right = anchorX + w;\n } else {\n next.right = anchorX;\n next.left = anchorX - w;\n }\n if (dirY > 0) {\n next.top = anchorY;\n next.bottom = anchorY + h;\n } else {\n next.bottom = anchorY;\n next.top = anchorY - h;\n }\n return next;\n };\n\n const moveCrop = (startEdges: Edges, dx: number, dy: number): Edges => {\n const w = startEdges.right - startEdges.left;\n const h = startEdges.bottom - startEdges.top;\n const left = Math.min(Math.max(startEdges.left + dx, 0), 1 - w);\n const top = Math.min(Math.max(startEdges.top + dy, 0), 1 - h);\n return { left, top, right: left + w, bottom: top + h };\n };\n\n const onPointerMove = useCallback(\n (e: React.PointerEvent<HTMLDivElement>) => {\n const drag = dragRef.current;\n if (!drag) {\n return;\n }\n const p = toNorm(e.clientX, e.clientY);\n if (drag.mode === \"focal\") {\n onChangeHotspot(clampHotspot({ x: p.x, y: p.y, width: 1, height: 1 }, edges));\n return;\n }\n if (drag.mode === \"move\") {\n const dx = p.x - drag.startPointer.x;\n const dy = p.y - drag.startPointer.y;\n commitEdges(moveCrop(drag.startEdges, dx, dy));\n return;\n }\n const handle = drag.mode.slice(\"resize:\".length) as Handle;\n commitEdges(ratio === null ? resizeFree(handle, p) : resizeLocked(handle, p, ratio));\n },\n [toNorm, onChangeHotspot, edges, commitEdges, ratio]\n );\n\n // Drag inside the crop rectangle repositions the whole crop window.\n const startMoveCrop = useCallback(\n (e: React.PointerEvent<HTMLDivElement>) => {\n dragRef.current = {\n mode: \"move\",\n startPointer: toNorm(e.clientX, e.clientY),\n startEdges: edges\n };\n surfaceRef.current?.setPointerCapture(e.pointerId);\n },\n [toNorm, edges]\n );\n\n // Dragging the marker itself moves the focal point (and only that).\n const startFocal = useCallback((e: React.PointerEvent<HTMLDivElement>) => {\n e.stopPropagation();\n dragRef.current = { mode: \"focal\" };\n surfaceRef.current?.setPointerCapture(e.pointerId);\n }, []);\n\n const startResize = useCallback(\n (handle: Handle) => (e: React.PointerEvent<HTMLDivElement>) => {\n e.stopPropagation();\n dragRef.current = { mode: `resize:${handle}` };\n surfaceRef.current?.setPointerCapture(e.pointerId);\n },\n []\n );\n\n const endDrag = useCallback((e: React.PointerEvent<HTMLDivElement>) => {\n dragRef.current = null;\n surfaceRef.current?.releasePointerCapture(e.pointerId);\n }, []);\n\n const onSelectAspect = useCallback(\n (value: string) => {\n onChangeAspect(value);\n const r = DEFAULT_ASPECT_RATIOS.find(ar => ar.id === value)?.ratio;\n if (value === FREE || !r) {\n return;\n }\n // `r` is a display (pixel) aspect ratio; convert to a normalized ratio\n // so the crop, stored in normalized image coordinates, renders as `r`.\n const rNorm = r / imageRatio;\n // Re-fit the current crop to the chosen ratio, centered within it.\n const cw = edges.right - edges.left;\n const ch = edges.bottom - edges.top;\n let w = cw;\n let h = w / rNorm;\n if (h > ch) {\n h = ch;\n w = h * rNorm;\n }\n const cx = (edges.left + edges.right) / 2;\n const cy = (edges.top + edges.bottom) / 2;\n commitEdges({\n left: clamp01(cx - w / 2),\n right: clamp01(cx + w / 2),\n top: clamp01(cy - h / 2),\n bottom: clamp01(cy + h / 2)\n });\n },\n [edges, commitEdges, imageRatio, onChangeAspect]\n );\n\n const activeHotspot = clampHotspot(hotspot, edges);\n const handles = ratio === null ? [...CORNER_HANDLES, ...EDGE_HANDLES] : CORNER_HANDLES;\n\n const handlePosition = (handle: Handle): { left: string; top: string } => {\n const cx = (edges.left + edges.right) / 2;\n const cy = (edges.top + edges.bottom) / 2;\n const x = handle.includes(\"w\") ? edges.left : handle.includes(\"e\") ? edges.right : cx;\n const y = handle.includes(\"n\") ? edges.top : handle.includes(\"s\") ? edges.bottom : cy;\n return { left: `${x * 100}%`, top: `${y * 100}%` };\n };\n\n const cursorFor = (handle: Handle): string => {\n const map: Record<Handle, string> = {\n nw: \"nwse-resize\",\n se: \"nwse-resize\",\n ne: \"nesw-resize\",\n sw: \"nesw-resize\",\n n: \"ns-resize\",\n s: \"ns-resize\",\n e: \"ew-resize\",\n w: \"ew-resize\"\n };\n return map[handle];\n };\n\n return (\n <div className={\"flex flex-col gap-sm\"}>\n <div className={\"flex items-end justify-between gap-md\"}>\n <SectionLabel>Crop &amp; focal point</SectionLabel>\n <div className={\"w-[200px] shrink-0\"}>\n <Select\n label={\"Crop shape\"}\n value={aspectId}\n options={aspectOptions}\n onChange={onSelectAspect}\n displayResetAction={false}\n />\n </div>\n </div>\n\n {/* Dark editing stage: the image sits centered on a near-black backdrop\n (like a pro image editor), so letterboxing around portrait images\n reads as intentional and the image stays the focal element. */}\n <div className={\"flex justify-center rounded-md bg-neutral-dark p-lg\"}>\n <div ref={wrapperRef} className={\"w-full\"}>\n <div\n ref={surfaceRef}\n onPointerMove={onPointerMove}\n onPointerUp={endDrag}\n className={\n \"relative mx-auto touch-none select-none overflow-hidden rounded-sm\"\n }\n style={surfaceStyle}\n >\n <img\n src={image.src}\n alt={\"\"}\n draggable={false}\n className={\n \"pointer-events-none absolute inset-0 h-full w-full object-cover\"\n }\n />\n\n {/* Crop rectangle: dims outside via box-shadow; interior drag moves the\n crop. The border is two-tone (white edge + a 1px dark ring) so it stays\n visible over both light and dark image areas. */}\n <div\n onPointerDown={startMoveCrop}\n className={\"absolute cursor-move border border-white/90\"}\n style={{\n left: `${edges.left * 100}%`,\n top: `${edges.top * 100}%`,\n width: `${(edges.right - edges.left) * 100}%`,\n height: `${(edges.bottom - edges.top) * 100}%`,\n boxShadow:\n \"0 0 0 1px rgba(0, 0, 0, 0.55), 0 0 0 9999px rgba(0, 0, 0, 0.45)\"\n }}\n >\n {/* Rule-of-thirds guides — a compositional aid and a cue for placing\n the focal point. */}\n <div className={\"pointer-events-none absolute inset-0\"}>\n <div className={\"absolute inset-y-0 left-1/3 w-px bg-white/30\"} />\n <div className={\"absolute inset-y-0 left-2/3 w-px bg-white/30\"} />\n <div className={\"absolute inset-x-0 top-1/3 h-px bg-white/30\"} />\n <div className={\"absolute inset-x-0 top-2/3 h-px bg-white/30\"} />\n </div>\n\n {/* Focal marker — the visible ring is small, but the draggable hit\n area around it is larger for easier grabbing. */}\n <div\n onPointerDown={startFocal}\n className={\n \"absolute flex h-8 w-8 -translate-x-1/2 -translate-y-1/2 cursor-grab touch-none items-center justify-center active:cursor-grabbing\"\n }\n style={{\n left: `${((activeHotspot.x - edges.left) / Math.max(MIN, edges.right - edges.left)) * 100}%`,\n top: `${((activeHotspot.y - edges.top) / Math.max(MIN, edges.bottom - edges.top)) * 100}%`\n }}\n >\n <div\n className={\n \"pointer-events-none relative h-6 w-6 rounded-full border-2 border-white bg-white/25 shadow-lg\"\n }\n >\n <div\n className={\n \"absolute left-1/2 top-1/2 h-1.5 w-1.5 -translate-x-1/2 -translate-y-1/2 rounded-full bg-white\"\n }\n />\n </div>\n </div>\n </div>\n\n {/* Resize handles — small visible dot inside a larger transparent hit\n area so the corners/edges are easy to grab. */}\n {handles.map(handle => {\n const pos = handlePosition(handle);\n return (\n <div\n key={handle}\n onPointerDown={startResize(handle)}\n className={\n \"absolute flex h-6 w-6 -translate-x-1/2 -translate-y-1/2 touch-none items-center justify-center\"\n }\n style={{\n left: pos.left,\n top: pos.top,\n cursor: cursorFor(handle)\n }}\n >\n <div\n className={\n \"pointer-events-none h-3 w-3 rounded-sm border border-neutral-dark bg-white shadow\"\n }\n />\n </div>\n );\n })}\n </div>\n </div>\n </div>\n\n <Text as={\"div\"} size={\"sm\"} className={\"text-neutral-strong\"}>\n Drag the handles to crop, drag inside to reposition, and drag the circle to set the\n focal point.\n </Text>\n </div>\n );\n};\n"],"names":["MIN","FREE","inferAspectId","crop","width","height","cropWidth","Math","cropHeight","displayRatio","match","DEFAULT_ASPECT_RATIOS","ar","MAX_CANVAS_WIDTH","MAX_CANVAS_HEIGHT","clamp01","n","cropToEdges","edgesToCrop","e","HANDLE_EDGES","CORNER_HANDLES","EDGE_HANDLES","clampHotspot","h","CropHotspotEditor","image","hotspot","aspectId","onChangeAspect","onChangeCrop","onChangeHotspot","surfaceRef","useRef","dragRef","wrapperRef","availWidth","setAvailWidth","useState","useLayoutEffect","el","measure","ResizeObserver","ro","imageRatio","surfaceStyle","useMemo","edges","ratio","aspectOptions","toNorm","useCallback","clientX","clientY","rect","commitEdges","next","resizeFree","handle","p","moves","resizeLocked","r","rNorm","anchorX","anchorY","dirX","dirY","maxW","maxH","w","moveCrop","startEdges","dx","dy","left","top","onPointerMove","drag","startMoveCrop","startFocal","startResize","endDrag","onSelectAspect","value","cw","ch","cx","cy","activeHotspot","handles","handlePosition","x","y","cursorFor","map","SectionLabel","Select","pos","Text"],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ import React from "react";
2
+ import { type ImageEditorAspectRatio, type ImageEditorImage, type ImageEditorValue } from "./types.js";
3
+ export interface ImageEditorProps {
4
+ open: boolean;
5
+ onClose: () => void;
6
+ /** The source image to edit. */
7
+ image: ImageEditorImage;
8
+ /** The current edit (crop/hotspot/alt/caption), if any. */
9
+ value?: ImageEditorValue;
10
+ /** Called with the new edit when the user saves. */
11
+ onSave: (value: ImageEditorValue) => void;
12
+ /** Preview ratios shown in the strip. Defaults to square / 4:3 / 16:9. */
13
+ aspectRatios?: ImageEditorAspectRatio[];
14
+ showAlt?: boolean;
15
+ showCaption?: boolean;
16
+ title?: React.ReactNode;
17
+ }
18
+ export declare const ImageEditor: ({ open, onClose, image, value, onSave, aspectRatios, showAlt, showCaption, title }: ImageEditorProps) => React.JSX.Element;
@@ -0,0 +1,151 @@
1
+ import react, { useEffect, useState } from "react";
2
+ import { ReactComponent } from "@webiny/icons/arrow_drop_down.svg";
3
+ import { Dialog } from "../Dialog/index.js";
4
+ import { Button } from "../Button/index.js";
5
+ import { DropdownMenu } from "../DropdownMenu/index.js";
6
+ import { CropHotspotEditor, FREE, inferAspectId } from "./CropHotspotEditor.js";
7
+ import { AspectRatioPreview } from "./AspectRatioPreview.js";
8
+ import { MetaFields } from "./MetaFields.js";
9
+ import { SectionLabel } from "./SectionLabel.js";
10
+ import { DEFAULT_ASPECT_RATIOS, DEFAULT_PREVIEW_RATIO_IDS } from "./types.js";
11
+ const isFullCrop = (crop)=>{
12
+ if (!crop) return true;
13
+ return 0 === crop.top && 0 === crop.left && 0 === crop.bottom && 0 === crop.right;
14
+ };
15
+ const isCenteredHotspot = (hotspot)=>!hotspot || 0.5 === hotspot.x && 0.5 === hotspot.y;
16
+ const ImageEditor = ({ open, onClose, image, value, onSave, aspectRatios = DEFAULT_ASPECT_RATIOS, showAlt = true, showCaption = true, title = "Edit image" })=>{
17
+ const [crop, setCrop] = useState(value?.crop);
18
+ const [hotspot, setHotspot] = useState(value?.hotspot);
19
+ const [alt, setAlt] = useState(value?.alt ?? "");
20
+ const [caption, setCaption] = useState(value?.caption ?? "");
21
+ const [aspectId, setAspectId] = useState(FREE);
22
+ const [selectedRatioIds, setSelectedRatioIds] = useState(()=>{
23
+ const defaults = aspectRatios.filter((ar)=>DEFAULT_PREVIEW_RATIO_IDS.includes(ar.id)).map((ar)=>ar.id);
24
+ return defaults.length > 0 ? defaults : aspectRatios.map((ar)=>ar.id);
25
+ });
26
+ const toggleRatio = (id)=>setSelectedRatioIds((prev)=>prev.includes(id) ? prev.filter((x)=>x !== id) : [
27
+ ...prev,
28
+ id
29
+ ]);
30
+ const visibleRatios = aspectRatios.filter((ar)=>selectedRatioIds.includes(ar.id));
31
+ const [naturalSize, setNaturalSize] = useState(null);
32
+ useEffect(()=>{
33
+ if (!open || !image.src || image.width > 0 && image.height > 0) return;
34
+ if ("u" < typeof document) return;
35
+ const probe = document.createElement("img");
36
+ probe.onload = ()=>setNaturalSize({
37
+ width: probe.naturalWidth,
38
+ height: probe.naturalHeight
39
+ });
40
+ probe.src = image.src;
41
+ return ()=>{
42
+ probe.onload = null;
43
+ };
44
+ }, [
45
+ open,
46
+ image.src,
47
+ image.width,
48
+ image.height
49
+ ]);
50
+ const effectiveImage = {
51
+ src: image.src,
52
+ width: image.width > 0 ? image.width : naturalSize?.width ?? 0,
53
+ height: image.height > 0 ? image.height : naturalSize?.height ?? 0
54
+ };
55
+ useEffect(()=>{
56
+ if (open) {
57
+ setCrop(value?.crop);
58
+ setHotspot(value?.hotspot);
59
+ setAlt(value?.alt ?? "");
60
+ setCaption(value?.caption ?? "");
61
+ }
62
+ }, [
63
+ open,
64
+ value
65
+ ]);
66
+ useEffect(()=>{
67
+ if (open) setAspectId(inferAspectId(value?.crop, effectiveImage.width, effectiveImage.height));
68
+ }, [
69
+ open,
70
+ value,
71
+ effectiveImage.width,
72
+ effectiveImage.height
73
+ ]);
74
+ const handleSave = ()=>{
75
+ const next = {};
76
+ if (!isFullCrop(crop)) next.crop = crop;
77
+ if (!isCenteredHotspot(hotspot)) next.hotspot = hotspot;
78
+ if (showAlt && alt.trim()) next.alt = alt.trim();
79
+ if (showCaption && caption.trim()) next.caption = caption.trim();
80
+ onSave(next);
81
+ onClose();
82
+ };
83
+ return /*#__PURE__*/ react.createElement(Dialog, {
84
+ open: open,
85
+ onClose: onClose,
86
+ title: title,
87
+ size: "lg",
88
+ actions: /*#__PURE__*/ react.createElement("div", {
89
+ className: "flex w-full items-center justify-end gap-sm"
90
+ }, /*#__PURE__*/ react.createElement(Button, {
91
+ variant: "secondary",
92
+ text: "Cancel",
93
+ onClick: onClose
94
+ }), /*#__PURE__*/ react.createElement(Button, {
95
+ variant: "primary",
96
+ text: "Save",
97
+ onClick: handleSave
98
+ }))
99
+ }, /*#__PURE__*/ react.createElement("div", {
100
+ className: "flex flex-col gap-lg"
101
+ }, /*#__PURE__*/ react.createElement(CropHotspotEditor, {
102
+ image: effectiveImage,
103
+ crop: crop,
104
+ hotspot: hotspot,
105
+ aspectId: aspectId,
106
+ onChangeAspect: setAspectId,
107
+ onChangeCrop: setCrop,
108
+ onChangeHotspot: setHotspot
109
+ }), /*#__PURE__*/ react.createElement("div", {
110
+ className: "flex flex-col gap-sm"
111
+ }, /*#__PURE__*/ react.createElement("div", {
112
+ className: "flex items-center justify-between"
113
+ }, /*#__PURE__*/ react.createElement(SectionLabel, null, "Preview"), /*#__PURE__*/ react.createElement(DropdownMenu, {
114
+ trigger: /*#__PURE__*/ react.createElement(Button, {
115
+ variant: "secondary",
116
+ text: `Preview at (${visibleRatios.length})`,
117
+ icon: /*#__PURE__*/ react.createElement(ReactComponent, null),
118
+ iconPosition: "end"
119
+ })
120
+ }, aspectRatios.map((ar)=>/*#__PURE__*/ react.createElement(DropdownMenu.CheckboxItem, {
121
+ key: ar.id,
122
+ text: ar.label,
123
+ checked: selectedRatioIds.includes(ar.id),
124
+ onCheckedChange: ()=>toggleRatio(ar.id),
125
+ onSelect: (e)=>e.preventDefault()
126
+ })))), visibleRatios.length > 0 ? /*#__PURE__*/ react.createElement("div", {
127
+ className: "flex flex-wrap items-start gap-md"
128
+ }, visibleRatios.map((ar)=>/*#__PURE__*/ react.createElement(AspectRatioPreview, {
129
+ key: ar.id,
130
+ src: effectiveImage.src,
131
+ imageWidth: effectiveImage.width,
132
+ imageHeight: effectiveImage.height,
133
+ crop: crop,
134
+ hotspot: hotspot,
135
+ aspectRatio: ar
136
+ }))) : /*#__PURE__*/ react.createElement("div", {
137
+ className: "text-sm text-neutral-strong"
138
+ }, "Select at least one shape to preview.")), showAlt || showCaption ? /*#__PURE__*/ react.createElement("div", {
139
+ className: "flex flex-col gap-sm"
140
+ }, /*#__PURE__*/ react.createElement(SectionLabel, null, "Details"), /*#__PURE__*/ react.createElement(MetaFields, {
141
+ alt: alt,
142
+ caption: caption,
143
+ showAlt: showAlt,
144
+ showCaption: showCaption,
145
+ onChangeAlt: setAlt,
146
+ onChangeCaption: setCaption
147
+ })) : null));
148
+ };
149
+ export { ImageEditor };
150
+
151
+ //# sourceMappingURL=ImageEditor.js.map