@webiny/admin-ui 6.6.0-alpha.0 → 6.6.0-alpha.2

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 (94) hide show
  1. package/Accordion/components/AccordionContent.js +4 -1
  2. package/Accordion/components/AccordionContent.js.map +1 -1
  3. package/Accordion/components/AccordionItem.d.ts +1 -0
  4. package/Accordion/components/AccordionItem.js.map +1 -1
  5. package/Accordion/components/AccordionItemDragHandle.d.ts +2 -1
  6. package/Accordion/components/AccordionItemDragHandle.js +2 -1
  7. package/Accordion/components/AccordionItemDragHandle.js.map +1 -1
  8. package/Accordion/components/AccordionTrigger.js +4 -2
  9. package/Accordion/components/AccordionTrigger.js.map +1 -1
  10. package/BetaBadge/BetaBadge.d.ts +6 -0
  11. package/BetaBadge/BetaBadge.js +7 -0
  12. package/BetaBadge/BetaBadge.js.map +1 -0
  13. package/Breadcrumbs/Breadcrumbs.d.ts +54 -0
  14. package/Breadcrumbs/Breadcrumbs.js +66 -0
  15. package/Breadcrumbs/Breadcrumbs.js.map +1 -0
  16. package/Breadcrumbs/Breadcrumbs.stories.d.ts +9 -0
  17. package/Breadcrumbs/Breadcrumbs.stories.js +71 -0
  18. package/Breadcrumbs/Breadcrumbs.stories.js.map +1 -0
  19. package/Breadcrumbs/index.d.ts +1 -0
  20. package/Breadcrumbs/index.js +1 -0
  21. package/Calendar/calendarClassNames.js +1 -1
  22. package/Calendar/calendarClassNames.js.map +1 -1
  23. package/CodeEditor/CodeEditorPrimitive.js +3 -1
  24. package/CodeEditor/CodeEditorPrimitive.js.map +1 -1
  25. package/DataTable/DataTable.d.ts +16 -6
  26. package/DataTable/DataTable.js +18 -7
  27. package/DataTable/DataTable.js.map +1 -1
  28. package/DataTable/components/ColumnsVisibility.d.ts +5 -4
  29. package/DataTable/components/ColumnsVisibility.js +7 -3
  30. package/DataTable/components/ColumnsVisibility.js.map +1 -1
  31. package/DatePicker/primitives/DateTimePicker.d.ts +1 -1
  32. package/DatePicker/primitives/DateTimePicker.js +15 -3
  33. package/DatePicker/primitives/DateTimePicker.js.map +1 -1
  34. package/DatePicker/primitives/components/DatePickerTrigger.js +1 -1
  35. package/DatePicker/primitives/components/DatePickerTrigger.js.map +1 -1
  36. package/DatePicker/utils/types.d.ts +2 -0
  37. package/DropdownMenu/components/DropdownMenuItem.d.ts +5 -0
  38. package/DropdownMenu/components/DropdownMenuItem.js +3 -2
  39. package/DropdownMenu/components/DropdownMenuItem.js.map +1 -1
  40. package/FilePicker/FilePicker.d.ts +12 -12
  41. package/FilePicker/primitives/FilePickerPrimitive.d.ts +12 -12
  42. package/FilePicker/primitives/components/previews/ImagePreview/ImagePreview.d.ts +13 -2
  43. package/FilePicker/primitives/components/previews/ImagePreview/ImagePreview.js +15 -6
  44. package/FilePicker/primitives/components/previews/ImagePreview/ImagePreview.js.map +1 -1
  45. package/FilePicker/primitives/components/previews/RichItemPreview/RichItemPreview.d.ts +6 -1
  46. package/FilePicker/primitives/components/previews/RichItemPreview/RichItemPreview.js +3 -2
  47. package/FilePicker/primitives/components/previews/RichItemPreview/RichItemPreview.js.map +1 -1
  48. package/FilePicker/primitives/components/previews/RichItemPreview/RichItemThumbnail.d.ts +10 -1
  49. package/FilePicker/primitives/components/previews/RichItemPreview/RichItemThumbnail.js +6 -2
  50. package/FilePicker/primitives/components/previews/RichItemPreview/RichItemThumbnail.js.map +1 -1
  51. package/ImageEditor/AspectRatioPreview.d.ts +16 -0
  52. package/ImageEditor/AspectRatioPreview.js +26 -0
  53. package/ImageEditor/AspectRatioPreview.js.map +1 -0
  54. package/ImageEditor/CropHotspotEditor.d.ts +22 -0
  55. package/ImageEditor/CropHotspotEditor.js +402 -0
  56. package/ImageEditor/CropHotspotEditor.js.map +1 -0
  57. package/ImageEditor/ImageEditor.d.ts +18 -0
  58. package/ImageEditor/ImageEditor.js +151 -0
  59. package/ImageEditor/ImageEditor.js.map +1 -0
  60. package/ImageEditor/ImageEditor.stories.d.ts +6 -0
  61. package/ImageEditor/ImageEditor.stories.js +42 -0
  62. package/ImageEditor/ImageEditor.stories.js.map +1 -0
  63. package/ImageEditor/MetaFields.d.ts +12 -0
  64. package/ImageEditor/MetaFields.js +20 -0
  65. package/ImageEditor/MetaFields.js.map +1 -0
  66. package/ImageEditor/SectionLabel.d.ts +8 -0
  67. package/ImageEditor/SectionLabel.js +7 -0
  68. package/ImageEditor/SectionLabel.js.map +1 -0
  69. package/ImageEditor/geometry.d.ts +46 -0
  70. package/ImageEditor/geometry.js +126 -0
  71. package/ImageEditor/geometry.js.map +1 -0
  72. package/ImageEditor/index.d.ts +3 -0
  73. package/ImageEditor/index.js +3 -0
  74. package/ImageEditor/types.d.ts +52 -0
  75. package/ImageEditor/types.js +35 -0
  76. package/ImageEditor/types.js.map +1 -0
  77. package/Markdown/Markdown.d.ts +47 -0
  78. package/Markdown/Markdown.js +17 -0
  79. package/Markdown/Markdown.js.map +1 -0
  80. package/Markdown/index.d.ts +1 -0
  81. package/Markdown/index.js +1 -0
  82. package/MultiFilePicker/MultiFilePicker.d.ts +12 -12
  83. package/MultiFilePicker/primitives/MultiFilePickerPrimitive.d.ts +12 -12
  84. package/MultiSelect/primitives/MultiSelectPrimitive.js +1 -1
  85. package/MultiSelect/primitives/MultiSelectPrimitive.js.map +1 -1
  86. package/Select/primitives/components/SelectContent.js +1 -1
  87. package/Select/primitives/components/SelectContent.js.map +1 -1
  88. package/Toast/useToast.stories.js.map +1 -1
  89. package/exports/admin/ui.d.ts +1 -0
  90. package/exports/admin/ui.js +1 -0
  91. package/index.d.ts +4 -0
  92. package/index.js +4 -0
  93. package/package.json +18 -18
  94. package/theme.css +9 -0
