@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.
- package/Accordion/components/AccordionContent.js +4 -1
- package/Accordion/components/AccordionContent.js.map +1 -1
- package/Accordion/components/AccordionItem.d.ts +1 -0
- package/Accordion/components/AccordionItem.js.map +1 -1
- package/Accordion/components/AccordionItemDragHandle.d.ts +2 -1
- package/Accordion/components/AccordionItemDragHandle.js +2 -1
- package/Accordion/components/AccordionItemDragHandle.js.map +1 -1
- package/Accordion/components/AccordionTrigger.js +4 -2
- package/Accordion/components/AccordionTrigger.js.map +1 -1
- package/BetaBadge/BetaBadge.d.ts +6 -0
- package/BetaBadge/BetaBadge.js +7 -0
- package/BetaBadge/BetaBadge.js.map +1 -0
- package/Breadcrumbs/Breadcrumbs.d.ts +54 -0
- package/Breadcrumbs/Breadcrumbs.js +66 -0
- package/Breadcrumbs/Breadcrumbs.js.map +1 -0
- package/Breadcrumbs/Breadcrumbs.stories.d.ts +9 -0
- package/Breadcrumbs/Breadcrumbs.stories.js +71 -0
- package/Breadcrumbs/Breadcrumbs.stories.js.map +1 -0
- package/Breadcrumbs/index.d.ts +1 -0
- package/Breadcrumbs/index.js +1 -0
- package/Calendar/calendarClassNames.js +1 -1
- package/Calendar/calendarClassNames.js.map +1 -1
- package/CodeEditor/CodeEditorPrimitive.js +3 -1
- package/CodeEditor/CodeEditorPrimitive.js.map +1 -1
- package/DataTable/DataTable.d.ts +16 -6
- package/DataTable/DataTable.js +18 -7
- package/DataTable/DataTable.js.map +1 -1
- package/DataTable/components/ColumnsVisibility.d.ts +5 -4
- package/DataTable/components/ColumnsVisibility.js +7 -3
- package/DataTable/components/ColumnsVisibility.js.map +1 -1
- package/DatePicker/primitives/DateTimePicker.d.ts +1 -1
- package/DatePicker/primitives/DateTimePicker.js +15 -3
- package/DatePicker/primitives/DateTimePicker.js.map +1 -1
- package/DatePicker/primitives/components/DatePickerTrigger.js +1 -1
- package/DatePicker/primitives/components/DatePickerTrigger.js.map +1 -1
- package/DatePicker/utils/types.d.ts +2 -0
- package/DropdownMenu/components/DropdownMenuItem.d.ts +5 -0
- package/DropdownMenu/components/DropdownMenuItem.js +3 -2
- package/DropdownMenu/components/DropdownMenuItem.js.map +1 -1
- package/FilePicker/FilePicker.d.ts +12 -12
- package/FilePicker/primitives/FilePickerPrimitive.d.ts +12 -12
- package/FilePicker/primitives/components/previews/ImagePreview/ImagePreview.d.ts +13 -2
- package/FilePicker/primitives/components/previews/ImagePreview/ImagePreview.js +15 -6
- package/FilePicker/primitives/components/previews/ImagePreview/ImagePreview.js.map +1 -1
- package/FilePicker/primitives/components/previews/RichItemPreview/RichItemPreview.d.ts +6 -1
- package/FilePicker/primitives/components/previews/RichItemPreview/RichItemPreview.js +3 -2
- package/FilePicker/primitives/components/previews/RichItemPreview/RichItemPreview.js.map +1 -1
- package/FilePicker/primitives/components/previews/RichItemPreview/RichItemThumbnail.d.ts +10 -1
- package/FilePicker/primitives/components/previews/RichItemPreview/RichItemThumbnail.js +6 -2
- package/FilePicker/primitives/components/previews/RichItemPreview/RichItemThumbnail.js.map +1 -1
- package/ImageEditor/AspectRatioPreview.d.ts +16 -0
- package/ImageEditor/AspectRatioPreview.js +26 -0
- package/ImageEditor/AspectRatioPreview.js.map +1 -0
- package/ImageEditor/CropHotspotEditor.d.ts +22 -0
- package/ImageEditor/CropHotspotEditor.js +402 -0
- package/ImageEditor/CropHotspotEditor.js.map +1 -0
- package/ImageEditor/ImageEditor.d.ts +18 -0
- package/ImageEditor/ImageEditor.js +151 -0
- package/ImageEditor/ImageEditor.js.map +1 -0
- package/ImageEditor/ImageEditor.stories.d.ts +6 -0
- package/ImageEditor/ImageEditor.stories.js +42 -0
- package/ImageEditor/ImageEditor.stories.js.map +1 -0
- package/ImageEditor/MetaFields.d.ts +12 -0
- package/ImageEditor/MetaFields.js +20 -0
- package/ImageEditor/MetaFields.js.map +1 -0
- package/ImageEditor/SectionLabel.d.ts +8 -0
- package/ImageEditor/SectionLabel.js +7 -0
- package/ImageEditor/SectionLabel.js.map +1 -0
- package/ImageEditor/geometry.d.ts +46 -0
- package/ImageEditor/geometry.js +126 -0
- package/ImageEditor/geometry.js.map +1 -0
- package/ImageEditor/index.d.ts +3 -0
- package/ImageEditor/index.js +3 -0
- package/ImageEditor/types.d.ts +52 -0
- package/ImageEditor/types.js +35 -0
- package/ImageEditor/types.js.map +1 -0
- package/Markdown/Markdown.d.ts +47 -0
- package/Markdown/Markdown.js +17 -0
- package/Markdown/Markdown.js.map +1 -0
- package/Markdown/index.d.ts +1 -0
- package/Markdown/index.js +1 -0
- package/MultiFilePicker/MultiFilePicker.d.ts +12 -12
- package/MultiFilePicker/primitives/MultiFilePickerPrimitive.d.ts +12 -12
- package/MultiSelect/primitives/MultiSelectPrimitive.js +1 -1
- package/MultiSelect/primitives/MultiSelectPrimitive.js.map +1 -1
- package/Select/primitives/components/SelectContent.js +1 -1
- package/Select/primitives/components/SelectContent.js.map +1 -1
- package/Toast/useToast.stories.js.map +1 -1
- package/exports/admin/ui.d.ts +1 -0
- package/exports/admin/ui.js +1 -0
- package/index.d.ts +4 -0
- package/index.js +4 -0
- package/package.json +18 -18
- 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
|
|
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
|
|
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":";;;;;;;;;;;;;;
|
|
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
|