@umodeapporg/ui 0.24.4 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/components/Form/ControlledTabs.js +1 -1
  2. package/dist/components/Form/ControlledViewToggle.js +1 -1
  3. package/dist/components/Navigation/UiNavigationContext.js +1 -1
  4. package/dist/components/UI/ControlledPopover.d.ts +3 -0
  5. package/dist/components/UI/ControlledPopover.js +19 -7
  6. package/dist/components/UI/ControlledPopover.js.map +1 -1
  7. package/dist/components/UI/ImageAnnotator/AnnotationEditor.d.ts +2 -1
  8. package/dist/components/UI/ImageAnnotator/AnnotationEditor.js +138 -43
  9. package/dist/components/UI/ImageAnnotator/AnnotationEditor.js.map +1 -1
  10. package/dist/components/UI/ImageAnnotator/ImageAnnotationDialog.d.ts +3 -1
  11. package/dist/components/UI/ImageAnnotator/ImageAnnotationDialog.js +3 -0
  12. package/dist/components/UI/ImageAnnotator/ImageAnnotationDialog.js.map +1 -1
  13. package/dist/components/UI/ImageAnnotator/canvas/AnnotationCropFrame.d.ts +16 -0
  14. package/dist/components/UI/ImageAnnotator/canvas/AnnotationCropFrame.js +156 -0
  15. package/dist/components/UI/ImageAnnotator/canvas/AnnotationCropFrame.js.map +1 -0
  16. package/dist/components/UI/ImageAnnotator/canvas/AnnotationStage.d.ts +8 -1
  17. package/dist/components/UI/ImageAnnotator/canvas/AnnotationStage.js +30 -5
  18. package/dist/components/UI/ImageAnnotator/canvas/AnnotationStage.js.map +1 -1
  19. package/dist/components/UI/ImageAnnotator/components/AnnotationCropBar.d.ts +12 -0
  20. package/dist/components/UI/ImageAnnotator/components/AnnotationCropBar.js +89 -0
  21. package/dist/components/UI/ImageAnnotator/components/AnnotationCropBar.js.map +1 -0
  22. package/dist/components/UI/ImageAnnotator/components/AnnotationTextOverlay.d.ts +3 -1
  23. package/dist/components/UI/ImageAnnotator/components/AnnotationTextOverlay.js +3 -2
  24. package/dist/components/UI/ImageAnnotator/components/AnnotationTextOverlay.js.map +1 -1
  25. package/dist/components/UI/ImageAnnotator/components/AnnotationToolbar.js +11 -0
  26. package/dist/components/UI/ImageAnnotator/components/AnnotationToolbar.js.map +1 -1
  27. package/dist/components/UI/ImageAnnotator/helpers/annotationCrop.d.ts +12 -0
  28. package/dist/components/UI/ImageAnnotator/helpers/annotationCrop.js +64 -0
  29. package/dist/components/UI/ImageAnnotator/helpers/annotationCrop.js.map +1 -0
  30. package/dist/components/UI/ImageAnnotator/hooks/useAnnotationDocument.d.ts +4 -2
  31. package/dist/components/UI/ImageAnnotator/hooks/useAnnotationDocument.js +23 -12
  32. package/dist/components/UI/ImageAnnotator/hooks/useAnnotationDocument.js.map +1 -1
  33. package/dist/components/UI/ImageAnnotator/hooks/useAnnotationShortcuts.js +2 -1
  34. package/dist/components/UI/ImageAnnotator/hooks/useAnnotationShortcuts.js.map +1 -1
  35. package/dist/components/UI/ImageAnnotator/interfaces/annotationDocument.d.ts +10 -1
  36. package/dist/components/UI/ImageAnnotator/interfaces/annotationDocument.js.map +1 -1
  37. package/dist/locales/en/ui.json.d.ts +17 -2
  38. package/dist/locales/en/ui.json.js +1 -1
  39. package/dist/locales/index.d.ts +30 -0
  40. package/dist/locales/pt/ui.json.d.ts +17 -2
  41. package/dist/locales/pt/ui.json.js +1 -1
  42. package/package.json +1 -1
@@ -6,7 +6,7 @@ import { useUiNavigation } from "../Navigation/UiNavigationContext.js";
6
6
  import { useUiAccess } from "../UI/access/UiAccessContext.js";
7
7
  import ControlledTooltip from "../UI/ControlledTooltip.js";
8
8
  import { formatNumber } from "../../helpers/formatHelpers.js";
9
- import { claimUrlParam, releaseUrlParam, TABS_URL_PARAM, queueUrlParamUpdate, URL_PARAM_PRIORITY } from "../../helpers/urlParamSync.js";
9
+ import { claimUrlParam, URL_PARAM_PRIORITY, TABS_URL_PARAM, releaseUrlParam, queueUrlParamUpdate } from "../../helpers/urlParamSync.js";
10
10
  import LoadingSpinner from "../UI/LoadingSpinner.js";
11
11
  import { MaterialSymbol } from "../UI/MaterialSymbol/index.js";
12
12
  import ControlledButton from "./ControlledButton.js";
@@ -8,7 +8,7 @@ import LoadingSpinner from "../UI/LoadingSpinner.js";
8
8
  import { MaterialSymbol } from "../UI/MaterialSymbol/index.js";
9
9
  import { formatNumber } from "../../helpers/formatHelpers.js";
10
10
  import { uiLogger } from "../../helpers/uiLogger.js";
11
- import { TABS_URL_PARAM, claimUrlParam, URL_PARAM_PRIORITY, releaseUrlParam, ownsUrlParam, queueUrlParamUpdate } from "../../helpers/urlParamSync.js";
11
+ import { TABS_URL_PARAM, claimUrlParam, releaseUrlParam, ownsUrlParam, queueUrlParamUpdate, URL_PARAM_PRIORITY } from "../../helpers/urlParamSync.js";
12
12
  const DEFAULT_URL_PARAM_KEY = "view";