@@ -1 +1 @@
1
- {"version":3,"file":"FilePicker/primitives/components/previews/ImagePreview/ImagePreview.js","sources":["../../../../../../src/FilePicker/primitives/components/previews/ImagePreview/ImagePreview.tsx"],"sourcesContent":["import React from \"react\";\nimport { cn, makeDecoratable } from \"~/utils.js\";\nimport { previewVariants } from \"../variants.js\";\nimport type { FilePreviewDefaultProps } from \"../../types.js\";\nimport { ItemActions } from \"~/FilePicker/primitives/components/previews/ItemActions.js\";\n\ntype ImagePreviewProps = FilePreviewDefaultProps;\n\nconst DecoratableImagePreview = ({\n value,\n className,\n variant,\n disabled,\n onRemoveItem,\n onReplaceItem,\n onEditItem\n}: ImagePreviewProps) => {\n return (\n <div\n className={cn(\n \"flex justify-center items-center py-sm rounded-md relative\",\n previewVariants({ variant }),\n className\n )}\n data-testid={\"image-preview\"}\n >\n <div\n className={\"cursor-pointer size-[128px] flex justify-center items-center\"}\n data-role={\"select-image\"}\n onClick={onReplaceItem}\n >\n <img src={value.url} alt={value.name} className={\"object-contain size-full\"} />\n </div>\n <div className={\"absolute top-1 right-1.5\"}>\n <ItemActions\n onRemoveItem={onRemoveItem}\n onEditItem={onEditItem}\n disabled={disabled}\n className={\"flex-col\"}\n />\n </div>\n </div>\n );\n};\n\nconst ImagePreview = makeDecoratable(\"FileImagePreview\", DecoratableImagePreview);\n\nexport { ImagePreview, type ImagePreviewProps };\n"],"names":["DecoratableImagePreview","value","className","variant","disabled","onRemoveItem","onReplaceItem","onEditItem","cn","previewVariants","ItemActions","ImagePreview","makeDecoratable"],"mappings":";;;;AAQA,MAAMA,0BAA0B,CAAC,EAC7BC,KAAK,EACLC,SAAS,EACTC,OAAO,EACPC,QAAQ,EACRC,YAAY,EACZC,aAAa,EACbC,UAAU,EACM,GACT,WAAP,GACI,oBAAC;QACG,WAAWC,GACP,8DACAC,gBAAgB;YAAEN;QAAQ,IAC1BD;QAEJ,eAAa;qBAEb,oBAAC;QACG,WAAW;QACX,aAAW;QACX,SAASI;qBAET,oBAAC;QAAI,KAAKL,MAAM,GAAG;QAAE,KAAKA,MAAM,IAAI;QAAE,WAAW;uBAErD,oBAAC;QAAI,WAAW;qBACZ,oBAACS,aAAWA;QACR,cAAcL;QACd,YAAYE;QACZ,UAAUH;QACV,WAAW;;AAO/B,MAAMO,eAAeC,gBAAgB,oBAAoBZ"}
1
+ {"version":3,"file":"FilePicker/primitives/components/previews/ImagePreview/ImagePreview.js","sources":["../../../../../../src/FilePicker/primitives/components/previews/ImagePreview/ImagePreview.tsx"],"sourcesContent":["import React from \"react\";\nimport { cn, makeDecoratable } from \"~/utils.js\";\nimport { previewVariants } from \"../variants.js\";\nimport type { FilePreviewDefaultProps } from \"../../types.js\";\nimport { ItemActions } from \"~/FilePicker/primitives/components/previews/ItemActions.js\";\n\ntype ImagePreviewProps = FilePreviewDefaultProps & {\n /**\n * Optional custom image renderer. Lets a consumer render, e.g., a cropped /\n * focal-point-aware thumbnail while keeping all of the preview chrome. When\n * omitted (or it returns a falsy value) a plain `<img>` is rendered.\n */\n renderImage?: (args: { url: string; name: string; className: string }) => React.ReactNode;\n};\n\nconst IMAGE_CLASS = \"object-contain size-full\";\n\nconst DecoratableImagePreview = ({\n value,\n className,\n variant,\n disabled,\n onRemoveItem,\n onReplaceItem,\n onEditItem,\n renderImage\n}: ImagePreviewProps) => {\n let image: React.ReactElement | React.ReactNode = (\n <img src={value.url} alt={value.name} className={IMAGE_CLASS} />\n );\n\n if (typeof renderImage === \"function\") {\n image = renderImage({ url: value.url, name: value.name, className: IMAGE_CLASS });\n }\n\n return (\n <div\n className={cn(\n \"flex justify-center items-center py-sm rounded-md relative\",\n previewVariants({ variant }),\n className\n )}\n data-testid={\"image-preview\"}\n >\n <div\n className={\"cursor-pointer size-[128px] flex justify-center items-center\"}\n data-role={\"select-image\"}\n onClick={onReplaceItem}\n >\n {image}\n </div>\n <div className={\"absolute top-1 right-1.5\"}>\n <ItemActions\n onRemoveItem={onRemoveItem}\n onEditItem={onEditItem}\n disabled={disabled}\n className={\"flex-col\"}\n />\n </div>\n </div>\n );\n};\n\nconst ImagePreview = makeDecoratable(\"FileImagePreview\", DecoratableImagePreview);\n\nexport { ImagePreview, type ImagePreviewProps };\n"],"names":["IMAGE_CLASS","DecoratableImagePreview","value","className","variant","disabled","onRemoveItem","onReplaceItem","onEditItem","renderImage","image","cn","previewVariants","ItemActions","ImagePreview","makeDecoratable"],"mappings":";;;;AAeA,MAAMA,cAAc;AAEpB,MAAMC,0BAA0B,CAAC,EAC7BC,KAAK,EACLC,SAAS,EACTC,OAAO,EACPC,QAAQ,EACRC,YAAY,EACZC,aAAa,EACbC,UAAU,EACVC,WAAW,EACK;IAChB,IAAIC,QAAAA,WAAAA,GACA,oBAAC;QAAI,KAAKR,MAAM,GAAG;QAAE,KAAKA,MAAM,IAAI;QAAE,WAAWF;;IAGrD,IAAI,AAAuB,cAAvB,OAAOS,aACPC,QAAQD,YAAY;QAAE,KAAKP,MAAM,GAAG;QAAE,MAAMA,MAAM,IAAI;QAAE,WAAWF;IAAY;IAGnF,OAAO,WAAP,GACI,oBAAC;QACG,WAAWW,GACP,8DACAC,gBAAgB;YAAER;QAAQ,IAC1BD;QAEJ,eAAa;qBAEb,oBAAC;QACG,WAAW;QACX,aAAW;QACX,SAASI;OAERG,QAAAA,WAAAA,GAEL,oBAAC;QAAI,WAAW;qBACZ,oBAACG,aAAWA;QACR,cAAcP;QACd,YAAYE;QACZ,UAAUH;QACV,WAAW;;AAK/B;AAEA,MAAMS,eAAeC,gBAAgB,oBAAoBd"}
@@ -2,8 +2,13 @@ import React from "react";
2
2
  import type { FilePreviewDefaultProps } from "../../types.js";
3
3
  type RichItemPreviewProps = FilePreviewDefaultProps & {
4
4
  preview?: "thumbnail" | "file-type" | "placeholder";
5
+ /** Optional custom image renderer for the thumbnail (e.g. a cropped preview). */
6
+ renderImage?: (args: {
7
+ url: string;
8
+ name: string;
9
+ }) => React.ReactNode;
5
10
  };
6
- declare const DecoratableRichItemPreview: ({ className, disabled, onRemoveItem, onReplaceItem, onEditItem, value, variant, preview, ...props }: RichItemPreviewProps) => React.JSX.Element;
11
+ declare const DecoratableRichItemPreview: ({ className, disabled, onRemoveItem, onReplaceItem, onEditItem, value, variant, preview, renderImage, ...props }: RichItemPreviewProps) => React.JSX.Element;
7
12
  declare const RichItemPreview: typeof DecoratableRichItemPreview & {
8
13
  original: typeof DecoratableRichItemPreview;
9
14
  originalName: string;
@@ -4,7 +4,7 @@ import { RichItemThumbnail } from "./RichItemThumbnail.js";
4
4
  import { ItemDescription } from "../ItemDescription.js";
5
5
  import { ItemActions } from "../ItemActions.js";
6
6
  import { previewVariants } from "../variants.js";
7
- const DecoratableRichItemPreview = ({ className, disabled, onRemoveItem, onReplaceItem, onEditItem, value, variant, preview, ...props })=>/*#__PURE__*/ react.createElement("div", {
7
+ const DecoratableRichItemPreview = ({ className, disabled, onRemoveItem, onReplaceItem, onEditItem, value, variant, preview, renderImage, ...props })=>/*#__PURE__*/ react.createElement("div", {
8
8
  "data-testid": "image-preview",
9
9
  className: cn("w-full rounded-md", previewVariants({
10
10
  variant,
@@ -20,7 +20,8 @@ const DecoratableRichItemPreview = ({ className, disabled, onRemoveItem, onRepla
20
20
  }, /*#__PURE__*/ react.createElement(RichItemThumbnail, {
21
21
  ...value,
22
22
  disabled: disabled,
23
- preview: preview
23
+ preview: preview,
24
+ renderImage: renderImage
24
25
  }), /*#__PURE__*/ react.createElement(ItemDescription, {
25
26
  item: value,
26
27
  disabled: disabled
@@ -1 +1 @@
1
- {"version":3,"file":"FilePicker/primitives/components/previews/RichItemPreview/RichItemPreview.js","sources":["../../../../../../src/FilePicker/primitives/components/previews/RichItemPreview/RichItemPreview.tsx"],"sourcesContent":["import React from \"react\";\nimport { cn, makeDecoratable } from \"~/utils.js\";\nimport { RichItemThumbnail } from \"./RichItemThumbnail.js\";\nimport { ItemDescription } from \"../ItemDescription.js\";\nimport { ItemActions } from \"~/FilePicker/primitives/components/previews/ItemActions.js\";\nimport { previewVariants } from \"../variants.js\";\nimport type { FilePreviewDefaultProps } from \"../../types.js\";\n\ntype RichItemPreviewProps = FilePreviewDefaultProps & {\n preview?: \"thumbnail\" | \"file-type\" | \"placeholder\";\n};\n\nconst DecoratableRichItemPreview = ({\n className,\n disabled,\n onRemoveItem,\n onReplaceItem,\n onEditItem,\n value,\n variant,\n preview,\n ...props\n}: RichItemPreviewProps) => {\n return (\n <div\n data-testid=\"image-preview\"\n className={cn(\"w-full rounded-md\", previewVariants({ variant, disabled }), className)}\n {...props}\n >\n <div\n data-role=\"select-image\"\n className=\"flex items-center justify-between gap-sm-extra min-w-0\"\n >\n <div\n className=\"flex items-center justify-between flex-1 cursor-pointer gap-sm-extra self-stretch min-w-0\"\n onClick={onReplaceItem}\n >\n <RichItemThumbnail {...value} disabled={disabled} preview={preview} />\n <ItemDescription item={value} disabled={disabled} />\n </div>\n\n <ItemActions\n onRemoveItem={onRemoveItem}\n onReplaceItem={onReplaceItem}\n onEditItem={onEditItem}\n disabled={disabled}\n className={\"pr-sm-extra\"}\n />\n </div>\n </div>\n );\n};\n\nconst RichItemPreview = makeDecoratable(\"RichItemPreview\", DecoratableRichItemPreview);\n\nexport { RichItemPreview, type RichItemPreviewProps };\n"],"names":["DecoratableRichItemPreview","className","disabled","onRemoveItem","onReplaceItem","onEditItem","value","variant","preview","props","cn","previewVariants","RichItemThumbnail","ItemDescription","ItemActions","RichItemPreview","makeDecoratable"],"mappings":";;;;;;AAYA,MAAMA,6BAA6B,CAAC,EAChCC,SAAS,EACTC,QAAQ,EACRC,YAAY,EACZC,aAAa,EACbC,UAAU,EACVC,KAAK,EACLC,OAAO,EACPC,OAAO,EACP,GAAGC,OACgB,GACZ,WAAP,GACI,oBAAC;QACG,eAAY;QACZ,WAAWC,GAAG,qBAAqBC,gBAAgB;YAAEJ;YAASL;QAAS,IAAID;QAC1E,GAAGQ,KAAK;qBAET,oBAAC;QACG,aAAU;QACV,WAAU;qBAEV,oBAAC;QACG,WAAU;QACV,SAASL;qBAET,oBAACQ,mBAAiBA;QAAE,GAAGN,KAAK;QAAE,UAAUJ;QAAU,SAASM;sBAC3D,oBAACK,iBAAeA;QAAC,MAAMP;QAAO,UAAUJ;uBAG5C,oBAACY,aAAWA;QACR,cAAcX;QACd,eAAeC;QACf,YAAYC;QACZ,UAAUH;QACV,WAAW;;AAO/B,MAAMa,kBAAkBC,gBAAgB,mBAAmBhB"}
1
+ {"version":3,"file":"FilePicker/primitives/components/previews/RichItemPreview/RichItemPreview.js","sources":["../../../../../../src/FilePicker/primitives/components/previews/RichItemPreview/RichItemPreview.tsx"],"sourcesContent":["import React from \"react\";\nimport { cn, makeDecoratable } from \"~/utils.js\";\nimport { RichItemThumbnail } from \"./RichItemThumbnail.js\";\nimport { ItemDescription } from \"../ItemDescription.js\";\nimport { ItemActions } from \"~/FilePicker/primitives/components/previews/ItemActions.js\";\nimport { previewVariants } from \"../variants.js\";\nimport type { FilePreviewDefaultProps } from \"../../types.js\";\n\ntype RichItemPreviewProps = FilePreviewDefaultProps & {\n preview?: \"thumbnail\" | \"file-type\" | \"placeholder\";\n /** Optional custom image renderer for the thumbnail (e.g. a cropped preview). */\n renderImage?: (args: { url: string; name: string }) => React.ReactNode;\n};\n\nconst DecoratableRichItemPreview = ({\n className,\n disabled,\n onRemoveItem,\n onReplaceItem,\n onEditItem,\n value,\n variant,\n preview,\n renderImage,\n ...props\n}: RichItemPreviewProps) => {\n return (\n <div\n data-testid=\"image-preview\"\n className={cn(\"w-full rounded-md\", previewVariants({ variant, disabled }), className)}\n {...props}\n >\n <div\n data-role=\"select-image\"\n className=\"flex items-center justify-between gap-sm-extra min-w-0\"\n >\n <div\n className=\"flex items-center justify-between flex-1 cursor-pointer gap-sm-extra self-stretch min-w-0\"\n onClick={onReplaceItem}\n >\n <RichItemThumbnail\n {...value}\n disabled={disabled}\n preview={preview}\n renderImage={renderImage}\n />\n <ItemDescription item={value} disabled={disabled} />\n </div>\n\n <ItemActions\n onRemoveItem={onRemoveItem}\n onReplaceItem={onReplaceItem}\n onEditItem={onEditItem}\n disabled={disabled}\n className={\"pr-sm-extra\"}\n />\n </div>\n </div>\n );\n};\n\nconst RichItemPreview = makeDecoratable(\"RichItemPreview\", DecoratableRichItemPreview);\n\nexport { RichItemPreview, type RichItemPreviewProps };\n"],"names":["DecoratableRichItemPreview","className","disabled","onRemoveItem","onReplaceItem","onEditItem","value","variant","preview","renderImage","props","cn","previewVariants","RichItemThumbnail","ItemDescription","ItemActions","RichItemPreview","makeDecoratable"],"mappings":";;;;;;AAcA,MAAMA,6BAA6B,CAAC,EAChCC,SAAS,EACTC,QAAQ,EACRC,YAAY,EACZC,aAAa,EACbC,UAAU,EACVC,KAAK,EACLC,OAAO,EACPC,OAAO,EACPC,WAAW,EACX,GAAGC,OACgB,GACZ,WAAP,GACI,oBAAC;QACG,eAAY;QACZ,WAAWC,GAAG,qBAAqBC,gBAAgB;YAAEL;YAASL;QAAS,IAAID;QAC1E,GAAGS,KAAK;qBAET,oBAAC;QACG,aAAU;QACV,WAAU;qBAEV,oBAAC;QACG,WAAU;QACV,SAASN;qBAET,oBAACS,mBAAiBA;QACb,GAAGP,KAAK;QACT,UAAUJ;QACV,SAASM;QACT,aAAaC;sBAEjB,oBAACK,iBAAeA;QAAC,MAAMR;QAAO,UAAUJ;uBAG5C,oBAACa,aAAWA;QACR,cAAcZ;QACd,eAAeC;QACf,YAAYC;QACZ,UAAUH;QACV,WAAW;;AAO/B,MAAMc,kBAAkBC,gBAAgB,mBAAmBjB"}
@@ -4,6 +4,15 @@ import type { RichItemPreviewProps } from "../../../../../FilePicker/primitives/
4
4
  type RichItemThumbnailProps = Omit<React.HTMLAttributes<HTMLDivElement>, "children"> & FileItemFormatted & {
5
5
  preview?: RichItemPreviewProps["preview"];
6
6
  disabled?: boolean;
7
+ /**
8
+ * Optional custom image renderer for image thumbnails (e.g. a cropped /
9
+ * focal-point-aware image). Falls back to the default `<img>` when omitted
10
+ * or when it returns a falsy value.
11
+ */
12
+ renderImage?: (args: {
13
+ url: string;
14
+ name: string;
15
+ }) => React.ReactNode;
7
16
  };
8
- declare const RichItemThumbnail: ({ url, name, className, mimeType, preview, disabled }: RichItemThumbnailProps) => React.JSX.Element;
17
+ declare const RichItemThumbnail: ({ url, name, className, mimeType, preview, disabled, renderImage }: RichItemThumbnailProps) => React.JSX.Element;
9
18
  export { RichItemThumbnail, type RichItemThumbnailProps };
@@ -58,11 +58,15 @@ const Placeholder = ({ name })=>/*#__PURE__*/ react.createElement("div", {
58
58
  size: "lg",
59
59
  color: "neutral-light"
60
60
  }));
61
- const RichItemThumbnail = ({ url, name, className, mimeType, preview, disabled })=>{
61
+ const RichItemThumbnail = ({ url, name, className, mimeType, preview, disabled, renderImage })=>{
62
62
  const isImage = mimeType?.startsWith("image/");
63
+ const customImage = "function" == typeof renderImage ? renderImage({
64
+ url,
65
+ name
66
+ }) : false;
63
67
  return /*#__PURE__*/ react.createElement("div", {
64
68
  className: cn("size-[56px] m-xs rounded-sm overflow-hidden relative", disabled && "[&_img]:filter [&_img]:grayscale [&_img]:opacity-50", className)
65
- }, "thumbnail" === preview || isImage ? /*#__PURE__*/ react.createElement(Thumbnail, {
69
+ }, "thumbnail" === preview || isImage ? customImage || /*#__PURE__*/ react.createElement(Thumbnail, {
66
70
  url: url,
67
71
  name: name
68
72
  }) : "placeholder" === preview ? /*#__PURE__*/ react.createElement(Placeholder, {
@@ -1 +1 @@
1
- {"version":3,"file":"FilePicker/primitives/components/previews/RichItemPreview/RichItemThumbnail.js","sources":["../../../../../../src/FilePicker/primitives/components/previews/RichItemPreview/RichItemThumbnail.tsx"],"sourcesContent":["import React from \"react\";\nimport { ReactComponent as PlaceholderIcon } from \"@webiny/icons/image.svg\";\nimport type { FileItemFormatted } from \"~/FilePicker/index.js\";\nimport { Icon } from \"~/Icon/index.js\";\nimport { cn } from \"~/utils.js\";\nimport type { RichItemPreviewProps } from \"~/FilePicker/primitives/components/index.js\";\n\nimport csvThumb from \"../assets/csv.svg\";\nimport docThumb from \"../assets/doc.svg\";\nimport docxThumb from \"../assets/docx.svg\";\nimport fileThumb from \"../assets/file.svg\";\nimport pdfThumb from \"../assets/pdf.svg\";\nimport pptThumb from \"../assets/ppt.svg\";\nimport pptxThumb from \"../assets/pptx.svg\";\nimport txtThumb from \"../assets/txt.svg\";\nimport xlsThumb from \"../assets/xls.svg\";\nimport xlsxThumb from \"../assets/xlsx.svg\";\n\ntype RichItemThumbnailProps = Omit<React.HTMLAttributes<HTMLDivElement>, \"children\"> &\n FileItemFormatted & {\n preview?: RichItemPreviewProps[\"preview\"];\n disabled?: boolean;\n };\n\ntype ThumbnailProps = Pick<FileItemFormatted, \"url\" | \"name\">;\n\nconst Thumbnail = ({ url, name }: ThumbnailProps) => {\n return (\n <div className={\"size-full bg-neutral-muted\"}>\n <img src={url} alt={name} className=\"size-full object-cover\" />\n </div>\n );\n};\n\ntype FileTypeProps = Pick<FileItemFormatted, \"mimeType\" | \"name\">;\n\nconst FileType = ({ mimeType = \"\", name }: FileTypeProps) => {\n const getMimeTypeSrc = (mimeType: string) => {\n switch (mimeType) {\n case \"text/csv\": // .csv\n return csvThumb;\n case \"application/msword\": // .doc\n return docThumb;\n case \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\": // .docx\n return docxThumb;\n case \"application/pdf\": // .pdf\n return pdfThumb;\n case \"application/vnd.ms-powerpoint\": // .ppt\n return pptThumb;\n case \"application/vnd.openxmlformats-officedocument.presentationml.presentation\": // .pptx\n return pptxThumb;\n case \"text/plain\": // .txt\n return txtThumb;\n case \"application/vnd.ms-excel\": // .xls\n return xlsThumb;\n case \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\": // .xlsx\n return xlsxThumb;\n default:\n return fileThumb;\n }\n };\n\n return <img src={getMimeTypeSrc(mimeType)} alt={name} className=\"size-full p-sm-extra\" />;\n};\n\ntype PlaceholderProps = Pick<FileItemFormatted, \"name\">;\n\nconst Placeholder = ({ name }: PlaceholderProps) => {\n return (\n <div className={\"size-full flex justify-center items-center bg-transparent\"}>\n <Icon icon={<PlaceholderIcon />} label={name} size={\"lg\"} color={\"neutral-light\"} />\n </div>\n );\n};\n\nconst RichItemThumbnail = ({\n url,\n name,\n className,\n mimeType,\n preview,\n disabled\n}: RichItemThumbnailProps) => {\n const isImage = mimeType?.startsWith(\"image/\");\n\n return (\n <div\n className={cn(\n \"size-[56px] m-xs rounded-sm overflow-hidden relative\",\n disabled && \"[&_img]:filter [&_img]:grayscale [&_img]:opacity-50\",\n className\n )}\n >\n {preview === \"thumbnail\" || isImage ? (\n <Thumbnail url={url} name={name} />\n ) : preview === \"placeholder\" ? (\n <Placeholder name={name} />\n ) : (\n <FileType mimeType={mimeType} name={name} />\n )}\n </div>\n );\n};\n\nexport { RichItemThumbnail, type RichItemThumbnailProps };\n"],"names":["Thumbnail","url","name","FileType","mimeType","getMimeTypeSrc","csvThumb","docThumb","docxThumb","pdfThumb","pptThumb","pptxThumb","txtThumb","xlsThumb","xlsxThumb","fileThumb","Placeholder","Icon","PlaceholderIcon","RichItemThumbnail","className","preview","disabled","isImage","cn"],"mappings":";;;;;;;;;;;;;;AA0BA,MAAMA,YAAY,CAAC,EAAEC,GAAG,EAAEC,IAAI,EAAkB,GACrC,WAAP,GACI,oBAAC;QAAI,WAAW;qBACZ,oBAAC;QAAI,KAAKD;QAAK,KAAKC;QAAM,WAAU;;AAOhD,MAAMC,WAAW,CAAC,EAAEC,WAAW,EAAE,EAAEF,IAAI,EAAiB;IACpD,MAAMG,iBAAiB,CAACD;QACpB,OAAQA;YACJ,KAAK;gBACD,OAAOE;YACX,KAAK;gBACD,OAAOC;YACX,KAAK;gBACD,OAAOC;YACX,KAAK;gBACD,OAAOC;YACX,KAAK;gBACD,OAAOC;YACX,KAAK;gBACD,OAAOC;YACX,KAAK;gBACD,OAAOC;YACX,KAAK;gBACD,OAAOC;YACX,KAAK;gBACD,OAAOC;YACX;gBACI,OAAOC;QACf;IACJ;IAEA,OAAO,WAAP,GAAO,oBAAC;QAAI,KAAKV,eAAeD;QAAW,KAAKF;QAAM,WAAU;;AACpE;AAIA,MAAMc,cAAc,CAAC,EAAEd,IAAI,EAAoB,GACpC,WAAP,GACI,oBAAC;QAAI,WAAW;qBACZ,oBAACe,MAAIA;QAAC,oBAAM,oBAACC,gBAAeA;QAAK,OAAOhB;QAAM,MAAM;QAAM,OAAO;;AAK7E,MAAMiB,oBAAoB,CAAC,EACvBlB,GAAG,EACHC,IAAI,EACJkB,SAAS,EACThB,QAAQ,EACRiB,OAAO,EACPC,QAAQ,EACa;IACrB,MAAMC,UAAUnB,UAAU,WAAW;IAErC,OAAO,WAAP,GACI,oBAAC;QACG,WAAWoB,GACP,wDACAF,YAAY,uDACZF;OAGHC,AAAY,gBAAZA,WAA2BE,UAAU,WAAVA,GACxB,oBAACvB,WAASA;QAAC,KAAKC;QAAK,MAAMC;SAC3BmB,AAAY,kBAAZA,UAA4B,WAAhB,GACZ,oBAACL,aAAWA;QAAC,MAAMd;uBAEnB,oBAACC,UAAQA;QAAC,UAAUC;QAAU,MAAMF;;AAIpD"}
1
+ {"version":3,"file":"FilePicker/primitives/components/previews/RichItemPreview/RichItemThumbnail.js","sources":["../../../../../../src/FilePicker/primitives/components/previews/RichItemPreview/RichItemThumbnail.tsx"],"sourcesContent":["import React from \"react\";\nimport { ReactComponent as PlaceholderIcon } from \"@webiny/icons/image.svg\";\nimport type { FileItemFormatted } from \"~/FilePicker/index.js\";\nimport { Icon } from \"~/Icon/index.js\";\nimport { cn } from \"~/utils.js\";\nimport type { RichItemPreviewProps } from \"~/FilePicker/primitives/components/index.js\";\n\nimport csvThumb from \"../assets/csv.svg\";\nimport docThumb from \"../assets/doc.svg\";\nimport docxThumb from \"../assets/docx.svg\";\nimport fileThumb from \"../assets/file.svg\";\nimport pdfThumb from \"../assets/pdf.svg\";\nimport pptThumb from \"../assets/ppt.svg\";\nimport pptxThumb from \"../assets/pptx.svg\";\nimport txtThumb from \"../assets/txt.svg\";\nimport xlsThumb from \"../assets/xls.svg\";\nimport xlsxThumb from \"../assets/xlsx.svg\";\n\ntype RichItemThumbnailProps = Omit<React.HTMLAttributes<HTMLDivElement>, \"children\"> &\n FileItemFormatted & {\n preview?: RichItemPreviewProps[\"preview\"];\n disabled?: boolean;\n /**\n * Optional custom image renderer for image thumbnails (e.g. a cropped /\n * focal-point-aware image). Falls back to the default `<img>` when omitted\n * or when it returns a falsy value.\n */\n renderImage?: (args: { url: string; name: string }) => React.ReactNode;\n };\n\ntype ThumbnailProps = Pick<FileItemFormatted, \"url\" | \"name\">;\n\nconst Thumbnail = ({ url, name }: ThumbnailProps) => {\n return (\n <div className={\"size-full bg-neutral-muted\"}>\n <img src={url} alt={name} className=\"size-full object-cover\" />\n </div>\n );\n};\n\ntype FileTypeProps = Pick<FileItemFormatted, \"mimeType\" | \"name\">;\n\nconst FileType = ({ mimeType = \"\", name }: FileTypeProps) => {\n const getMimeTypeSrc = (mimeType: string) => {\n switch (mimeType) {\n case \"text/csv\": // .csv\n return csvThumb;\n case \"application/msword\": // .doc\n return docThumb;\n case \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\": // .docx\n return docxThumb;\n case \"application/pdf\": // .pdf\n return pdfThumb;\n case \"application/vnd.ms-powerpoint\": // .ppt\n return pptThumb;\n case \"application/vnd.openxmlformats-officedocument.presentationml.presentation\": // .pptx\n return pptxThumb;\n case \"text/plain\": // .txt\n return txtThumb;\n case \"application/vnd.ms-excel\": // .xls\n return xlsThumb;\n case \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\": // .xlsx\n return xlsxThumb;\n default:\n return fileThumb;\n }\n };\n\n return <img src={getMimeTypeSrc(mimeType)} alt={name} className=\"size-full p-sm-extra\" />;\n};\n\ntype PlaceholderProps = Pick<FileItemFormatted, \"name\">;\n\nconst Placeholder = ({ name }: PlaceholderProps) => {\n return (\n <div className={\"size-full flex justify-center items-center bg-transparent\"}>\n <Icon icon={<PlaceholderIcon />} label={name} size={\"lg\"} color={\"neutral-light\"} />\n </div>\n );\n};\n\nconst RichItemThumbnail = ({\n url,\n name,\n className,\n mimeType,\n preview,\n disabled,\n renderImage\n}: RichItemThumbnailProps) => {\n const isImage = mimeType?.startsWith(\"image/\");\n const customImage = typeof renderImage === \"function\" ? renderImage({ url, name }) : false;\n\n return (\n <div\n className={cn(\n \"size-[56px] m-xs rounded-sm overflow-hidden relative\",\n disabled && \"[&_img]:filter [&_img]:grayscale [&_img]:opacity-50\",\n className\n )}\n >\n {preview === \"thumbnail\" || isImage ? (\n customImage || <Thumbnail url={url} name={name} />\n ) : preview === \"placeholder\" ? (\n <Placeholder name={name} />\n ) : (\n <FileType mimeType={mimeType} name={name} />\n )}\n </div>\n );\n};\n\nexport { RichItemThumbnail, type RichItemThumbnailProps };\n"],"names":["Thumbnail","url","name","FileType","mimeType","getMimeTypeSrc","csvThumb","docThumb","docxThumb","pdfThumb","pptThumb","pptxThumb","txtThumb","xlsThumb","xlsxThumb","fileThumb","Placeholder","Icon","PlaceholderIcon","RichItemThumbnail","className","preview","disabled","renderImage","isImage","customImage","cn"],"mappings":";;;;;;;;;;;;;;AAgCA,MAAMA,YAAY,CAAC,EAAEC,GAAG,EAAEC,IAAI,EAAkB,GACrC,WAAP,GACI,oBAAC;QAAI,WAAW;qBACZ,oBAAC;QAAI,KAAKD;QAAK,KAAKC;QAAM,WAAU;;AAOhD,MAAMC,WAAW,CAAC,EAAEC,WAAW,EAAE,EAAEF,IAAI,EAAiB;IACpD,MAAMG,iBAAiB,CAACD;QACpB,OAAQA;YACJ,KAAK;gBACD,OAAOE;YACX,KAAK;gBACD,OAAOC;YACX,KAAK;gBACD,OAAOC;YACX,KAAK;gBACD,OAAOC;YACX,KAAK;gBACD,OAAOC;YACX,KAAK;gBACD,OAAOC;YACX,KAAK;gBACD,OAAOC;YACX,KAAK;gBACD,OAAOC;YACX,KAAK;gBACD,OAAOC;YACX;gBACI,OAAOC;QACf;IACJ;IAEA,OAAO,WAAP,GAAO,oBAAC;QAAI,KAAKV,eAAeD;QAAW,KAAKF;QAAM,WAAU;;AACpE;AAIA,MAAMc,cAAc,CAAC,EAAEd,IAAI,EAAoB,GACpC,WAAP,GACI,oBAAC;QAAI,WAAW;qBACZ,oBAACe,MAAIA;QAAC,oBAAM,oBAACC,gBAAeA;QAAK,OAAOhB;QAAM,MAAM;QAAM,OAAO;;AAK7E,MAAMiB,oBAAoB,CAAC,EACvBlB,GAAG,EACHC,IAAI,EACJkB,SAAS,EACThB,QAAQ,EACRiB,OAAO,EACPC,QAAQ,EACRC,WAAW,EACU;IACrB,MAAMC,UAAUpB,UAAU,WAAW;IACrC,MAAMqB,cAAc,AAAuB,cAAvB,OAAOF,cAA6BA,YAAY;QAAEtB;QAAKC;IAAK,KAAK;IAErF,OAAO,WAAP,GACI,oBAAC;QACG,WAAWwB,GACP,wDACAJ,YAAY,uDACZF;OAGHC,AAAY,gBAAZA,WAA2BG,UACxBC,eAAe,WAAfA,GAAe,oBAACzB,WAASA;QAAC,KAAKC;QAAK,MAAMC;SAC1CmB,AAAY,kBAAZA,UAA4B,WAAhB,GACZ,oBAACL,aAAWA;QAAC,MAAMd;uBAEnB,oBAACC,UAAQA;QAAC,UAAUC;QAAU,MAAMF;;AAIpD"}
@@ -0,0 +1,16 @@
1
+ import React from "react";
2
+ import type { ImageEditorAspectRatio, ImageEditorCrop, ImageEditorHotspot } from "./types.js";
3
+ interface AspectRatioPreviewProps {
4
+ src: string;
5
+ imageWidth: number;
6
+ imageHeight: number;
7
+ crop: ImageEditorCrop | undefined;
8
+ hotspot: ImageEditorHotspot | undefined;
9
+ aspectRatio: ImageEditorAspectRatio;
10
+ }
11
+ /**
12
+ * A single live preview showing how the current crop + hotspot renders at one
13
+ * target aspect ratio. Uses the shared preview geometry so it matches the site.
14
+ */
15
+ export declare const AspectRatioPreview: ({ src, imageWidth, imageHeight, crop, hotspot, aspectRatio }: AspectRatioPreviewProps) => React.JSX.Element;
16
+ export {};
@@ -0,0 +1,26 @@
1
+ import react from "react";
2
+ import { getPreviewStyles } from "./geometry.js";
3
+ const PREVIEW_HEIGHT = 120;
4
+ const AspectRatioPreview = ({ src, imageWidth, imageHeight, crop, hotspot, aspectRatio })=>{
5
+ const styles = getPreviewStyles(imageWidth, imageHeight, crop, hotspot, aspectRatio.ratio);
6
+ const width = Math.round(PREVIEW_HEIGHT * aspectRatio.ratio);
7
+ return /*#__PURE__*/ react.createElement("div", {
8
+ className: "flex flex-col items-center gap-xs"
9
+ }, /*#__PURE__*/ react.createElement("div", {
10
+ className: "relative overflow-hidden rounded-sm border border-neutral-dimmed bg-neutral-light",
11
+ style: {
12
+ width: `${width}px`,
13
+ height: `${PREVIEW_HEIGHT}px`
14
+ }
15
+ }, /*#__PURE__*/ react.createElement("img", {
16
+ src: src,
17
+ alt: "",
18
+ style: styles.image,
19
+ draggable: false
20
+ })), /*#__PURE__*/ react.createElement("span", {
21
+ className: "text-sm text-neutral-strong"
22
+ }, aspectRatio.label));
23
+ };
24
+ export { AspectRatioPreview };
25
+
26
+ //# sourceMappingURL=AspectRatioPreview.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ImageEditor/AspectRatioPreview.js","sources":["../../src/ImageEditor/AspectRatioPreview.tsx"],"sourcesContent":["import React from \"react\";\nimport { getPreviewStyles } from \"./geometry.js\";\nimport type { ImageEditorAspectRatio, ImageEditorCrop, ImageEditorHotspot } from \"./types.js\";\n\ninterface AspectRatioPreviewProps {\n src: string;\n imageWidth: number;\n imageHeight: number;\n crop: ImageEditorCrop | undefined;\n hotspot: ImageEditorHotspot | undefined;\n aspectRatio: ImageEditorAspectRatio;\n}\n\n// Fixed preview height (px). Each thumbnail keeps this height regardless of how\n// many are shown; the width is derived from the aspect ratio.\nconst PREVIEW_HEIGHT = 120;\n\n/**\n * A single live preview showing how the current crop + hotspot renders at one\n * target aspect ratio. Uses the shared preview geometry so it matches the site.\n */\nexport const AspectRatioPreview = ({\n src,\n imageWidth,\n imageHeight,\n crop,\n hotspot,\n aspectRatio\n}: AspectRatioPreviewProps) => {\n const styles = getPreviewStyles(imageWidth, imageHeight, crop, hotspot, aspectRatio.ratio);\n const width = Math.round(PREVIEW_HEIGHT * aspectRatio.ratio);\n\n return (\n <div className={\"flex flex-col items-center gap-xs\"}>\n <div\n className={\n \"relative overflow-hidden rounded-sm border border-neutral-dimmed bg-neutral-light\"\n }\n style={{ width: `${width}px`, height: `${PREVIEW_HEIGHT}px` }}\n >\n <img src={src} alt={\"\"} style={styles.image} draggable={false} />\n </div>\n <span className={\"text-sm text-neutral-strong\"}>{aspectRatio.label}</span>\n </div>\n );\n};\n"],"names":["PREVIEW_HEIGHT","AspectRatioPreview","src","imageWidth","imageHeight","crop","hotspot","aspectRatio","styles","getPreviewStyles","width","Math"],"mappings":";;AAeA,MAAMA,iBAAiB;AAMhB,MAAMC,qBAAqB,CAAC,EAC/BC,GAAG,EACHC,UAAU,EACVC,WAAW,EACXC,IAAI,EACJC,OAAO,EACPC,WAAW,EACW;IACtB,MAAMC,SAASC,iBAAiBN,YAAYC,aAAaC,MAAMC,SAASC,YAAY,KAAK;IACzF,MAAMG,QAAQC,KAAK,KAAK,CAACX,iBAAiBO,YAAY,KAAK;IAE3D,OAAO,WAAP,GACI,oBAAC;QAAI,WAAW;qBACZ,oBAAC;QACG,WACI;QAEJ,OAAO;YAAE,OAAO,GAAGG,MAAM,EAAE,CAAC;YAAE,QAAQ,GAAGV,eAAe,EAAE,CAAC;QAAC;qBAE5D,oBAAC;QAAI,KAAKE;QAAK,KAAK;QAAI,OAAOM,OAAO,KAAK;QAAE,WAAW;uBAE5D,oBAAC;QAAK,WAAW;OAAgCD,YAAY,KAAK;AAG9E"}
@@ -0,0 +1,22 @@
1
+ import React from "react";
2
+ import type { ImageEditorCrop, ImageEditorHotspot, ImageEditorImage } from "./types.js";
3
+ interface CropHotspotEditorProps {
4
+ image: ImageEditorImage;
5
+ crop: ImageEditorCrop | undefined;
6
+ hotspot: ImageEditorHotspot | undefined;
7
+ /** Currently selected crop shape id ("free" or a preset id). Controlled by the parent. */
8
+ aspectId: string;
9
+ onChangeAspect: (id: string) => void;
10
+ onChangeCrop: (crop: ImageEditorCrop) => void;
11
+ onChangeHotspot: (hotspot: ImageEditorHotspot) => void;
12
+ }
13
+ export declare const FREE = "free";
14
+ /**
15
+ * Infer which crop-shape preset a saved crop corresponds to, by comparing the
16
+ * crop's on-screen (pixel) aspect ratio against the presets. The crop stores only
17
+ * edge insets — not the shape used to draw it — so on reopen we recover the shape
18
+ * here; a crop drawn with a preset matches it closely, while a free crop won't.
19
+ */
20
+ export declare const inferAspectId: (crop: ImageEditorCrop | undefined, width: number, height: number) => string;
21
+ export declare const CropHotspotEditor: ({ image, crop, hotspot, aspectId, onChangeAspect, onChangeCrop, onChangeHotspot }: CropHotspotEditorProps) => React.JSX.Element;
22
+ export {};
@@ -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