13
13
  const SIZE_STYLES = {
14
14
  sm: {
@@ -1,5 +1,5 @@
1
1
  import { jsx } from "react/jsx-runtime";
2
- import { forwardRef, createContext, useContext, useMemo } from "react";
2
+ import { useContext, useMemo, createContext, forwardRef } from "react";
3
3
  const FallbackLink = forwardRef(({ to, children, ...rest }, ref) => /* @__PURE__ */ jsx("a", { ref, href: to, ...rest, children }));
4
4
  FallbackLink.displayName = "FallbackLink";
5
5
  const createFallbackNavigation = () => {
@@ -7,6 +7,8 @@ interface IControlledPopoverRootProps {
7
7
  handler?: (open: boolean) => void;
8
8
  placement?: TPopoverPlacement;
9
9
  offsetPx?: number;
10
+ /** Sizes the content to the trigger width. Use it when the popover acts as a select dropdown. */
11
+ matchTriggerWidth?: boolean;
10
12
  }
11
13
  export declare const ControlledPopoverRoot: React.FC<IControlledPopoverRootProps>;
12
14
  /**
@@ -34,6 +36,7 @@ interface IControlledPopoverProps {
34
36
  onOpenChange?: (open: boolean) => void;
35
37
  contentClassName?: string;
36
38
  tooltipClassName?: string;
39
+ matchTriggerWidth?: boolean;
37
40
  }
38
41
  /**
39
42
  * A tooltip on the trigger must sit OUTSIDE the handler: the handler clones its child to
@@ -22,7 +22,8 @@ const ControlledPopoverRoot = ({
22
22
  open,
23
23
  handler,
24
24
  placement = "bottom-start",
25
- offsetPx = 6
25
+ offsetPx = 6,
26
+ matchTriggerWidth = false
26
27
  }) => {
27
28
  const contentId = useId();
28
29
  const [uncontrolledOpen, setUncontrolledOpen] = useState(false);
@@ -35,7 +36,8 @@ const ControlledPopoverRoot = ({
35
36
  const { setReference, setFloating, floatingRef, referenceRef } = useAnchoredFloating({
36
37
  open: isOpen,
37
38
  placement,
38
- offsetPx
39
+ offsetPx,
40
+ matchReferenceWidth: matchTriggerWidth
39
41
  });
40
42
  return /* @__PURE__ */ jsx(
41
43
  PopoverContext.Provider,
@@ -130,13 +132,23 @@ const ControlledPopover = ({
130
132
  open,
131
133
  onOpenChange,
132
134
  contentClassName,
133
- tooltipClassName
135
+ tooltipClassName,
136
+ matchTriggerWidth
134
137
  }) => {
135
138
  const handle = /* @__PURE__ */ jsx(ControlledPopoverHandler, { children: trigger });
136
- return /* @__PURE__ */ jsxs(ControlledPopoverRoot, { open, handler: onOpenChange, placement, children: [
137
- triggerTooltip ? /* @__PURE__ */ jsx(ControlledTooltip, { content: triggerTooltip, className: classNames(tooltipClassName), children: handle }) : handle,
138
- /* @__PURE__ */ jsx(ControlledPopoverContent, { className: contentClassName, children })
139
- ] });
139
+ return /* @__PURE__ */ jsxs(
140
+ ControlledPopoverRoot,
141
+ {
142
+ open,
143
+ handler: onOpenChange,
144
+ placement,
145
+ matchTriggerWidth,
146
+ children: [
147
+ triggerTooltip ? /* @__PURE__ */ jsx(ControlledTooltip, { content: triggerTooltip, className: classNames(tooltipClassName), children: handle }) : handle,
148
+ /* @__PURE__ */ jsx(ControlledPopoverContent, { className: contentClassName, children })
149
+ ]
150
+ }
151
+ );
140
152
  };
141
153
  export {
142
154
  ControlledPopoverContent,
@@ -1 +1 @@
1
- {"version":3,"file":"ControlledPopover.js","sources":["../../../src/components/UI/ControlledPopover.tsx"],"sourcesContent":["import React, { cloneElement, createContext, isValidElement, useContext, useEffect, useId, useState } from 'react';\nimport classNames from 'classnames';\nimport { AnimatePresence, motion } from 'motion/react';\nimport { createPortal } from 'react-dom';\nimport { twMerge } from 'tailwind-merge';\n\nimport ControlledTooltip from '@/components/UI/ControlledTooltip';\nimport { composeRefs } from '@/helpers/composeRefs';\nimport { isPressOnLayeredSurface } from '@/helpers/layeredSurfaces';\nimport { getPortalHost } from '@/helpers/portalHost';\nimport { TFloatingPlacement, useAnchoredFloating } from '@/hooks/useAnchoredFloating';\nimport { floatingVariants } from '@/motion';\n\nexport type TPopoverPlacement = TFloatingPlacement;\n\ninterface IPopoverContext {\n open: boolean;\n setOpen: (open: boolean) => void;\n placement: TFloatingPlacement;\n offsetPx: number;\n contentId: string;\n setReference: (node: HTMLElement | null) => void;\n setFloating: (node: HTMLElement | null) => void;\n floatingRef: React.MutableRefObject<HTMLElement | null>;\n referenceRef: React.MutableRefObject<HTMLElement | null>;\n}\n\nconst PopoverContext = createContext<IPopoverContext | null>(null);\n\nconst usePopoverContext = (): IPopoverContext => {\n const context = useContext(PopoverContext);\n if (!context) throw new Error('Popover parts must be rendered inside ControlledPopoverRoot');\n return context;\n};\n\ninterface IControlledPopoverRootProps {\n children: React.ReactNode;\n open?: boolean;\n handler?: (open: boolean) => void;\n placement?: TPopoverPlacement;\n offsetPx?: number;\n}\n\nexport const ControlledPopoverRoot: React.FC<IControlledPopoverRootProps> = ({\n children,\n open,\n handler,\n placement = 'bottom-start',\n offsetPx = 6,\n}) => {\n const contentId = useId();\n const [uncontrolledOpen, setUncontrolledOpen] = useState(false);\n\n const isControlled = open !== undefined;\n const isOpen = isControlled ? Boolean(open) : uncontrolledOpen;\n\n const setOpen = (next: boolean) => {\n if (!isControlled) setUncontrolledOpen(next);\n handler?.(next);\n };\n\n const { setReference, setFloating, floatingRef, referenceRef } = useAnchoredFloating({\n open: isOpen,\n placement,\n offsetPx,\n });\n\n return (\n <PopoverContext.Provider\n value={{\n open: isOpen,\n setOpen,\n placement,\n offsetPx,\n contentId,\n setReference,\n setFloating,\n floatingRef,\n referenceRef,\n }}\n >\n {children}\n </PopoverContext.Provider>\n );\n};\n\ninterface IHandlerChildProps {\n onClick?: (event: React.MouseEvent<HTMLElement>) => void;\n ref?: React.Ref<HTMLElement>;\n}\n\n/**\n * Repassa ao filho o que ele recebe de fora, além do que injeta.\n *\n * O gatilho é o alvo natural de um `ControlledTooltip`, e o tooltip se prende clonando o filho e\n * injetando `onPointerEnter`. Aceitando só `children`, este componente engolia essa injeção e o\n * tooltip simplesmente nunca abria, mesmo com a ordem que a documentação manda usar\n * (tooltip POR FORA do handler). Os `onClick` são encadeados: o de fora roda antes de abrir.\n */\nexport const ControlledPopoverHandler = React.forwardRef<\n HTMLElement,\n { children: React.ReactNode } & React.HTMLAttributes<HTMLElement>\n>(({ children, onClick: onOuterClick, ...rest }, forwardedRef) => {\n const { open, setOpen, setReference, contentId } = usePopoverContext();\n\n if (!isValidElement(children)) return <>{children}</>;\n\n const child = children as React.ReactElement<IHandlerChildProps>;\n\n return cloneElement(child, {\n ...rest,\n ref: composeRefs<HTMLElement>(setReference, forwardedRef, (child as { ref?: React.Ref<HTMLElement> }).ref),\n 'aria-expanded': open,\n 'aria-controls': open ? contentId : undefined,\n onClick: (event: React.MouseEvent<HTMLElement>) => {\n onOuterClick?.(event);\n child.props.onClick?.(event);\n setOpen(!open);\n },\n } as IHandlerChildProps);\n});\n\nControlledPopoverHandler.displayName = 'ControlledPopoverHandler';\n\ninterface IControlledPopoverContentProps extends Omit<\n React.HTMLAttributes<HTMLDivElement>,\n 'className' | 'onDrag' | 'onDragStart' | 'onDragEnd' | 'onAnimationStart' | 'onAnimationEnd' | 'onAnimationIteration'\n> {\n children: React.ReactNode;\n className?: string;\n}\n\nexport const ControlledPopoverContent: React.FC<IControlledPopoverContentProps> = ({\n children,\n className,\n ...rest\n}) => {\n const { open, setOpen, contentId, setFloating, floatingRef, referenceRef } = usePopoverContext();\n\n useEffect(() => {\n if (!open) return undefined;\n\n const handleKeyDown = (event: KeyboardEvent) => {\n if (event.key === 'Escape') setOpen(false);\n };\n\n const handlePointerDown = (event: PointerEvent) => {\n const target = event.target as Node | null;\n if (!target) return;\n if (floatingRef.current?.contains(target) || referenceRef.current?.contains(target)) return;\n if (isPressOnLayeredSurface(target, floatingRef.current)) return;\n setOpen(false);\n };\n\n document.addEventListener('keydown', handleKeyDown);\n document.addEventListener('pointerdown', handlePointerDown, true);\n\n return () => {\n document.removeEventListener('keydown', handleKeyDown);\n document.removeEventListener('pointerdown', handlePointerDown, true);\n };\n }, [open, setOpen, floatingRef, referenceRef]);\n\n const portalHost = getPortalHost();\n if (portalHost === null) return null;\n\n return createPortal(\n <AnimatePresence>\n {open && (\n <motion.div\n key=\"popover-content\"\n id={contentId}\n ref={setFloating}\n variants={floatingVariants}\n initial=\"hidden\"\n animate=\"visible\"\n exit=\"exit\"\n role=\"dialog\"\n {...rest}\n className={twMerge(\n 'fixed left-0 top-0 z-[9999] overflow-auto rounded-lg border border-border p-2 text-sm text-foreground glass',\n className,\n )}\n >\n {children}\n </motion.div>\n )}\n </AnimatePresence>,\n portalHost,\n );\n};\n\ninterface IControlledPopoverProps {\n trigger: React.ReactNode;\n children: React.ReactNode;\n triggerTooltip?: string;\n placement?: TPopoverPlacement;\n open?: boolean;\n onOpenChange?: (open: boolean) => void;\n contentClassName?: string;\n tooltipClassName?: string;\n}\n\n/**\n * A tooltip on the trigger must sit OUTSIDE the handler: the handler clones its child to\n * inject the click handler and the reference ref, and a tooltip in that position swallows\n * both, so the popover stops opening.\n */\nconst ControlledPopover: React.FC<IControlledPopoverProps> = ({\n trigger,\n children,\n triggerTooltip,\n placement = 'bottom-start',\n open,\n onOpenChange,\n contentClassName,\n tooltipClassName,\n}) => {\n const handle = <ControlledPopoverHandler>{trigger}</ControlledPopoverHandler>;\n\n return (\n <ControlledPopoverRoot open={open} handler={onOpenChange} placement={placement}>\n {triggerTooltip ? (\n <ControlledTooltip content={triggerTooltip} className={classNames(tooltipClassName)}>\n {handle}\n </ControlledTooltip>\n ) : (\n handle\n )}\n\n <ControlledPopoverContent className={contentClassName}>{children}</ControlledPopoverContent>\n </ControlledPopoverRoot>\n );\n};\n\nexport default ControlledPopover;\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ControlledPopover.js","sources":["../../../src/components/UI/ControlledPopover.tsx"],"sourcesContent":["import React, { cloneElement, createContext, isValidElement, useContext, useEffect, useId, useState } from 'react';\nimport classNames from 'classnames';\nimport { AnimatePresence, motion } from 'motion/react';\nimport { createPortal } from 'react-dom';\nimport { twMerge } from 'tailwind-merge';\n\nimport ControlledTooltip from '@/components/UI/ControlledTooltip';\nimport { composeRefs } from '@/helpers/composeRefs';\nimport { isPressOnLayeredSurface } from '@/helpers/layeredSurfaces';\nimport { getPortalHost } from '@/helpers/portalHost';\nimport { TFloatingPlacement, useAnchoredFloating } from '@/hooks/useAnchoredFloating';\nimport { floatingVariants } from '@/motion';\n\nexport type TPopoverPlacement = TFloatingPlacement;\n\ninterface IPopoverContext {\n open: boolean;\n setOpen: (open: boolean) => void;\n placement: TFloatingPlacement;\n offsetPx: number;\n contentId: string;\n setReference: (node: HTMLElement | null) => void;\n setFloating: (node: HTMLElement | null) => void;\n floatingRef: React.MutableRefObject<HTMLElement | null>;\n referenceRef: React.MutableRefObject<HTMLElement | null>;\n}\n\nconst PopoverContext = createContext<IPopoverContext | null>(null);\n\nconst usePopoverContext = (): IPopoverContext => {\n const context = useContext(PopoverContext);\n if (!context) throw new Error('Popover parts must be rendered inside ControlledPopoverRoot');\n return context;\n};\n\ninterface IControlledPopoverRootProps {\n children: React.ReactNode;\n open?: boolean;\n handler?: (open: boolean) => void;\n placement?: TPopoverPlacement;\n offsetPx?: number;\n /** Sizes the content to the trigger width. Use it when the popover acts as a select dropdown. */\n matchTriggerWidth?: boolean;\n}\n\nexport const ControlledPopoverRoot: React.FC<IControlledPopoverRootProps> = ({\n children,\n open,\n handler,\n placement = 'bottom-start',\n offsetPx = 6,\n matchTriggerWidth = false,\n}) => {\n const contentId = useId();\n const [uncontrolledOpen, setUncontrolledOpen] = useState(false);\n\n const isControlled = open !== undefined;\n const isOpen = isControlled ? Boolean(open) : uncontrolledOpen;\n\n const setOpen = (next: boolean) => {\n if (!isControlled) setUncontrolledOpen(next);\n handler?.(next);\n };\n\n const { setReference, setFloating, floatingRef, referenceRef } = useAnchoredFloating({\n open: isOpen,\n placement,\n offsetPx,\n matchReferenceWidth: matchTriggerWidth,\n });\n\n return (\n <PopoverContext.Provider\n value={{\n open: isOpen,\n setOpen,\n placement,\n offsetPx,\n contentId,\n setReference,\n setFloating,\n floatingRef,\n referenceRef,\n }}\n >\n {children}\n </PopoverContext.Provider>\n );\n};\n\ninterface IHandlerChildProps {\n onClick?: (event: React.MouseEvent<HTMLElement>) => void;\n ref?: React.Ref<HTMLElement>;\n}\n\n/**\n * Repassa ao filho o que ele recebe de fora, além do que injeta.\n *\n * O gatilho é o alvo natural de um `ControlledTooltip`, e o tooltip se prende clonando o filho e\n * injetando `onPointerEnter`. Aceitando só `children`, este componente engolia essa injeção e o\n * tooltip simplesmente nunca abria, mesmo com a ordem que a documentação manda usar\n * (tooltip POR FORA do handler). Os `onClick` são encadeados: o de fora roda antes de abrir.\n */\nexport const ControlledPopoverHandler = React.forwardRef<\n HTMLElement,\n { children: React.ReactNode } & React.HTMLAttributes<HTMLElement>\n>(({ children, onClick: onOuterClick, ...rest }, forwardedRef) => {\n const { open, setOpen, setReference, contentId } = usePopoverContext();\n\n if (!isValidElement(children)) return <>{children}</>;\n\n const child = children as React.ReactElement<IHandlerChildProps>;\n\n return cloneElement(child, {\n ...rest,\n ref: composeRefs<HTMLElement>(setReference, forwardedRef, (child as { ref?: React.Ref<HTMLElement> }).ref),\n 'aria-expanded': open,\n 'aria-controls': open ? contentId : undefined,\n onClick: (event: React.MouseEvent<HTMLElement>) => {\n onOuterClick?.(event);\n child.props.onClick?.(event);\n setOpen(!open);\n },\n } as IHandlerChildProps);\n});\n\nControlledPopoverHandler.displayName = 'ControlledPopoverHandler';\n\ninterface IControlledPopoverContentProps extends Omit<\n React.HTMLAttributes<HTMLDivElement>,\n 'className' | 'onDrag' | 'onDragStart' | 'onDragEnd' | 'onAnimationStart' | 'onAnimationEnd' | 'onAnimationIteration'\n> {\n children: React.ReactNode;\n className?: string;\n}\n\nexport const ControlledPopoverContent: React.FC<IControlledPopoverContentProps> = ({\n children,\n className,\n ...rest\n}) => {\n const { open, setOpen, contentId, setFloating, floatingRef, referenceRef } = usePopoverContext();\n\n useEffect(() => {\n if (!open) return undefined;\n\n const handleKeyDown = (event: KeyboardEvent) => {\n if (event.key === 'Escape') setOpen(false);\n };\n\n const handlePointerDown = (event: PointerEvent) => {\n const target = event.target as Node | null;\n if (!target) return;\n if (floatingRef.current?.contains(target) || referenceRef.current?.contains(target)) return;\n if (isPressOnLayeredSurface(target, floatingRef.current)) return;\n setOpen(false);\n };\n\n document.addEventListener('keydown', handleKeyDown);\n document.addEventListener('pointerdown', handlePointerDown, true);\n\n return () => {\n document.removeEventListener('keydown', handleKeyDown);\n document.removeEventListener('pointerdown', handlePointerDown, true);\n };\n }, [open, setOpen, floatingRef, referenceRef]);\n\n const portalHost = getPortalHost();\n if (portalHost === null) return null;\n\n return createPortal(\n <AnimatePresence>\n {open && (\n <motion.div\n key=\"popover-content\"\n id={contentId}\n ref={setFloating}\n variants={floatingVariants}\n initial=\"hidden\"\n animate=\"visible\"\n exit=\"exit\"\n role=\"dialog\"\n {...rest}\n className={twMerge(\n 'fixed left-0 top-0 z-[9999] overflow-auto rounded-lg border border-border p-2 text-sm text-foreground glass',\n className,\n )}\n >\n {children}\n </motion.div>\n )}\n </AnimatePresence>,\n portalHost,\n );\n};\n\ninterface IControlledPopoverProps {\n trigger: React.ReactNode;\n children: React.ReactNode;\n triggerTooltip?: string;\n placement?: TPopoverPlacement;\n open?: boolean;\n onOpenChange?: (open: boolean) => void;\n contentClassName?: string;\n tooltipClassName?: string;\n matchTriggerWidth?: boolean;\n}\n\n/**\n * A tooltip on the trigger must sit OUTSIDE the handler: the handler clones its child to\n * inject the click handler and the reference ref, and a tooltip in that position swallows\n * both, so the popover stops opening.\n */\nconst ControlledPopover: React.FC<IControlledPopoverProps> = ({\n trigger,\n children,\n triggerTooltip,\n placement = 'bottom-start',\n open,\n onOpenChange,\n contentClassName,\n tooltipClassName,\n matchTriggerWidth,\n}) => {\n const handle = <ControlledPopoverHandler>{trigger}</ControlledPopoverHandler>;\n\n return (\n <ControlledPopoverRoot\n open={open}\n handler={onOpenChange}\n placement={placement}\n matchTriggerWidth={matchTriggerWidth}\n >\n {triggerTooltip ? (\n <ControlledTooltip content={triggerTooltip} className={classNames(tooltipClassName)}>\n {handle}\n </ControlledTooltip>\n ) : (\n handle\n )}\n\n <ControlledPopoverContent className={contentClassName}>{children}</ControlledPopoverContent>\n </ControlledPopoverRoot>\n );\n};\n\nexport default ControlledPopover;\n"],"names":[],"mappings":"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"}
@@ -1,10 +1,11 @@
1
- import { IAnnotationDocument, IAnnotationSize, TAnnotationElement } from './interfaces/annotationDocument';
1
+ import { IAnnotationCropRect, IAnnotationDocument, IAnnotationSize, TAnnotationElement } from './interfaces/annotationDocument';
2
2
  interface IAnnotationEditorProps {
3
3
  baseImage: HTMLImageElement;
4
4
  imageSize: IAnnotationSize;
5
5
  mimeType: string;
6
6
  imageName?: string;
7
7
  initialElements: Array<TAnnotationElement>;
8
+ initialCrop: IAnnotationCropRect | null;
8
9
  onSave: (file: File, document: IAnnotationDocument) => Promise<void> | void;
9
10
  onSaved: () => void;
10
11
  }
@@ -1,22 +1,25 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
- import { useRef, useState, useEffect, useMemo, useCallback } from "react";
2
+ import { useRef, useState, useMemo, useEffect, useCallback } from "react";
3
3
  import { useTranslation } from "react-i18next";
4
4
  import { uiLogger } from "../../../helpers/uiLogger.js";
5
5
  import { useBreakpoint } from "../../../hooks/useBreakpoint.js";
6
6
  import { useNotification } from "../../../hooks/useNotification.js";
7
7
  import AnnotationStage from "./canvas/AnnotationStage.js";
8
8
  import AnnotationActionBar from "./components/AnnotationActionBar.js";
9
+ import AnnotationCropBar from "./components/AnnotationCropBar.js";
9
10
  import AnnotationElementActions from "./components/AnnotationElementActions.js";
10
11
  import AnnotationStyleBar from "./components/AnnotationStyleBar.js";
11
12
  import AnnotationTextOverlay from "./components/AnnotationTextOverlay.js";
12
13
  import AnnotationToolbar from "./components/AnnotationToolbar.js";
13
14
  import { fitStageToViewport } from "./helpers/annotationCanvas.js";
14
- import { styleFromElement, applyStyleToElement, createImageElement, createTextElement, imageMaxDimension } from "./helpers/annotationElements.js";
15
+ import { fullImageCrop, aspectRatioFor, fitCropToAspect, isFullImageCrop } from "./helpers/annotationCrop.js";
16
+ import { styleFromElement, applyStyleToElement, translateElement, createImageElement, createTextElement, imageMaxDimension } from "./helpers/annotationElements.js";
15
17
  import { exportStageToFile } from "./helpers/annotationExport.js";
16
18
  import { useAnnotationDocument } from "./hooks/useAnnotationDocument.js";
17
19
  import { useAnnotationShortcuts } from "./hooks/useAnnotationShortcuts.js";
18
20
  import { useCoarsePointer } from "./hooks/useCoarsePointer.js";
19
21
  import { DEFAULT_ANNOTATION_STYLE } from "./annotationStyle.js";
22
+ const CROP_STAGE_GUTTER_PX = 12;
20
23
  const ELEMENT_TOOLS = ["freehand", "line", "arrow", "rectangle", "ellipse", "text", "image"];
21
24
  const readFileAsDataUrl = (file) => new Promise((resolve, reject) => {
22
25
  const reader = new FileReader();
@@ -37,6 +40,7 @@ const AnnotationEditor = ({
37
40
  mimeType,
38
41
  imageName,
39
42
  initialElements,
43
+ initialCrop,
40
44
  onSave,
41
45
  onSaved
42
46
  }) => {
@@ -52,8 +56,19 @@ const AnnotationEditor = ({
52
56
  const [style, setStyle] = useState(DEFAULT_ANNOTATION_STYLE);
53
57
  const [editingText, setEditingText] = useState(null);
54
58
  const [saving, setSaving] = useState(false);
55
- const controller = useAnnotationDocument(imageSize, initialElements);
59
+ const [cropDraft, setCropDraft] = useState(null);
60
+ const [cropAspect, setCropAspect] = useState("free");
61
+ const controller = useAnnotationDocument(imageSize, initialElements, initialCrop);
56
62
  const { selectedElement, selectedId, updateSelected, addElement, removeElement, updateElement } = controller;
63
+ const isCropping = tool === "crop";
64
+ const viewRect = useMemo(
65
+ () => isCropping ? fullImageCrop(imageSize) : controller.crop ?? fullImageCrop(imageSize),
66
+ [isCropping, controller.crop, imageSize]
67
+ );
68
+ const viewSize = useMemo(
69
+ () => ({ width: viewRect.width, height: viewRect.height }),
70
+ [viewRect.width, viewRect.height]
71
+ );
57
72
  useEffect(() => {
58
73
  const node = viewportRef.current;
59
74
  if (!node) return;
@@ -64,7 +79,14 @@ const AnnotationEditor = ({
64
79
  observer.observe(node);
65
80
  return () => observer.disconnect();
66
81
  }, []);
67
- const fit = useMemo(() => fitStageToViewport(imageSize, viewport), [imageSize, viewport]);
82
+ const stageGutter = isCropping ? CROP_STAGE_GUTTER_PX : 0;
83
+ const fit = useMemo(
84
+ () => fitStageToViewport(viewSize, {
85
+ width: viewport.width - stageGutter * 2,
86
+ height: viewport.height - stageGutter * 2
87
+ }),
88
+ [viewSize, viewport, stageGutter]
89
+ );
68
90
  useEffect(() => {
69
91
  if (!selectedId) return;
70
92
  setStyle((current) => {
@@ -99,23 +121,72 @@ const AnnotationEditor = ({
99
121
  const handleToolChange = useCallback(
100
122
  (next) => {
101
123
  handleTextCommit();
124
+ if (next === "crop") {
125
+ controller.select(null);
126
+ setCropDraft(controller.crop ?? fullImageCrop(imageSize));
127
+ setCropAspect("free");
128
+ }
102
129
  setTool(next);
103
130
  },
104
- [handleTextCommit]
131
+ [handleTextCommit, controller, imageSize]
105
132
  );
133
+ const handleCropAspectChange = useCallback(
134
+ (aspect) => {
135
+ setCropAspect(aspect);
136
+ const ratio = aspectRatioFor(aspect, imageSize);
137
+ if (ratio === null) return;
138
+ setCropDraft((current) => fitCropToAspect(current ?? fullImageCrop(imageSize), ratio, imageSize));
139
+ },
140
+ [imageSize]
141
+ );
142
+ const handleCropReset = useCallback(() => {
143
+ setCropAspect("free");
144
+ setCropDraft(fullImageCrop(imageSize));
145
+ }, [imageSize]);
146
+ const handleCropCancel = useCallback(() => {
147
+ setCropDraft(null);
148
+ setTool("select");
149
+ }, []);
150
+ const handleCropApply = useCallback(() => {
151
+ const draft = cropDraft;
152
+ setCropDraft(null);
153
+ setTool("select");
154
+ if (!draft) return;
155
+ const next = isFullImageCrop(draft, imageSize) ? null : draft;
156
+ const current = controller.crop;
157
+ const unchanged = next === current || next !== null && current !== null && next.x === current.x && next.y === current.y && next.width === current.width && next.height === current.height;
158
+ if (!unchanged) controller.setCrop(next);
159
+ }, [cropDraft, imageSize, controller]);
160
+ useEffect(() => {
161
+ if (!isCropping) return;
162
+ const handleKeyDown = (event) => {
163
+ if (event.key === "Escape") {
164
+ event.preventDefault();
165
+ event.stopPropagation();
166
+ handleCropCancel();
167
+ return;
168
+ }
169
+ if (event.key === "Enter") {
170
+ event.preventDefault();
171
+ handleCropApply();
172
+ }
173
+ };
174
+ window.addEventListener("keydown", handleKeyDown, true);
175
+ return () => window.removeEventListener("keydown", handleKeyDown, true);
176
+ }, [isCropping, handleCropCancel, handleCropApply]);
106
177
  const handleInsertImage = useCallback(
107
178
  async (file) => {
108
179
  try {
109
180
  const source = await readFileAsDataUrl(file);
110
181
  const natural = await loadNaturalSize(source);
111
- addElement(createImageElement(source, natural, imageSize));
182
+ addElement(translateElement(createImageElement(source, natural, viewSize), viewRect.x, viewRect.y));
112
183
  setTool("select");
113
184
  } catch (error) {
114
185
  uiLogger.error("Failed to insert an image into the annotation", error);
115
186
  notify({ message: t("imageAnnotator.imageInsertFailed"), type: "error" });
116
187
  }
117
188
  },
118
- [addElement, imageSize, notify, t]
189
+ [addElement, viewSize, viewRect.x, viewRect.y, notify, t]
119
190
  );
120
191
  const handleRequestTextEdit = useCallback(
121
192
  (id) => {
@@ -144,14 +215,14 @@ const AnnotationEditor = ({
144
215
  }, [editingText, controller.elements, removeElement]);
145
216
  const handleSave = async () => {
146
217
  const stage = stageRef.current;
147
- if (!stage || saving) return;
218
+ if (!stage || saving || isCropping) return;
148
219
  setSaving(true);
149
220
  handleTextCommit();
150
221
  controller.select(null);
151
222
  try {
152
223
  await nextFrame();
153
224
  await nextFrame();
154
- const file = await exportStageToFile({ stage, imageSize, mimeType, imageName });
225
+ const file = await exportStageToFile({ stage, imageSize: viewSize, mimeType, imageName });
155
226
  await onSave(file, controller.document);
156
227
  onSaved();
157
228
  } catch (error) {
@@ -162,7 +233,7 @@ const AnnotationEditor = ({
162
233
  }
163
234
  };
164
235
  useAnnotationShortcuts({
165
- active: !editingText,
236
+ active: !editingText && !isCropping,
166
237
  controller,
167
238
  imageMaxDimension: imageMaxDimension(imageSize),
168
239
  onToolChange: handleToolChange
@@ -189,10 +260,10 @@ const AnnotationEditor = ({
189
260
  {
190
261
  touch,
191
262
  saving,
192
- canUndo: controller.canUndo,
193
- canRedo: controller.canRedo,
194
- canClear: !controller.isEmpty,
195
- canSave: !controller.isEmpty,
263
+ canUndo: controller.canUndo && !isCropping,
264
+ canRedo: controller.canRedo && !isCropping,
265
+ canClear: controller.elements.length > 0 && !isCropping,
266
+ canSave: !controller.isEmpty && !isCropping,
196
267
  elementActions: touch ? elementActions : void 0,
197
268
  onUndo: controller.undo,
198
269
  onRedo: controller.redo,
@@ -211,7 +282,18 @@ const AnnotationEditor = ({
211
282
  onInsertImage: handleInsertImage
212
283
  }
213
284
  ),
214
- /* @__PURE__ */ jsx(
285
+ isCropping ? /* @__PURE__ */ jsx(
286
+ AnnotationCropBar,
287
+ {
288
+ aspect: cropAspect,
289
+ touch,
290
+ canReset: !isFullImageCrop(cropDraft, imageSize),
291
+ onAspectChange: handleCropAspectChange,
292
+ onReset: handleCropReset,
293
+ onCancel: handleCropCancel,
294
+ onApply: handleCropApply
295
+ }
296
+ ) : /* @__PURE__ */ jsx(
215
297
  AnnotationStyleBar,
216
298
  {
217
299
  style,
@@ -228,34 +310,47 @@ const AnnotationEditor = ({
228
310
  {
229
311
  ref: viewportRef,
230
312
  className: "relative flex min-h-0 flex-1 items-center justify-center overflow-hidden rounded-xl bg-ink/95 p-3",
231
- children: fit.width > 1 && /* @__PURE__ */ jsxs("div", { className: "relative", style: { width: fit.width, height: fit.height }, children: [
232
- /* @__PURE__ */ jsx(
233
- AnnotationStage,
234
- {
235
- baseImage,
236
- imageSize,
237
- fit,
238
- tool,
239
- style,
240
- controller,
241
- editingTextId: editingText?.id ?? null,
242
- stageRef,
243
- onRequestTextEdit: handleRequestTextEdit,
244
- onTextPlaced: handleTextPlaced
245
- }
246
- ),
247
- editingText && editingElement && /* @__PURE__ */ jsx(
248
- AnnotationTextOverlay,
249
- {
250
- element: editingElement,
251
- viewScale: fit.scale,
252
- value: editingText.value,
253
- onChange: (value) => setEditingText((current) => current ? { ...current, value } : current),
254
- onCommit: handleTextCommit,
255
- onCancel: handleTextCancel
256
- }
257
- )
258
- ] })
313
+ children: fit.width > 1 && /* @__PURE__ */ jsxs(
314
+ "div",
315
+ {
316
+ className: "relative",
317
+ style: { width: fit.width + stageGutter * 2, height: fit.height + stageGutter * 2 },
318
+ children: [
319
+ /* @__PURE__ */ jsx(
320
+ AnnotationStage,
321
+ {
322
+ baseImage,
323
+ imageSize,
324
+ fit,
325
+ viewRect,
326
+ cropDraft,
327
+ cropAspectRatio: aspectRatioFor(cropAspect, imageSize),
328
+ gutter: stageGutter,
329
+ onCropDraftChange: setCropDraft,
330
+ tool,
331
+ style,
332
+ controller,
333
+ editingTextId: editingText?.id ?? null,
334
+ stageRef,
335
+ onRequestTextEdit: handleRequestTextEdit,
336
+ onTextPlaced: handleTextPlaced
337
+ }
338
+ ),
339
+ editingText && editingElement && /* @__PURE__ */ jsx(
340
+ AnnotationTextOverlay,
341
+ {
342
+ element: editingElement,
343
+ viewScale: fit.scale,
344
+ viewOrigin: viewRect,
345
+ value: editingText.value,
346
+ onChange: (value) => setEditingText((current) => current ? { ...current, value } : current),
347
+ onCommit: handleTextCommit,
348
+ onCancel: handleTextCancel
349
+ }
350
+ )
351
+ ]
352
+ }
353
+ )
259
354
  }
260
355
  ),
261
356
  touch && /* @__PURE__ */ jsx("div", { className: "shrink-0", children: actionBar })
@@ -1 +1 @@
1
- {"version":3,"file":"AnnotationEditor.js","sources":["../../../../src/components/UI/ImageAnnotator/AnnotationEditor.tsx"],"sourcesContent":["import { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport Konva from 'konva';\nimport { useTranslation } from 'react-i18next';\n\nimport { uiLogger } from '@/helpers/uiLogger';\nimport { useBreakpoint } from '@/hooks/useBreakpoint';\nimport { useNotification } from '@/hooks/useNotification';\n\nimport AnnotationStage from './canvas/AnnotationStage';\nimport AnnotationActionBar from './components/AnnotationActionBar';\nimport AnnotationElementActions from './components/AnnotationElementActions';\nimport AnnotationStyleBar from './components/AnnotationStyleBar';\nimport AnnotationTextOverlay from './components/AnnotationTextOverlay';\nimport AnnotationToolbar from './components/AnnotationToolbar';\nimport { fitStageToViewport } from './helpers/annotationCanvas';\nimport {\n applyStyleToElement,\n createImageElement,\n createTextElement,\n imageMaxDimension,\n styleFromElement,\n} from './helpers/annotationElements';\nimport { exportStageToFile } from './helpers/annotationExport';\nimport { useAnnotationDocument } from './hooks/useAnnotationDocument';\nimport { useAnnotationShortcuts } from './hooks/useAnnotationShortcuts';\nimport { useCoarsePointer } from './hooks/useCoarsePointer';\nimport {\n IAnnotationDocument,\n IAnnotationImageElement,\n IAnnotationPoint,\n IAnnotationSize,\n TAnnotationElement,\n TAnnotationElementType,\n TAnnotationTool,\n} from './interfaces/annotationDocument';\nimport { DEFAULT_ANNOTATION_STYLE, IAnnotationStyle } from './annotationStyle';\n\nconst ELEMENT_TOOLS: Array<TAnnotationTool> = ['freehand', 'line', 'arrow', 'rectangle', 'ellipse', 'text', 'image'];\n\ninterface IAnnotationEditorProps {\n baseImage: HTMLImageElement;\n imageSize: IAnnotationSize;\n mimeType: string;\n imageName?: string;\n initialElements: Array<TAnnotationElement>;\n onSave: (file: File, document: IAnnotationDocument) => Promise<void> | void;\n onSaved: () => void;\n}\n\nconst readFileAsDataUrl = (file: File): Promise<string> =>\n new Promise((resolve, reject) => {\n const reader = new FileReader();\n reader.onload = () => resolve(String(reader.result));\n reader.onerror = () => reject(new Error('Could not read the selected image'));\n reader.readAsDataURL(file);\n });\n\nconst loadNaturalSize = (source: string): Promise<IAnnotationSize> =>\n new Promise((resolve, reject) => {\n const element = new Image();\n element.onload = () => resolve({ width: element.naturalWidth, height: element.naturalHeight });\n element.onerror = () => reject(new Error('Could not decode the selected image'));\n element.src = source;\n });\n\nconst nextFrame = (): Promise<void> => new Promise((resolve) => window.requestAnimationFrame(() => resolve()));\n\nconst AnnotationEditor: React.FC<IAnnotationEditorProps> = ({\n baseImage,\n imageSize,\n mimeType,\n imageName,\n initialElements,\n onSave,\n onSaved,\n}) => {\n const { t } = useTranslation('ui');\n const { notify } = useNotification();\n const { isTabletDown } = useBreakpoint();\n const isCoarsePointer = useCoarsePointer();\n const touch = isCoarsePointer || isTabletDown;\n\n const stageRef = useRef<Konva.Stage | null>(null);\n const viewportRef = useRef<HTMLDivElement | null>(null);\n\n const [viewport, setViewport] = useState<IAnnotationSize>({ width: 0, height: 0 });\n const [tool, setTool] = useState<TAnnotationTool>('select');\n const [style, setStyle] = useState<IAnnotationStyle>(DEFAULT_ANNOTATION_STYLE);\n const [editingText, setEditingText] = useState<{ id: string; value: string } | null>(null);\n const [saving, setSaving] = useState(false);\n\n const controller = useAnnotationDocument(imageSize, initialElements);\n const { selectedElement, selectedId, updateSelected, addElement, removeElement, updateElement } = controller;\n\n useEffect(() => {\n const node = viewportRef.current;\n if (!node) return;\n\n const observer = new ResizeObserver((entries) => {\n const rect = entries[0]?.contentRect;\n if (rect) setViewport({ width: rect.width, height: rect.height });\n });\n observer.observe(node);\n\n return () => observer.disconnect();\n }, []);\n\n const fit = useMemo(() => fitStageToViewport(imageSize, viewport), [imageSize, viewport]);\n\n /** Selecting mirrors the element into the toolbar, so the controls never describe a stale style. */\n useEffect(() => {\n if (!selectedId) return;\n setStyle((current) => {\n const element = controller.elements.find((item) => item.id === selectedId);\n return element ? styleFromElement(element, current, imageSize) : current;\n });\n }, [selectedId]);\n\n const handleStyleChange = useCallback(\n (patch: Partial<IAnnotationStyle>) => {\n const next = { ...style, ...patch };\n setStyle(next);\n if (selectedId) updateSelected((element) => applyStyleToElement(element, next, imageSize));\n },\n [style, selectedId, updateSelected, imageSize],\n );\n\n const handleImageStyleChange = useCallback(\n (patch: Partial<IAnnotationImageElement>) => {\n updateSelected((element) => (element.type === 'image' ? { ...element, ...patch } : element));\n },\n [updateSelected],\n );\n\n const handleTextCommit = useCallback(() => {\n if (!editingText) return;\n const trimmed = editingText.value.trim();\n setEditingText(null);\n\n if (!trimmed) {\n removeElement(editingText.id);\n return;\n }\n updateElement(editingText.id, (element) => (element.type === 'text' ? { ...element, text: trimmed } : element));\n }, [editingText, removeElement, updateElement]);\n\n const handleToolChange = useCallback(\n (next: TAnnotationTool) => {\n handleTextCommit();\n setTool(next);\n },\n [handleTextCommit],\n );\n\n const handleInsertImage = useCallback(\n async (file: File) => {\n try {\n const source = await readFileAsDataUrl(file);\n const natural = await loadNaturalSize(source);\n addElement(createImageElement(source, natural, imageSize));\n setTool('select');\n } catch (error) {\n uiLogger.error('Failed to insert an image into the annotation', error);\n notify({ message: t('imageAnnotator.imageInsertFailed'), type: 'error' });\n }\n },\n [addElement, imageSize, notify, t],\n );\n\n const handleRequestTextEdit = useCallback(\n (id: string) => {\n const element = controller.elements.find((item) => item.id === id);\n if (element?.type !== 'text') return;\n setTool('select');\n controller.select(id);\n setEditingText({ id, value: element.text });\n },\n [controller],\n );\n\n /**\n * The element is created here, not in the stage: looking it up right after `addElement` would\n * read the previous render's list and silently skip opening the editor.\n */\n const handleTextPlaced = useCallback(\n (point: IAnnotationPoint) => {\n const element = createTextElement(point, style, imageSize);\n addElement(element);\n setTool('select');\n setEditingText({ id: element.id, value: '' });\n },\n [style, imageSize, addElement],\n );\n\n const handleTextCancel = useCallback(() => {\n if (!editingText) return;\n const element = controller.elements.find((item) => item.id === editingText.id);\n setEditingText(null);\n if (element?.type === 'text' && !element.text.trim()) removeElement(editingText.id);\n }, [editingText, controller.elements, removeElement]);\n\n const handleSave = async () => {\n const stage = stageRef.current;\n if (!stage || saving) return;\n\n setSaving(true);\n handleTextCommit();\n controller.select(null);\n\n try {\n await nextFrame();\n await nextFrame();\n const file = await exportStageToFile({ stage, imageSize, mimeType, imageName });\n await onSave(file, controller.document);\n onSaved();\n } catch (error) {\n uiLogger.error('Failed to save annotated image', error);\n notify({ message: t('imageAnnotator.sendFailed'), type: 'error' });\n } finally {\n setSaving(false);\n }\n };\n\n useAnnotationShortcuts({\n active: !editingText,\n controller,\n imageMaxDimension: imageMaxDimension(imageSize),\n onToolChange: handleToolChange,\n });\n\n const activeType = useMemo<TAnnotationElementType | null>(() => {\n if (selectedElement) return selectedElement.type;\n return ELEMENT_TOOLS.includes(tool) ? (tool as TAnnotationElementType) : null;\n }, [selectedElement, tool]);\n\n const editingElement = useMemo(() => {\n const element = controller.elements.find((item) => item.id === editingText?.id);\n return element?.type === 'text' ? element : null;\n }, [controller.elements, editingText?.id]);\n\n const elementActions = selectedElement ? (\n <AnnotationElementActions\n touch={touch}\n onReorder={controller.reorderSelected}\n onDuplicate={controller.duplicateSelected}\n onDelete={() => selectedId && removeElement(selectedId)}\n />\n ) : null;\n\n const actionBar = (\n <AnnotationActionBar\n touch={touch}\n saving={saving}\n canUndo={controller.canUndo}\n canRedo={controller.canRedo}\n canClear={!controller.isEmpty}\n canSave={!controller.isEmpty}\n elementActions={touch ? elementActions : undefined}\n onUndo={controller.undo}\n onRedo={controller.redo}\n onClear={controller.clear}\n onSave={handleSave}\n />\n );\n\n return (\n <div className=\"flex h-full flex-col gap-y-2\">\n <AnnotationToolbar\n tool={tool}\n touch={touch}\n actions={touch ? undefined : actionBar}\n onToolChange={handleToolChange}\n onInsertImage={handleInsertImage}\n />\n\n <AnnotationStyleBar\n style={style}\n activeType={activeType}\n selectedElement={selectedElement}\n touch={touch}\n elementActions={touch ? undefined : elementActions}\n onStyleChange={handleStyleChange}\n onImageStyleChange={handleImageStyleChange}\n />\n\n <div\n ref={viewportRef}\n className=\"relative flex min-h-0 flex-1 items-center justify-center overflow-hidden rounded-xl bg-ink/95 p-3\"\n >\n {fit.width > 1 && (\n <div className=\"relative\" style={{ width: fit.width, height: fit.height }}>\n <AnnotationStage\n baseImage={baseImage}\n imageSize={imageSize}\n fit={fit}\n tool={tool}\n style={style}\n controller={controller}\n editingTextId={editingText?.id ?? null}\n stageRef={stageRef}\n onRequestTextEdit={handleRequestTextEdit}\n onTextPlaced={handleTextPlaced}\n />\n\n {editingText && editingElement && (\n <AnnotationTextOverlay\n element={editingElement}\n viewScale={fit.scale}\n value={editingText.value}\n onChange={(value) => setEditingText((current) => (current ? { ...current, value } : current))}\n onCommit={handleTextCommit}\n onCancel={handleTextCancel}\n />\n )}\n </div>\n )}\n </div>\n\n {touch && <div className=\"shrink-0\">{actionBar}</div>}\n </div>\n );\n};\n\nexport default AnnotationEditor;\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"AnnotationEditor.js","sources":["../../../../src/components/UI/ImageAnnotator/AnnotationEditor.tsx"],"sourcesContent":["import { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport Konva from 'konva';\nimport { useTranslation } from 'react-i18next';\n\nimport { uiLogger } from '@/helpers/uiLogger';\nimport { useBreakpoint } from '@/hooks/useBreakpoint';\nimport { useNotification } from '@/hooks/useNotification';\n\nimport AnnotationStage from './canvas/AnnotationStage';\nimport AnnotationActionBar from './components/AnnotationActionBar';\nimport AnnotationCropBar from './components/AnnotationCropBar';\nimport AnnotationElementActions from './components/AnnotationElementActions';\nimport AnnotationStyleBar from './components/AnnotationStyleBar';\nimport AnnotationTextOverlay from './components/AnnotationTextOverlay';\nimport AnnotationToolbar from './components/AnnotationToolbar';\nimport { fitStageToViewport } from './helpers/annotationCanvas';\nimport {\n aspectRatioFor,\n fitCropToAspect,\n fullImageCrop,\n isFullImageCrop,\n TAnnotationCropAspect,\n} from './helpers/annotationCrop';\nimport {\n applyStyleToElement,\n createImageElement,\n createTextElement,\n imageMaxDimension,\n styleFromElement,\n translateElement,\n} from './helpers/annotationElements';\nimport { exportStageToFile } from './helpers/annotationExport';\nimport { useAnnotationDocument } from './hooks/useAnnotationDocument';\nimport { useAnnotationShortcuts } from './hooks/useAnnotationShortcuts';\nimport { useCoarsePointer } from './hooks/useCoarsePointer';\nimport {\n IAnnotationCropRect,\n IAnnotationDocument,\n IAnnotationImageElement,\n IAnnotationPoint,\n IAnnotationSize,\n TAnnotationElement,\n TAnnotationElementType,\n TAnnotationTool,\n} from './interfaces/annotationDocument';\nimport { DEFAULT_ANNOTATION_STYLE, IAnnotationStyle } from './annotationStyle';\n\nconst CROP_STAGE_GUTTER_PX = 12;\n\nconst ELEMENT_TOOLS: Array<TAnnotationTool> = ['freehand', 'line', 'arrow', 'rectangle', 'ellipse', 'text', 'image'];\n\ninterface IAnnotationEditorProps {\n baseImage: HTMLImageElement;\n imageSize: IAnnotationSize;\n mimeType: string;\n imageName?: string;\n initialElements: Array<TAnnotationElement>;\n initialCrop: IAnnotationCropRect | null;\n onSave: (file: File, document: IAnnotationDocument) => Promise<void> | void;\n onSaved: () => void;\n}\n\nconst readFileAsDataUrl = (file: File): Promise<string> =>\n new Promise((resolve, reject) => {\n const reader = new FileReader();\n reader.onload = () => resolve(String(reader.result));\n reader.onerror = () => reject(new Error('Could not read the selected image'));\n reader.readAsDataURL(file);\n });\n\nconst loadNaturalSize = (source: string): Promise<IAnnotationSize> =>\n new Promise((resolve, reject) => {\n const element = new Image();\n element.onload = () => resolve({ width: element.naturalWidth, height: element.naturalHeight });\n element.onerror = () => reject(new Error('Could not decode the selected image'));\n element.src = source;\n });\n\nconst nextFrame = (): Promise<void> => new Promise((resolve) => window.requestAnimationFrame(() => resolve()));\n\nconst AnnotationEditor: React.FC<IAnnotationEditorProps> = ({\n baseImage,\n imageSize,\n mimeType,\n imageName,\n initialElements,\n initialCrop,\n onSave,\n onSaved,\n}) => {\n const { t } = useTranslation('ui');\n const { notify } = useNotification();\n const { isTabletDown } = useBreakpoint();\n const isCoarsePointer = useCoarsePointer();\n const touch = isCoarsePointer || isTabletDown;\n\n const stageRef = useRef<Konva.Stage | null>(null);\n const viewportRef = useRef<HTMLDivElement | null>(null);\n\n const [viewport, setViewport] = useState<IAnnotationSize>({ width: 0, height: 0 });\n const [tool, setTool] = useState<TAnnotationTool>('select');\n const [style, setStyle] = useState<IAnnotationStyle>(DEFAULT_ANNOTATION_STYLE);\n const [editingText, setEditingText] = useState<{ id: string; value: string } | null>(null);\n const [saving, setSaving] = useState(false);\n const [cropDraft, setCropDraft] = useState<IAnnotationCropRect | null>(null);\n const [cropAspect, setCropAspect] = useState<TAnnotationCropAspect>('free');\n\n const controller = useAnnotationDocument(imageSize, initialElements, initialCrop);\n const { selectedElement, selectedId, updateSelected, addElement, removeElement, updateElement } = controller;\n\n const isCropping = tool === 'crop';\n const viewRect = useMemo<IAnnotationCropRect>(\n () => (isCropping ? fullImageCrop(imageSize) : (controller.crop ?? fullImageCrop(imageSize))),\n [isCropping, controller.crop, imageSize],\n );\n const viewSize = useMemo<IAnnotationSize>(\n () => ({ width: viewRect.width, height: viewRect.height }),\n [viewRect.width, viewRect.height],\n );\n\n useEffect(() => {\n const node = viewportRef.current;\n if (!node) return;\n\n const observer = new ResizeObserver((entries) => {\n const rect = entries[0]?.contentRect;\n if (rect) setViewport({ width: rect.width, height: rect.height });\n });\n observer.observe(node);\n\n return () => observer.disconnect();\n }, []);\n\n const stageGutter = isCropping ? CROP_STAGE_GUTTER_PX : 0;\n const fit = useMemo(\n () =>\n fitStageToViewport(viewSize, {\n width: viewport.width - stageGutter * 2,\n height: viewport.height - stageGutter * 2,\n }),\n [viewSize, viewport, stageGutter],\n );\n\n /** Selecting mirrors the element into the toolbar, so the controls never describe a stale style. */\n useEffect(() => {\n if (!selectedId) return;\n setStyle((current) => {\n const element = controller.elements.find((item) => item.id === selectedId);\n return element ? styleFromElement(element, current, imageSize) : current;\n });\n }, [selectedId]);\n\n const handleStyleChange = useCallback(\n (patch: Partial<IAnnotationStyle>) => {\n const next = { ...style, ...patch };\n setStyle(next);\n if (selectedId) updateSelected((element) => applyStyleToElement(element, next, imageSize));\n },\n [style, selectedId, updateSelected, imageSize],\n );\n\n const handleImageStyleChange = useCallback(\n (patch: Partial<IAnnotationImageElement>) => {\n updateSelected((element) => (element.type === 'image' ? { ...element, ...patch } : element));\n },\n [updateSelected],\n );\n\n const handleTextCommit = useCallback(() => {\n if (!editingText) return;\n const trimmed = editingText.value.trim();\n setEditingText(null);\n\n if (!trimmed) {\n removeElement(editingText.id);\n return;\n }\n updateElement(editingText.id, (element) => (element.type === 'text' ? { ...element, text: trimmed } : element));\n }, [editingText, removeElement, updateElement]);\n\n const handleToolChange = useCallback(\n (next: TAnnotationTool) => {\n handleTextCommit();\n if (next === 'crop') {\n controller.select(null);\n setCropDraft(controller.crop ?? fullImageCrop(imageSize));\n setCropAspect('free');\n }\n setTool(next);\n },\n [handleTextCommit, controller, imageSize],\n );\n\n const handleCropAspectChange = useCallback(\n (aspect: TAnnotationCropAspect) => {\n setCropAspect(aspect);\n const ratio = aspectRatioFor(aspect, imageSize);\n if (ratio === null) return;\n setCropDraft((current) => fitCropToAspect(current ?? fullImageCrop(imageSize), ratio, imageSize));\n },\n [imageSize],\n );\n\n const handleCropReset = useCallback(() => {\n setCropAspect('free');\n setCropDraft(fullImageCrop(imageSize));\n }, [imageSize]);\n\n const handleCropCancel = useCallback(() => {\n setCropDraft(null);\n setTool('select');\n }, []);\n\n const handleCropApply = useCallback(() => {\n const draft = cropDraft;\n setCropDraft(null);\n setTool('select');\n if (!draft) return;\n const next = isFullImageCrop(draft, imageSize) ? null : draft;\n const current = controller.crop;\n const unchanged =\n next === current ||\n (next !== null &&\n current !== null &&\n next.x === current.x &&\n next.y === current.y &&\n next.width === current.width &&\n next.height === current.height);\n if (!unchanged) controller.setCrop(next);\n }, [cropDraft, imageSize, controller]);\n\n useEffect(() => {\n if (!isCropping) return;\n\n const handleKeyDown = (event: KeyboardEvent) => {\n if (event.key === 'Escape') {\n event.preventDefault();\n event.stopPropagation();\n handleCropCancel();\n return;\n }\n if (event.key === 'Enter') {\n event.preventDefault();\n handleCropApply();\n }\n };\n\n window.addEventListener('keydown', handleKeyDown, true);\n return () => window.removeEventListener('keydown', handleKeyDown, true);\n }, [isCropping, handleCropCancel, handleCropApply]);\n\n const handleInsertImage = useCallback(\n async (file: File) => {\n try {\n const source = await readFileAsDataUrl(file);\n const natural = await loadNaturalSize(source);\n addElement(translateElement(createImageElement(source, natural, viewSize), viewRect.x, viewRect.y));\n setTool('select');\n } catch (error) {\n uiLogger.error('Failed to insert an image into the annotation', error);\n notify({ message: t('imageAnnotator.imageInsertFailed'), type: 'error' });\n }\n },\n [addElement, viewSize, viewRect.x, viewRect.y, notify, t],\n );\n\n const handleRequestTextEdit = useCallback(\n (id: string) => {\n const element = controller.elements.find((item) => item.id === id);\n if (element?.type !== 'text') return;\n setTool('select');\n controller.select(id);\n setEditingText({ id, value: element.text });\n },\n [controller],\n );\n\n /**\n * The element is created here, not in the stage: looking it up right after `addElement` would\n * read the previous render's list and silently skip opening the editor.\n */\n const handleTextPlaced = useCallback(\n (point: IAnnotationPoint) => {\n const element = createTextElement(point, style, imageSize);\n addElement(element);\n setTool('select');\n setEditingText({ id: element.id, value: '' });\n },\n [style, imageSize, addElement],\n );\n\n const handleTextCancel = useCallback(() => {\n if (!editingText) return;\n const element = controller.elements.find((item) => item.id === editingText.id);\n setEditingText(null);\n if (element?.type === 'text' && !element.text.trim()) removeElement(editingText.id);\n }, [editingText, controller.elements, removeElement]);\n\n const handleSave = async () => {\n const stage = stageRef.current;\n if (!stage || saving || isCropping) return;\n\n setSaving(true);\n handleTextCommit();\n controller.select(null);\n\n try {\n await nextFrame();\n await nextFrame();\n const file = await exportStageToFile({ stage, imageSize: viewSize, mimeType, imageName });\n await onSave(file, controller.document);\n onSaved();\n } catch (error) {\n uiLogger.error('Failed to save annotated image', error);\n notify({ message: t('imageAnnotator.sendFailed'), type: 'error' });\n } finally {\n setSaving(false);\n }\n };\n\n useAnnotationShortcuts({\n active: !editingText && !isCropping,\n controller,\n imageMaxDimension: imageMaxDimension(imageSize),\n onToolChange: handleToolChange,\n });\n\n const activeType = useMemo<TAnnotationElementType | null>(() => {\n if (selectedElement) return selectedElement.type;\n return ELEMENT_TOOLS.includes(tool) ? (tool as TAnnotationElementType) : null;\n }, [selectedElement, tool]);\n\n const editingElement = useMemo(() => {\n const element = controller.elements.find((item) => item.id === editingText?.id);\n return element?.type === 'text' ? element : null;\n }, [controller.elements, editingText?.id]);\n\n const elementActions = selectedElement ? (\n <AnnotationElementActions\n touch={touch}\n onReorder={controller.reorderSelected}\n onDuplicate={controller.duplicateSelected}\n onDelete={() => selectedId && removeElement(selectedId)}\n />\n ) : null;\n\n const actionBar = (\n <AnnotationActionBar\n touch={touch}\n saving={saving}\n canUndo={controller.canUndo && !isCropping}\n canRedo={controller.canRedo && !isCropping}\n canClear={controller.elements.length > 0 && !isCropping}\n canSave={!controller.isEmpty && !isCropping}\n elementActions={touch ? elementActions : undefined}\n onUndo={controller.undo}\n onRedo={controller.redo}\n onClear={controller.clear}\n onSave={handleSave}\n />\n );\n\n return (\n <div className=\"flex h-full flex-col gap-y-2\">\n <AnnotationToolbar\n tool={tool}\n touch={touch}\n actions={touch ? undefined : actionBar}\n onToolChange={handleToolChange}\n onInsertImage={handleInsertImage}\n />\n\n {isCropping ? (\n <AnnotationCropBar\n aspect={cropAspect}\n touch={touch}\n canReset={!isFullImageCrop(cropDraft, imageSize)}\n onAspectChange={handleCropAspectChange}\n onReset={handleCropReset}\n onCancel={handleCropCancel}\n onApply={handleCropApply}\n />\n ) : (\n <AnnotationStyleBar\n style={style}\n activeType={activeType}\n selectedElement={selectedElement}\n touch={touch}\n elementActions={touch ? undefined : elementActions}\n onStyleChange={handleStyleChange}\n onImageStyleChange={handleImageStyleChange}\n />\n )}\n\n <div\n ref={viewportRef}\n className=\"relative flex min-h-0 flex-1 items-center justify-center overflow-hidden rounded-xl bg-ink/95 p-3\"\n >\n {fit.width > 1 && (\n <div\n className=\"relative\"\n style={{ width: fit.width + stageGutter * 2, height: fit.height + stageGutter * 2 }}\n >\n <AnnotationStage\n baseImage={baseImage}\n imageSize={imageSize}\n fit={fit}\n viewRect={viewRect}\n cropDraft={cropDraft}\n cropAspectRatio={aspectRatioFor(cropAspect, imageSize)}\n gutter={stageGutter}\n onCropDraftChange={setCropDraft}\n tool={tool}\n style={style}\n controller={controller}\n editingTextId={editingText?.id ?? null}\n stageRef={stageRef}\n onRequestTextEdit={handleRequestTextEdit}\n onTextPlaced={handleTextPlaced}\n />\n\n {editingText && editingElement && (\n <AnnotationTextOverlay\n element={editingElement}\n viewScale={fit.scale}\n viewOrigin={viewRect}\n value={editingText.value}\n onChange={(value) => setEditingText((current) => (current ? { ...current, value } : current))}\n onCommit={handleTextCommit}\n onCancel={handleTextCancel}\n />\n )}\n </div>\n )}\n </div>\n\n {touch && <div className=\"shrink-0\">{actionBar}</div>}\n </div>\n );\n};\n\nexport default AnnotationEditor;\n"],"names":[],"mappings":"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"}
@@ -1,10 +1,12 @@
1
- import { IAnnotationDocument, TAnnotationElement } from './interfaces/annotationDocument';
1
+ import { IAnnotationCropRect, IAnnotationDocument, TAnnotationElement } from './interfaces/annotationDocument';
2
2
  interface IImageAnnotationDialogProps {
3
3
  open: boolean;
4
4
  imageUrl: string;
5
5
  imageName?: string;
6
6
  /** Previously saved annotations, when the host persists the document alongside the file. */
7
7
  initialElements?: Array<TAnnotationElement>;
8
+ /** Previously saved crop (`document.crop`), restored so a reopened image keeps its framing. */
9
+ initialCrop?: IAnnotationCropRect | null;
8
10
  onClose: () => void;
9
11
  /** Receives the flattened image plus the editable document; resolving closes the dialog. */
10
12
  onSave: (file: File, document: IAnnotationDocument) => Promise<void> | void;
@@ -5,6 +5,7 @@ import ControlledDialog from "../../Form/ControlledDialog.js";
5
5
  import LoadingSpinner from "../LoadingSpinner.js";
6
6
  import NoDataFound from "../NoDataFound/index.js";
7
7
  import { useBreakpoint } from "../../../hooks/useBreakpoint.js";
8
+ import { sanitizeInitialCrop } from "./helpers/annotationCrop.js";
8
9
  import { useAnnotationBaseImage } from "./hooks/useAnnotationBaseImage.js";
9
10
  const AnnotationEditor = lazy(() => import("./AnnotationEditor.js"));
10
11
  const ImageAnnotationDialog = ({
@@ -12,6 +13,7 @@ const ImageAnnotationDialog = ({
12
13
  imageUrl,
13
14
  imageName,
14
15
  initialElements,
16
+ initialCrop,
15
17
  onClose,
16
18
  onSave
17
19
  }) => {
@@ -42,6 +44,7 @@ const ImageAnnotationDialog = ({
42
44
  mimeType,
43
45
  imageName,
44
46
  initialElements: initialElements ?? [],
47
+ initialCrop: sanitizeInitialCrop(initialCrop, size),
45
48
  onSave,
46
49
  onSaved: onClose
47
50
  }
@@ -1 +1 @@
1
- {"version":3,"file":"ImageAnnotationDialog.js","sources":["../../../../src/components/UI/ImageAnnotator/ImageAnnotationDialog.tsx"],"sourcesContent":["import { lazy, Suspense } from 'react';\nimport { useTranslation } from 'react-i18next';\n\nimport ControlledDialog from '@/components/Form/ControlledDialog';\nimport LoadingSpinner from '@/components/UI/LoadingSpinner';\nimport NoDataFound from '@/components/UI/NoDataFound';\nimport { useBreakpoint } from '@/hooks/useBreakpoint';\n\nimport { useAnnotationBaseImage } from './hooks/useAnnotationBaseImage';\nimport { IAnnotationDocument, TAnnotationElement } from './interfaces/annotationDocument';\n\n/** Konva only reaches the bundle when someone actually opens the editor. */\nconst AnnotationEditor = lazy(() => import('./AnnotationEditor'));\n\ninterface IImageAnnotationDialogProps {\n open: boolean;\n imageUrl: string;\n imageName?: string;\n /** Previously saved annotations, when the host persists the document alongside the file. */\n initialElements?: Array<TAnnotationElement>;\n onClose: () => void;\n /** Receives the flattened image plus the editable document; resolving closes the dialog. */\n onSave: (file: File, document: IAnnotationDocument) => Promise<void> | void;\n}\n\nconst ImageAnnotationDialog: React.FC<IImageAnnotationDialogProps> = ({\n open,\n imageUrl,\n imageName,\n initialElements,\n onClose,\n onSave,\n}) => {\n const { t } = useTranslation('ui');\n const { isMobile } = useBreakpoint();\n const { image, size, mimeType, loading, failed } = useAnnotationBaseImage(open, imageUrl);\n\n return (\n <ControlledDialog\n open={open}\n size={isMobile ? 'xxl' : 'xl'}\n title={t('imageAnnotator.title')}\n onCancel={onClose}\n useFooter={false}\n fitBody\n >\n <div className=\"flex h-[78vh] flex-col\">\n {(loading || (!image && !failed)) && (\n <div className=\"flex flex-1 items-center justify-center\">\n <LoadingSpinner size={8} />\n </div>\n )}\n\n {failed && (\n <div className=\"flex flex-1 items-center justify-center\">\n <NoDataFound variant=\"error\" size=\"sm\" title={t('imageAnnotator.loadError')} />\n </div>\n )}\n\n {image && size && (\n <Suspense\n fallback={\n <div className=\"flex flex-1 items-center justify-center\">\n <LoadingSpinner size={8} />\n </div>\n }\n >\n <AnnotationEditor\n baseImage={image}\n imageSize={size}\n mimeType={mimeType}\n imageName={imageName}\n initialElements={initialElements ?? []}\n onSave={onSave}\n onSaved={onClose}\n />\n </Suspense>\n )}\n </div>\n </ControlledDialog>\n );\n};\n\nexport default ImageAnnotationDialog;\n"],"names":[],"mappings":";;;;;;;;AAYA,MAAM,mBAAmB,KAAK,MAAM,OAAO,uBAAoB,CAAC;AAahE,MAAM,wBAA+D,CAAC;AAAA,EACpE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,MAAM;AACJ,QAAM,EAAE,EAAA,IAAM,eAAe,IAAI;AACjC,QAAM,EAAE,SAAA,IAAa,cAAA;AACrB,QAAM,EAAE,OAAO,MAAM,UAAU,SAAS,WAAW,uBAAuB,MAAM,QAAQ;AAExF,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC;AAAA,MACA,MAAM,WAAW,QAAQ;AAAA,MACzB,OAAO,EAAE,sBAAsB;AAAA,MAC/B,UAAU;AAAA,MACV,WAAW;AAAA,MACX,SAAO;AAAA,MAEP,UAAA,qBAAC,OAAA,EAAI,WAAU,0BACX,UAAA;AAAA,SAAA,WAAY,CAAC,SAAS,CAAC,WACvB,oBAAC,OAAA,EAAI,WAAU,2CACb,UAAA,oBAAC,gBAAA,EAAe,MAAM,EAAA,CAAG,GAC3B;AAAA,QAGD,UACC,oBAAC,OAAA,EAAI,WAAU,2CACb,UAAA,oBAAC,aAAA,EAAY,SAAQ,SAAQ,MAAK,MAAK,OAAO,EAAE,0BAA0B,GAAG,GAC/E;AAAA,QAGD,SAAS,QACR;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,8BACG,OAAA,EAAI,WAAU,2CACb,UAAA,oBAAC,gBAAA,EAAe,MAAM,EAAA,CAAG,EAAA,CAC3B;AAAA,YAGF,UAAA;AAAA,cAAC;AAAA,cAAA;AAAA,gBACC,WAAW;AAAA,gBACX,WAAW;AAAA,gBACX;AAAA,gBACA;AAAA,gBACA,iBAAiB,mBAAmB,CAAA;AAAA,gBACpC;AAAA,gBACA,SAAS;AAAA,cAAA;AAAA,YAAA;AAAA,UACX;AAAA,QAAA;AAAA,MACF,EAAA,CAEJ;AAAA,IAAA;AAAA,EAAA;AAGN;"}
1
+ {"version":3,"file":"ImageAnnotationDialog.js","sources":["../../../../src/components/UI/ImageAnnotator/ImageAnnotationDialog.tsx"],"sourcesContent":["import { lazy, Suspense } from 'react';\nimport { useTranslation } from 'react-i18next';\n\nimport ControlledDialog from '@/components/Form/ControlledDialog';\nimport LoadingSpinner from '@/components/UI/LoadingSpinner';\nimport NoDataFound from '@/components/UI/NoDataFound';\nimport { useBreakpoint } from '@/hooks/useBreakpoint';\n\nimport { sanitizeInitialCrop } from './helpers/annotationCrop';\nimport { useAnnotationBaseImage } from './hooks/useAnnotationBaseImage';\nimport { IAnnotationCropRect, IAnnotationDocument, TAnnotationElement } from './interfaces/annotationDocument';\n\n/** Konva only reaches the bundle when someone actually opens the editor. */\nconst AnnotationEditor = lazy(() => import('./AnnotationEditor'));\n\ninterface IImageAnnotationDialogProps {\n open: boolean;\n imageUrl: string;\n imageName?: string;\n /** Previously saved annotations, when the host persists the document alongside the file. */\n initialElements?: Array<TAnnotationElement>;\n /** Previously saved crop (`document.crop`), restored so a reopened image keeps its framing. */\n initialCrop?: IAnnotationCropRect | null;\n onClose: () => void;\n /** Receives the flattened image plus the editable document; resolving closes the dialog. */\n onSave: (file: File, document: IAnnotationDocument) => Promise<void> | void;\n}\n\nconst ImageAnnotationDialog: React.FC<IImageAnnotationDialogProps> = ({\n open,\n imageUrl,\n imageName,\n initialElements,\n initialCrop,\n onClose,\n onSave,\n}) => {\n const { t } = useTranslation('ui');\n const { isMobile } = useBreakpoint();\n const { image, size, mimeType, loading, failed } = useAnnotationBaseImage(open, imageUrl);\n\n return (\n <ControlledDialog\n open={open}\n size={isMobile ? 'xxl' : 'xl'}\n title={t('imageAnnotator.title')}\n onCancel={onClose}\n useFooter={false}\n fitBody\n >\n <div className=\"flex h-[78vh] flex-col\">\n {(loading || (!image && !failed)) && (\n <div className=\"flex flex-1 items-center justify-center\">\n <LoadingSpinner size={8} />\n </div>\n )}\n\n {failed && (\n <div className=\"flex flex-1 items-center justify-center\">\n <NoDataFound variant=\"error\" size=\"sm\" title={t('imageAnnotator.loadError')} />\n </div>\n )}\n\n {image && size && (\n <Suspense\n fallback={\n <div className=\"flex flex-1 items-center justify-center\">\n <LoadingSpinner size={8} />\n </div>\n }\n >\n <AnnotationEditor\n baseImage={image}\n imageSize={size}\n mimeType={mimeType}\n imageName={imageName}\n initialElements={initialElements ?? []}\n initialCrop={sanitizeInitialCrop(initialCrop, size)}\n onSave={onSave}\n onSaved={onClose}\n />\n </Suspense>\n )}\n </div>\n </ControlledDialog>\n );\n};\n\nexport default ImageAnnotationDialog;\n"],"names":[],"mappings":";;;;;;;;;AAaA,MAAM,mBAAmB,KAAK,MAAM,OAAO,uBAAoB,CAAC;AAehE,MAAM,wBAA+D,CAAC;AAAA,EACpE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,MAAM;AACJ,QAAM,EAAE,EAAA,IAAM,eAAe,IAAI;AACjC,QAAM,EAAE,SAAA,IAAa,cAAA;AACrB,QAAM,EAAE,OAAO,MAAM,UAAU,SAAS,WAAW,uBAAuB,MAAM,QAAQ;AAExF,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC;AAAA,MACA,MAAM,WAAW,QAAQ;AAAA,MACzB,OAAO,EAAE,sBAAsB;AAAA,MAC/B,UAAU;AAAA,MACV,WAAW;AAAA,MACX,SAAO;AAAA,MAEP,UAAA,qBAAC,OAAA,EAAI,WAAU,0BACX,UAAA;AAAA,SAAA,WAAY,CAAC,SAAS,CAAC,WACvB,oBAAC,OAAA,EAAI,WAAU,2CACb,UAAA,oBAAC,gBAAA,EAAe,MAAM,EAAA,CAAG,GAC3B;AAAA,QAGD,UACC,oBAAC,OAAA,EAAI,WAAU,2CACb,UAAA,oBAAC,aAAA,EAAY,SAAQ,SAAQ,MAAK,MAAK,OAAO,EAAE,0BAA0B,GAAG,GAC/E;AAAA,QAGD,SAAS,QACR;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,8BACG,OAAA,EAAI,WAAU,2CACb,UAAA,oBAAC,gBAAA,EAAe,MAAM,EAAA,CAAG,EAAA,CAC3B;AAAA,YAGF,UAAA;AAAA,cAAC;AAAA,cAAA;AAAA,gBACC,WAAW;AAAA,gBACX,WAAW;AAAA,gBACX;AAAA,gBACA;AAAA,gBACA,iBAAiB,mBAAmB,CAAA;AAAA,gBACpC,aAAa,oBAAoB,aAAa,IAAI;AAAA,gBAClD;AAAA,gBACA,SAAS;AAAA,cAAA;AAAA,YAAA;AAAA,UACX;AAAA,QAAA;AAAA,MACF,EAAA,CAEJ;AAAA,IAAA;AAAA,EAAA;AAGN;"}
@@ -0,0 +1,16 @@
1
+ import { IAnnotationCropRect, IAnnotationSize } from '../interfaces/annotationDocument';
2
+ interface IAnnotationCropFrameProps {
3
+ crop: IAnnotationCropRect;
4
+ imageSize: IAnnotationSize;
5
+ viewScale: number;
6
+ /** Screen-pixel offset of the image inside the stage; absolute bounds start here. */
7
+ gutter: number;
8
+ aspectRatio: number | null;
9
+ onChange: (crop: IAnnotationCropRect) => void;
10
+ }
11
+ /**
12
+ * Drawn on a stage that shows the whole image, so the frame's local coordinates are image pixels
13
+ * and its absolute coordinates are those pixels times the view scale, shifted by the gutter.
14
+ */
15
+ declare const AnnotationCropFrame: React.FC<IAnnotationCropFrameProps>;
16
+ export default AnnotationCropFrame;