@umodeapporg/ui 0.16.1 → 0.17.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.
- package/dist/components/UI/ImageAiAdjust/ImageAiAdjustDialog.js +3 -2
- package/dist/components/UI/ImageAiAdjust/ImageAiAdjustDialog.js.map +1 -1
- package/dist/components/UI/ImageMaskPainter/index.d.ts +6 -1
- package/dist/components/UI/ImageMaskPainter/index.js +59 -44
- package/dist/components/UI/ImageMaskPainter/index.js.map +1 -1
- package/dist/helpers/objectHelpers.js +1 -2
- package/dist/helpers/objectHelpers.js.map +1 -1
- package/package.json +1 -1
|
@@ -16,6 +16,7 @@ import ImageAiDiscardReasonDialog from "./ImageAiDiscardReasonDialog.js";
|
|
|
16
16
|
import { submitImageAiReview } from "./imageAiReview.js";
|
|
17
17
|
import ImageAiVerificationNotice from "./ImageAiVerificationNotice.js";
|
|
18
18
|
import { useImageAiAdjust, IMAGE_AI_ADJUST_MAX_INSTRUCTIONS } from "./useImageAiAdjust.js";
|
|
19
|
+
const MASK_PAINTER_MAX_HEIGHT = "48vh";
|
|
19
20
|
const ImageAiAdjustDialog = ({
|
|
20
21
|
open,
|
|
21
22
|
imageUrl,
|
|
@@ -111,7 +112,7 @@ const ImageAiAdjustDialog = ({
|
|
|
111
112
|
fitBody: true,
|
|
112
113
|
children: [
|
|
113
114
|
/* @__PURE__ */ jsxs("div", { className: "flex max-h-[76vh] flex-col gap-y-3 overflow-y-auto", children: [
|
|
114
|
-
/* @__PURE__ */ jsxs("div", { className: classNames("flex gap-3", isTabletDown ? "flex-col" : "flex-row"), children: [
|
|
115
|
+
/* @__PURE__ */ jsxs("div", { className: classNames("flex gap-3", isTabletDown || markingArea ? "flex-col" : "flex-row"), children: [
|
|
115
116
|
/* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-y-1.5", children: [
|
|
116
117
|
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center justify-between gap-2", children: [
|
|
117
118
|
/* @__PURE__ */ jsx("span", { className: "text-xxs font-semibold uppercase tracking-wide text-foreground-subtle", children: t("imageAiAdjust.original") }),
|
|
@@ -140,7 +141,7 @@ const ImageAiAdjustDialog = ({
|
|
|
140
141
|
imageUrl,
|
|
141
142
|
disabled: isGenerating || saving,
|
|
142
143
|
onMaskChange: setMask,
|
|
143
|
-
|
|
144
|
+
maxHeight: MASK_PAINTER_MAX_HEIGHT
|
|
144
145
|
}
|
|
145
146
|
)
|
|
146
147
|
] }) : /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center rounded-xl bg-ink/95 p-2", children: /* @__PURE__ */ jsx(ControlledImage, { src: imageUrl, className: previewClass }) })
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ImageAiAdjustDialog.js","sources":["../../../../src/components/UI/ImageAiAdjust/ImageAiAdjustDialog.tsx"],"sourcesContent":["import { useState } from 'react';\nimport classNames from 'classnames';\nimport { useTranslation } from 'react-i18next';\n\nimport ControlledButton from '@/components/Form/ControlledButton';\nimport ControlledDialog from '@/components/Form/ControlledDialog';\nimport ControlledImage from '@/components/Form/ControlledImage';\nimport ControlledTextarea from '@/components/Form/ControlledTextarea';\nimport ImageMaskPainter from '@/components/UI/ImageMaskPainter';\nimport LoadingSpinner from '@/components/UI/LoadingSpinner';\nimport { MaterialSymbol } from '@/components/UI/MaterialSymbol';\nimport { uiLogger } from '@/helpers/uiLogger';\nimport { useBreakpoint } from '@/hooks/useBreakpoint';\nimport { useNotification } from '@/hooks/useNotification';\n\nimport ImageAiDiscardReasonDialog from './ImageAiDiscardReasonDialog';\nimport { IImageAiDiscardReason, submitImageAiReview, TImageAiReviewHandler } from './imageAiReview';\nimport ImageAiVerificationNotice from './ImageAiVerificationNotice';\nimport { IMAGE_AI_ADJUST_MAX_INSTRUCTIONS, TImageAiAdjuster, useImageAiAdjust } from './useImageAiAdjust';\n\ntype TPendingDiscard = 'tryAgain' | 'regenerate';\n\ninterface IImageAiAdjustDialogProps {\n open: boolean;\n imageUrl: string;\n imageName?: string;\n /** Opcional: quando existe, a geração fica associada ao produto no criAI. */\n productId?: string;\n /** Runs the generation; the host decides which AI backend serves it. */\n adjustImage: TImageAiAdjuster;\n onClose: () => void;\n /** Recebe a imagem ajustada; resolver fecha o diálogo, lançar mantém aberto. */\n onSave: (file: File) => Promise<void> | void;\n /**\n * Optional feedback sink. Only used when the adjuster resolved a `generationId`: using the\n * result approves it, discarding asks for a reason first. Failures never block the flow.\n */\n onReviewResult?: TImageAiReviewHandler;\n}\n\nconst ImageAiAdjustDialog: React.FC<IImageAiAdjustDialogProps> = ({\n open,\n imageUrl,\n imageName,\n productId,\n adjustImage,\n onClose,\n onSave,\n onReviewResult,\n}) => {\n const { t } = useTranslation('ui');\n const { isTabletDown } = useBreakpoint();\n const { notify } = useNotification();\n\n const [saving, setSaving] = useState(false);\n const [pendingDiscard, setPendingDiscard] = useState<TPendingDiscard | null>(null);\n const [markingArea, setMarkingArea] = useState(false);\n const [mask, setMask] = useState<File | null>(null);\n const {\n instructions,\n setInstructions,\n resultUrl,\n resultGenerationId,\n resultVerification,\n isGenerating,\n canGenerate,\n generate,\n cancel,\n discardResult,\n buildResultFile,\n } = useImageAiAdjust({ imageUrl, imageName, productId, adjustImage, mask: markingArea ? mask : null });\n\n const toggleMarkingArea = () => {\n if (markingArea) setMask(null);\n setMarkingArea(!markingArea);\n };\n\n const reviewEnabled = Boolean(onReviewResult && resultGenerationId);\n\n const handleSave = async () => {\n if (!resultUrl || saving) return;\n\n setSaving(true);\n try {\n await onSave(await buildResultFile());\n if (reviewEnabled && resultGenerationId) {\n void submitImageAiReview(onReviewResult, { generationId: resultGenerationId, decision: 'approved' });\n }\n onClose();\n } catch (error) {\n uiLogger.error('Failed to save the AI adjusted image', error);\n notify({ message: t('imageAiAdjust.errorSave'), type: 'error' });\n } finally {\n setSaving(false);\n }\n };\n\n const handleTryAgain = () => {\n if (reviewEnabled) {\n setPendingDiscard('tryAgain');\n return;\n }\n discardResult();\n };\n\n const handleGenerate = () => {\n if (reviewEnabled && resultUrl) {\n setPendingDiscard('regenerate');\n return;\n }\n void generate();\n };\n\n const handleDiscardConfirmed = ({ reasonCode, reason }: IImageAiDiscardReason) => {\n const action = pendingDiscard;\n setPendingDiscard(null);\n if (resultGenerationId) {\n void submitImageAiReview(onReviewResult, {\n generationId: resultGenerationId,\n decision: 'discarded',\n reasonCode,\n reason,\n });\n }\n if (action === 'regenerate') {\n void generate();\n return;\n }\n discardResult();\n };\n\n const previewClass = 'max-h-[38vh] w-auto max-w-full rounded-lg object-contain';\n\n return (\n <ControlledDialog\n open={open}\n size={isTabletDown ? 'xxl' : 'lg'}\n title={t('imageAiAdjust.title')}\n subtitle={t('imageAiAdjust.subtitle')}\n onCancel={onClose}\n useFooter={false}\n fitBody\n >\n <div className=\"flex max-h-[76vh] flex-col gap-y-3 overflow-y-auto\">\n <div className={classNames('flex gap-3', isTabletDown ? 'flex-col' : 'flex-row')}>\n <div className=\"flex min-w-0 flex-1 flex-col gap-y-1.5\">\n <div className=\"flex flex-wrap items-center justify-between gap-2\">\n <span className=\"text-xxs font-semibold uppercase tracking-wide text-foreground-subtle\">\n {t('imageAiAdjust.original')}\n </span>\n <ControlledButton\n size=\"sm\"\n variant=\"text\"\n tone={markingArea ? 'brand' : 'neutral'}\n aria-pressed={markingArea}\n disabled={isGenerating || saving}\n onClick={toggleMarkingArea}\n className=\"flex items-center gap-x-1.5 px-2 py-1 normal-case\"\n >\n <MaterialSymbol aria-hidden icon={markingArea ? 'close' : 'brush'} size={14} />\n {markingArea ? t('imageAiAdjust.mask.stop') : t('imageAiAdjust.mask.start')}\n </ControlledButton>\n </div>\n {markingArea ? (\n <>\n <span className=\"text-xs text-foreground-muted\">{t('imageAiAdjust.mask.hint')}</span>\n <ImageMaskPainter\n imageUrl={imageUrl}\n disabled={isGenerating || saving}\n onMaskChange={setMask}\n imageClassName={previewClass}\n />\n </>\n ) : (\n <div className=\"flex items-center justify-center rounded-xl bg-ink/95 p-2\">\n <ControlledImage src={imageUrl} className={previewClass} />\n </div>\n )}\n </div>\n\n <div className=\"flex min-w-0 flex-1 flex-col gap-y-1.5\">\n <span className=\"text-xxs font-semibold uppercase tracking-wide text-foreground-subtle\">\n {t('imageAiAdjust.result')}\n </span>\n <div className=\"flex min-h-40 items-center justify-center rounded-xl bg-ink/95 p-2\">\n {isGenerating && (\n <div className=\"flex flex-col items-center gap-y-2 p-4\">\n <LoadingSpinner size={8} color=\"white\" />\n <span className=\"text-center text-xs text-white\">{t('imageAiAdjust.generating')}</span>\n </div>\n )}\n\n {!isGenerating && resultUrl && <ControlledImage src={resultUrl} className={previewClass} />}\n\n {!isGenerating && !resultUrl && (\n <span className=\"max-w-64 p-4 text-center text-xs text-white/70\">{t('imageAiAdjust.resultHint')}</span>\n )}\n </div>\n {!isGenerating && resultUrl && <ImageAiVerificationNotice verification={resultVerification} />}\n </div>\n </div>\n\n <ControlledTextarea\n resize=\"none\"\n value={instructions}\n rows={3}\n maxLength={IMAGE_AI_ADJUST_MAX_INSTRUCTIONS}\n disabled={isGenerating || saving}\n aria-label={t('imageAiAdjust.promptLabel')}\n placeholder={t('imageAiAdjust.promptPlaceholder')}\n onChange={(event) => setInstructions(event.target.value)}\n className=\"rounded-xl\"\n />\n\n <div className=\"flex flex-wrap items-center justify-end gap-2\">\n {isGenerating ? (\n <ControlledButton size=\"sm\" variant=\"outlined\" onClick={cancel} className=\"normal-case\">\n {t('actions.cancel')}\n </ControlledButton>\n ) : (\n <>\n {resultUrl && (\n <ControlledButton\n size=\"sm\"\n variant=\"outlined\"\n disabled={saving}\n onClick={handleTryAgain}\n className=\"flex items-center gap-x-1.5 normal-case\"\n >\n <MaterialSymbol icon=\"refresh\" size={14} />\n {t('imageAiAdjust.tryAgain')}\n </ControlledButton>\n )}\n\n <ControlledButton\n size=\"sm\"\n variant={resultUrl ? 'outlined' : 'filled'}\n disabled={!canGenerate || saving}\n onClick={handleGenerate}\n className=\"flex items-center gap-x-1.5 normal-case\"\n >\n <MaterialSymbol icon=\"auto_fix_high\" size={14} />\n {t('imageAiAdjust.generate')}\n </ControlledButton>\n\n {resultUrl && (\n <ControlledButton\n size=\"sm\"\n disabled={saving}\n onClick={handleSave}\n className=\"flex items-center gap-x-1.5 normal-case\"\n >\n {saving ? (\n <LoadingSpinner size={4} color=\"white\" />\n ) : (\n <MaterialSymbol icon=\"check\" size={14} color=\"white\" />\n )}\n {t('imageAiAdjust.useResult')}\n </ControlledButton>\n )}\n </>\n )}\n </div>\n </div>\n\n <ImageAiDiscardReasonDialog\n open={pendingDiscard !== null}\n onCancel={() => setPendingDiscard(null)}\n onConfirm={handleDiscardConfirmed}\n />\n </ControlledDialog>\n );\n};\n\nexport default ImageAiAdjustDialog;\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ImageAiAdjustDialog.js","sources":["../../../../src/components/UI/ImageAiAdjust/ImageAiAdjustDialog.tsx"],"sourcesContent":["import { useState } from 'react';\nimport classNames from 'classnames';\nimport { useTranslation } from 'react-i18next';\n\nimport ControlledButton from '@/components/Form/ControlledButton';\nimport ControlledDialog from '@/components/Form/ControlledDialog';\nimport ControlledImage from '@/components/Form/ControlledImage';\nimport ControlledTextarea from '@/components/Form/ControlledTextarea';\nimport ImageMaskPainter from '@/components/UI/ImageMaskPainter';\nimport LoadingSpinner from '@/components/UI/LoadingSpinner';\nimport { MaterialSymbol } from '@/components/UI/MaterialSymbol';\nimport { uiLogger } from '@/helpers/uiLogger';\nimport { useBreakpoint } from '@/hooks/useBreakpoint';\nimport { useNotification } from '@/hooks/useNotification';\n\nimport ImageAiDiscardReasonDialog from './ImageAiDiscardReasonDialog';\nimport { IImageAiDiscardReason, submitImageAiReview, TImageAiReviewHandler } from './imageAiReview';\nimport ImageAiVerificationNotice from './ImageAiVerificationNotice';\nimport { IMAGE_AI_ADJUST_MAX_INSTRUCTIONS, TImageAiAdjuster, useImageAiAdjust } from './useImageAiAdjust';\n\ntype TPendingDiscard = 'tryAgain' | 'regenerate';\n\n/** Leaves room for the instructions and the actions under the painter without scrolling the dialog. */\nconst MASK_PAINTER_MAX_HEIGHT = '48vh';\n\ninterface IImageAiAdjustDialogProps {\n open: boolean;\n imageUrl: string;\n imageName?: string;\n /** Opcional: quando existe, a geração fica associada ao produto no criAI. */\n productId?: string;\n /** Runs the generation; the host decides which AI backend serves it. */\n adjustImage: TImageAiAdjuster;\n onClose: () => void;\n /** Recebe a imagem ajustada; resolver fecha o diálogo, lançar mantém aberto. */\n onSave: (file: File) => Promise<void> | void;\n /**\n * Optional feedback sink. Only used when the adjuster resolved a `generationId`: using the\n * result approves it, discarding asks for a reason first. Failures never block the flow.\n */\n onReviewResult?: TImageAiReviewHandler;\n}\n\nconst ImageAiAdjustDialog: React.FC<IImageAiAdjustDialogProps> = ({\n open,\n imageUrl,\n imageName,\n productId,\n adjustImage,\n onClose,\n onSave,\n onReviewResult,\n}) => {\n const { t } = useTranslation('ui');\n const { isTabletDown } = useBreakpoint();\n const { notify } = useNotification();\n\n const [saving, setSaving] = useState(false);\n const [pendingDiscard, setPendingDiscard] = useState<TPendingDiscard | null>(null);\n const [markingArea, setMarkingArea] = useState(false);\n const [mask, setMask] = useState<File | null>(null);\n const {\n instructions,\n setInstructions,\n resultUrl,\n resultGenerationId,\n resultVerification,\n isGenerating,\n canGenerate,\n generate,\n cancel,\n discardResult,\n buildResultFile,\n } = useImageAiAdjust({ imageUrl, imageName, productId, adjustImage, mask: markingArea ? mask : null });\n\n const toggleMarkingArea = () => {\n if (markingArea) setMask(null);\n setMarkingArea(!markingArea);\n };\n\n const reviewEnabled = Boolean(onReviewResult && resultGenerationId);\n\n const handleSave = async () => {\n if (!resultUrl || saving) return;\n\n setSaving(true);\n try {\n await onSave(await buildResultFile());\n if (reviewEnabled && resultGenerationId) {\n void submitImageAiReview(onReviewResult, { generationId: resultGenerationId, decision: 'approved' });\n }\n onClose();\n } catch (error) {\n uiLogger.error('Failed to save the AI adjusted image', error);\n notify({ message: t('imageAiAdjust.errorSave'), type: 'error' });\n } finally {\n setSaving(false);\n }\n };\n\n const handleTryAgain = () => {\n if (reviewEnabled) {\n setPendingDiscard('tryAgain');\n return;\n }\n discardResult();\n };\n\n const handleGenerate = () => {\n if (reviewEnabled && resultUrl) {\n setPendingDiscard('regenerate');\n return;\n }\n void generate();\n };\n\n const handleDiscardConfirmed = ({ reasonCode, reason }: IImageAiDiscardReason) => {\n const action = pendingDiscard;\n setPendingDiscard(null);\n if (resultGenerationId) {\n void submitImageAiReview(onReviewResult, {\n generationId: resultGenerationId,\n decision: 'discarded',\n reasonCode,\n reason,\n });\n }\n if (action === 'regenerate') {\n void generate();\n return;\n }\n discardResult();\n };\n\n const previewClass = 'max-h-[38vh] w-auto max-w-full rounded-lg object-contain';\n\n return (\n <ControlledDialog\n open={open}\n size={isTabletDown ? 'xxl' : 'lg'}\n title={t('imageAiAdjust.title')}\n subtitle={t('imageAiAdjust.subtitle')}\n onCancel={onClose}\n useFooter={false}\n fitBody\n >\n <div className=\"flex max-h-[76vh] flex-col gap-y-3 overflow-y-auto\">\n <div className={classNames('flex gap-3', isTabletDown || markingArea ? 'flex-col' : 'flex-row')}>\n <div className=\"flex min-w-0 flex-1 flex-col gap-y-1.5\">\n <div className=\"flex flex-wrap items-center justify-between gap-2\">\n <span className=\"text-xxs font-semibold uppercase tracking-wide text-foreground-subtle\">\n {t('imageAiAdjust.original')}\n </span>\n <ControlledButton\n size=\"sm\"\n variant=\"text\"\n tone={markingArea ? 'brand' : 'neutral'}\n aria-pressed={markingArea}\n disabled={isGenerating || saving}\n onClick={toggleMarkingArea}\n className=\"flex items-center gap-x-1.5 px-2 py-1 normal-case\"\n >\n <MaterialSymbol aria-hidden icon={markingArea ? 'close' : 'brush'} size={14} />\n {markingArea ? t('imageAiAdjust.mask.stop') : t('imageAiAdjust.mask.start')}\n </ControlledButton>\n </div>\n {markingArea ? (\n <>\n <span className=\"text-xs text-foreground-muted\">{t('imageAiAdjust.mask.hint')}</span>\n <ImageMaskPainter\n imageUrl={imageUrl}\n disabled={isGenerating || saving}\n onMaskChange={setMask}\n maxHeight={MASK_PAINTER_MAX_HEIGHT}\n />\n </>\n ) : (\n <div className=\"flex items-center justify-center rounded-xl bg-ink/95 p-2\">\n <ControlledImage src={imageUrl} className={previewClass} />\n </div>\n )}\n </div>\n\n <div className=\"flex min-w-0 flex-1 flex-col gap-y-1.5\">\n <span className=\"text-xxs font-semibold uppercase tracking-wide text-foreground-subtle\">\n {t('imageAiAdjust.result')}\n </span>\n <div className=\"flex min-h-40 items-center justify-center rounded-xl bg-ink/95 p-2\">\n {isGenerating && (\n <div className=\"flex flex-col items-center gap-y-2 p-4\">\n <LoadingSpinner size={8} color=\"white\" />\n <span className=\"text-center text-xs text-white\">{t('imageAiAdjust.generating')}</span>\n </div>\n )}\n\n {!isGenerating && resultUrl && <ControlledImage src={resultUrl} className={previewClass} />}\n\n {!isGenerating && !resultUrl && (\n <span className=\"max-w-64 p-4 text-center text-xs text-white/70\">{t('imageAiAdjust.resultHint')}</span>\n )}\n </div>\n {!isGenerating && resultUrl && <ImageAiVerificationNotice verification={resultVerification} />}\n </div>\n </div>\n\n <ControlledTextarea\n resize=\"none\"\n value={instructions}\n rows={3}\n maxLength={IMAGE_AI_ADJUST_MAX_INSTRUCTIONS}\n disabled={isGenerating || saving}\n aria-label={t('imageAiAdjust.promptLabel')}\n placeholder={t('imageAiAdjust.promptPlaceholder')}\n onChange={(event) => setInstructions(event.target.value)}\n className=\"rounded-xl\"\n />\n\n <div className=\"flex flex-wrap items-center justify-end gap-2\">\n {isGenerating ? (\n <ControlledButton size=\"sm\" variant=\"outlined\" onClick={cancel} className=\"normal-case\">\n {t('actions.cancel')}\n </ControlledButton>\n ) : (\n <>\n {resultUrl && (\n <ControlledButton\n size=\"sm\"\n variant=\"outlined\"\n disabled={saving}\n onClick={handleTryAgain}\n className=\"flex items-center gap-x-1.5 normal-case\"\n >\n <MaterialSymbol icon=\"refresh\" size={14} />\n {t('imageAiAdjust.tryAgain')}\n </ControlledButton>\n )}\n\n <ControlledButton\n size=\"sm\"\n variant={resultUrl ? 'outlined' : 'filled'}\n disabled={!canGenerate || saving}\n onClick={handleGenerate}\n className=\"flex items-center gap-x-1.5 normal-case\"\n >\n <MaterialSymbol icon=\"auto_fix_high\" size={14} />\n {t('imageAiAdjust.generate')}\n </ControlledButton>\n\n {resultUrl && (\n <ControlledButton\n size=\"sm\"\n disabled={saving}\n onClick={handleSave}\n className=\"flex items-center gap-x-1.5 normal-case\"\n >\n {saving ? (\n <LoadingSpinner size={4} color=\"white\" />\n ) : (\n <MaterialSymbol icon=\"check\" size={14} color=\"white\" />\n )}\n {t('imageAiAdjust.useResult')}\n </ControlledButton>\n )}\n </>\n )}\n </div>\n </div>\n\n <ImageAiDiscardReasonDialog\n open={pendingDiscard !== null}\n onCancel={() => setPendingDiscard(null)}\n onConfirm={handleDiscardConfirmed}\n />\n </ControlledDialog>\n );\n};\n\nexport default ImageAiAdjustDialog;\n"],"names":[],"mappings":"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"}
|
|
@@ -11,7 +11,12 @@ export interface IImageMaskPainterProps {
|
|
|
11
11
|
onMaskChange: (mask: File | null) => void;
|
|
12
12
|
disabled?: boolean;
|
|
13
13
|
maskFileName?: string;
|
|
14
|
-
/**
|
|
14
|
+
/**
|
|
15
|
+
* The tallest the image may get, as any CSS length. The image fills the available width up to
|
|
16
|
+
* this height, scaled up when its natural size is smaller.
|
|
17
|
+
*/
|
|
18
|
+
maxHeight?: string;
|
|
19
|
+
/** Sizes the image with classes instead, at its natural size at most. Turns the fill off. */
|
|
15
20
|
imageClassName?: string;
|
|
16
21
|
className?: string;
|
|
17
22
|
}
|
|
@@ -12,11 +12,13 @@ import { redrawStrokes, overlayHasPaint, exportMaskFile, drawStrokeTail } from "
|
|
|
12
12
|
const IMAGE_MASK_BRUSH_MIN = 8;
|
|
13
13
|
const IMAGE_MASK_BRUSH_MAX = 96;
|
|
14
14
|
const DEFAULT_BRUSH_SIZE = 32;
|
|
15
|
+
const DEFAULT_MAX_HEIGHT = "70vh";
|
|
15
16
|
const ImageMaskPainter = ({
|
|
16
17
|
imageUrl,
|
|
17
18
|
onMaskChange,
|
|
18
19
|
disabled = false,
|
|
19
20
|
maskFileName = "mask.png",
|
|
21
|
+
maxHeight = DEFAULT_MAX_HEIGHT,
|
|
20
22
|
imageClassName,
|
|
21
23
|
className
|
|
22
24
|
}) => {
|
|
@@ -28,8 +30,8 @@ const ImageMaskPainter = ({
|
|
|
28
30
|
const activeStrokeRef = useRef(null);
|
|
29
31
|
const onMaskChangeRef = useRef(onMaskChange);
|
|
30
32
|
const emitSequenceRef = useRef(0);
|
|
31
|
-
const [
|
|
32
|
-
const ready =
|
|
33
|
+
const [loaded, setLoaded] = useState(null);
|
|
34
|
+
const ready = loaded?.url === imageUrl;
|
|
33
35
|
const [mode, setMode] = useState("paint");
|
|
34
36
|
const [brushSize, setBrushSize] = useState(DEFAULT_BRUSH_SIZE);
|
|
35
37
|
const [strokeCount, setStrokeCount] = useState(0);
|
|
@@ -65,7 +67,7 @@ const ImageMaskPainter = ({
|
|
|
65
67
|
strokesRef.current = [];
|
|
66
68
|
activeStrokeRef.current = null;
|
|
67
69
|
setStrokeCount(0);
|
|
68
|
-
|
|
70
|
+
setLoaded({ url: imageUrl, width: naturalWidth, height: naturalHeight });
|
|
69
71
|
};
|
|
70
72
|
const toNaturalPoint = (event) => {
|
|
71
73
|
const canvas = event.currentTarget;
|
|
@@ -137,6 +139,11 @@ const ImageMaskPainter = ({
|
|
|
137
139
|
void emitMask();
|
|
138
140
|
};
|
|
139
141
|
const controlsDisabled = disabled || !ready;
|
|
142
|
+
const fillsFrame = !imageClassName && ready && loaded !== null;
|
|
143
|
+
const frameStyle = fillsFrame ? {
|
|
144
|
+
aspectRatio: `${loaded.width} / ${loaded.height}`,
|
|
145
|
+
width: `min(100%, calc(${maxHeight} * ${loaded.width / loaded.height}))`
|
|
146
|
+
} : void 0;
|
|
140
147
|
return /* @__PURE__ */ jsxs("div", { className: classNames("flex min-w-0 flex-col gap-y-2", className), children: [
|
|
141
148
|
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
|
|
142
149
|
/* @__PURE__ */ jsxs(
|
|
@@ -215,48 +222,56 @@ const ImageMaskPainter = ({
|
|
|
215
222
|
}
|
|
216
223
|
)
|
|
217
224
|
] }),
|
|
218
|
-
/* @__PURE__ */ jsx("div", { className: "flex items-center justify-center rounded-xl bg-ink/95 p-2", children: /* @__PURE__ */ jsxs(
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
225
|
+
/* @__PURE__ */ jsx("div", { className: "flex items-center justify-center rounded-xl bg-ink/95 p-2", children: /* @__PURE__ */ jsxs(
|
|
226
|
+
"div",
|
|
227
|
+
{
|
|
228
|
+
style: frameStyle,
|
|
229
|
+
className: classNames("relative max-w-full overflow-hidden", fillsFrame ? "min-w-0" : "inline-flex"),
|
|
230
|
+
children: [
|
|
231
|
+
/* @__PURE__ */ jsx(
|
|
232
|
+
"img",
|
|
233
|
+
{
|
|
234
|
+
src: imageUrl,
|
|
235
|
+
alt: "",
|
|
236
|
+
draggable: false,
|
|
237
|
+
onLoad: handleImageLoad,
|
|
238
|
+
style: imageClassName || fillsFrame ? void 0 : { maxHeight },
|
|
239
|
+
className: classNames(
|
|
240
|
+
"block select-none rounded-lg object-contain",
|
|
241
|
+
imageClassName ?? (fillsFrame ? "size-full" : "w-auto max-w-full")
|
|
242
|
+
)
|
|
243
|
+
},
|
|
244
|
+
imageUrl
|
|
245
|
+
),
|
|
246
|
+
/* @__PURE__ */ jsx(
|
|
247
|
+
"canvas",
|
|
248
|
+
{
|
|
249
|
+
ref: canvasRef,
|
|
250
|
+
role: "img",
|
|
251
|
+
"aria-label": t("imageMaskPainter.surfaceLabel"),
|
|
252
|
+
onPointerDown: handlePointerDown,
|
|
253
|
+
onPointerMove: handlePointerMove,
|
|
254
|
+
onPointerUp: finishStroke,
|
|
255
|
+
onPointerCancel: finishStroke,
|
|
256
|
+
onPointerLeave: hideCursor,
|
|
257
|
+
className: classNames(
|
|
258
|
+
"absolute inset-0 size-full touch-none rounded-lg opacity-50",
|
|
259
|
+
controlsDisabled ? "cursor-not-allowed" : "cursor-crosshair"
|
|
260
|
+
)
|
|
261
|
+
}
|
|
262
|
+
),
|
|
263
|
+
/* @__PURE__ */ jsx(
|
|
264
|
+
"span",
|
|
265
|
+
{
|
|
266
|
+
ref: cursorRef,
|
|
267
|
+
"aria-hidden": true,
|
|
268
|
+
style: { width: brushSize, height: brushSize },
|
|
269
|
+
className: "pointer-events-none absolute left-0 top-0 rounded-full border-2 border-brand opacity-0 shadow-sm"
|
|
270
|
+
}
|
|
229
271
|
)
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
/* @__PURE__ */ jsx(
|
|
234
|
-
"canvas",
|
|
235
|
-
{
|
|
236
|
-
ref: canvasRef,
|
|
237
|
-
role: "img",
|
|
238
|
-
"aria-label": t("imageMaskPainter.surfaceLabel"),
|
|
239
|
-
onPointerDown: handlePointerDown,
|
|
240
|
-
onPointerMove: handlePointerMove,
|
|
241
|
-
onPointerUp: finishStroke,
|
|
242
|
-
onPointerCancel: finishStroke,
|
|
243
|
-
onPointerLeave: hideCursor,
|
|
244
|
-
className: classNames(
|
|
245
|
-
"absolute inset-0 size-full touch-none rounded-lg opacity-50",
|
|
246
|
-
controlsDisabled ? "cursor-not-allowed" : "cursor-crosshair"
|
|
247
|
-
)
|
|
248
|
-
}
|
|
249
|
-
),
|
|
250
|
-
/* @__PURE__ */ jsx(
|
|
251
|
-
"span",
|
|
252
|
-
{
|
|
253
|
-
ref: cursorRef,
|
|
254
|
-
"aria-hidden": true,
|
|
255
|
-
style: { width: brushSize, height: brushSize },
|
|
256
|
-
className: "pointer-events-none absolute left-0 top-0 rounded-full border-2 border-brand opacity-0 shadow-sm"
|
|
257
|
-
}
|
|
258
|
-
)
|
|
259
|
-
] }) })
|
|
272
|
+
]
|
|
273
|
+
}
|
|
274
|
+
) })
|
|
260
275
|
] });
|
|
261
276
|
};
|
|
262
277
|
export {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../../../../src/components/UI/ImageMaskPainter/index.tsx"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react';\nimport classNames from 'classnames';\nimport { useTranslation } from 'react-i18next';\n\nimport ControlledButton from '@/components/Form/ControlledButton';\nimport ControlledSlider from '@/components/UI/ControlledSlider';\nimport { MaterialSymbol } from '@/components/UI/MaterialSymbol';\nimport { readThemeToken } from '@/helpers/themeTokens';\nimport { uiLogger } from '@/helpers/uiLogger';\nimport { useResolvedTheme } from '@/hooks/useResolvedTheme';\n\nimport {\n drawStrokeTail,\n exportMaskFile,\n IMaskPoint,\n IMaskStroke,\n overlayHasPaint,\n redrawStrokes,\n TMaskBrushMode,\n} from './maskStrokes';\n\nexport type { TMaskBrushMode } from './maskStrokes';\n\n/** Brush diameter in on-screen pixels; converted to natural pixels per stroke. */\nexport const IMAGE_MASK_BRUSH_MIN = 8;\nexport const IMAGE_MASK_BRUSH_MAX = 96;\nconst DEFAULT_BRUSH_SIZE = 32;\n\nexport interface IImageMaskPainterProps {\n imageUrl: string;\n /**\n * Called after every stroke, undo and clear with a PNG at the image's natural resolution\n * (white = painted, black = untouched), or null when nothing is painted.\n */\n onMaskChange: (mask: File | null) => void;\n disabled?: boolean;\n maskFileName?: string;\n /** Sizes the image; defaults to fitting a dialog preview. */\n imageClassName?: string;\n className?: string;\n}\n\nconst ImageMaskPainter: React.FC<IImageMaskPainterProps> = ({\n imageUrl,\n onMaskChange,\n disabled = false,\n maskFileName = 'mask.png',\n imageClassName,\n className,\n}) => {\n const { t } = useTranslation('ui');\n const resolvedTheme = useResolvedTheme();\n\n const canvasRef = useRef<HTMLCanvasElement | null>(null);\n const cursorRef = useRef<HTMLSpanElement | null>(null);\n const strokesRef = useRef<Array<IMaskStroke>>([]);\n const activeStrokeRef = useRef<{ pointerId: number; stroke: IMaskStroke } | null>(null);\n const onMaskChangeRef = useRef(onMaskChange);\n const emitSequenceRef = useRef(0);\n\n const [loadedUrl, setLoadedUrl] = useState<string | null>(null);\n const ready = loadedUrl === imageUrl;\n const [mode, setMode] = useState<TMaskBrushMode>('paint');\n const [brushSize, setBrushSize] = useState(DEFAULT_BRUSH_SIZE);\n const [strokeCount, setStrokeCount] = useState(0);\n\n useEffect(() => {\n onMaskChangeRef.current = onMaskChange;\n }, [onMaskChange]);\n\n const overlayColor = useCallback(() => readThemeToken('brand'), []);\n\n const getContext = useCallback(() => canvasRef.current?.getContext('2d') ?? null, []);\n\n const repaint = useCallback(() => {\n const context = getContext();\n if (context) redrawStrokes(context, strokesRef.current, overlayColor());\n }, [getContext, overlayColor]);\n\n // The brand token differs per theme; strokes already on screen follow it.\n useEffect(() => {\n if (ready) repaint();\n }, [ready, resolvedTheme, repaint]);\n\n const emitMask = useCallback(async () => {\n const sequence = ++emitSequenceRef.current;\n const canvas = canvasRef.current;\n try {\n const mask = canvas && overlayHasPaint(canvas) ? await exportMaskFile(canvas, maskFileName) : null;\n if (sequence === emitSequenceRef.current) onMaskChangeRef.current(mask);\n } catch (error) {\n uiLogger.error('Failed to export the image mask', error);\n if (sequence === emitSequenceRef.current) onMaskChangeRef.current(null);\n }\n }, [maskFileName]);\n\n const handleImageLoad = (event: React.SyntheticEvent<HTMLImageElement>) => {\n const canvas = canvasRef.current;\n const { naturalWidth, naturalHeight } = event.currentTarget;\n if (!canvas || !naturalWidth || !naturalHeight) return;\n canvas.width = naturalWidth;\n canvas.height = naturalHeight;\n strokesRef.current = [];\n activeStrokeRef.current = null;\n setStrokeCount(0);\n setLoadedUrl(imageUrl);\n };\n\n const toNaturalPoint = (event: React.PointerEvent<HTMLCanvasElement>): { point: IMaskPoint; scale: number } => {\n const canvas = event.currentTarget;\n const rect = canvas.getBoundingClientRect();\n const scale = rect.width > 0 ? canvas.width / rect.width : 1;\n return {\n point: { x: (event.clientX - rect.left) * scale, y: (event.clientY - rect.top) * scale },\n scale,\n };\n };\n\n const moveCursor = (event: React.PointerEvent<HTMLCanvasElement>) => {\n const cursor = cursorRef.current;\n if (!cursor) return;\n if (event.pointerType !== 'mouse') {\n cursor.style.opacity = '0';\n return;\n }\n const rect = event.currentTarget.getBoundingClientRect();\n cursor.style.opacity = '1';\n cursor.style.transform = `translate(${event.clientX - rect.left - brushSize / 2}px, ${event.clientY - rect.top - brushSize / 2}px)`;\n };\n\n const hideCursor = () => {\n if (cursorRef.current) cursorRef.current.style.opacity = '0';\n };\n\n const handlePointerDown = (event: React.PointerEvent<HTMLCanvasElement>) => {\n if (disabled || !ready || activeStrokeRef.current) return;\n if (event.pointerType === 'mouse' && event.button !== 0) return;\n const context = getContext();\n if (!context) return;\n\n event.preventDefault();\n try {\n event.currentTarget.setPointerCapture(event.pointerId);\n } catch {\n // Synthetic events carry no active pointer; drawing still works without capture.\n }\n const { point, scale } = toNaturalPoint(event);\n const stroke: IMaskStroke = { mode, size: Math.max(1, brushSize * scale), points: [point] };\n activeStrokeRef.current = { pointerId: event.pointerId, stroke };\n drawStrokeTail(context, stroke, overlayColor());\n };\n\n const handlePointerMove = (event: React.PointerEvent<HTMLCanvasElement>) => {\n moveCursor(event);\n const active = activeStrokeRef.current;\n if (!active || active.pointerId !== event.pointerId) return;\n const context = getContext();\n if (!context) return;\n\n active.stroke.points.push(toNaturalPoint(event).point);\n drawStrokeTail(context, active.stroke, overlayColor());\n };\n\n const finishStroke = (event: React.PointerEvent<HTMLCanvasElement>) => {\n const active = activeStrokeRef.current;\n if (!active || active.pointerId !== event.pointerId) return;\n activeStrokeRef.current = null;\n strokesRef.current = [...strokesRef.current, active.stroke];\n setStrokeCount(strokesRef.current.length);\n void emitMask();\n };\n\n const handleUndo = () => {\n if (strokesRef.current.length === 0) return;\n strokesRef.current = strokesRef.current.slice(0, -1);\n setStrokeCount(strokesRef.current.length);\n repaint();\n void emitMask();\n };\n\n const handleClear = () => {\n if (strokesRef.current.length === 0) return;\n strokesRef.current = [];\n setStrokeCount(0);\n repaint();\n void emitMask();\n };\n\n const controlsDisabled = disabled || !ready;\n\n return (\n <div className={classNames('flex min-w-0 flex-col gap-y-2', className)}>\n <div className=\"flex flex-wrap items-center gap-2\">\n <ControlledButton\n size=\"sm\"\n variant={mode === 'paint' ? 'filled' : 'outlined'}\n aria-pressed={mode === 'paint'}\n disabled={controlsDisabled}\n onClick={() => setMode('paint')}\n className=\"flex items-center gap-x-1.5 normal-case\"\n >\n <MaterialSymbol aria-hidden icon=\"brush\" size={14} />\n {t('imageMaskPainter.paint')}\n </ControlledButton>\n <ControlledButton\n size=\"sm\"\n variant={mode === 'erase' ? 'filled' : 'outlined'}\n aria-pressed={mode === 'erase'}\n disabled={controlsDisabled}\n onClick={() => setMode('erase')}\n className=\"flex items-center gap-x-1.5 normal-case\"\n >\n <MaterialSymbol aria-hidden icon=\"ink_eraser\" size={14} />\n {t('imageMaskPainter.erase')}\n </ControlledButton>\n\n <label className=\"flex min-w-32 flex-1 items-center gap-x-2 text-xs text-foreground-muted\">\n <span className=\"shrink-0\">{t('imageMaskPainter.brushSize')}</span>\n <ControlledSlider\n value={brushSize}\n min={IMAGE_MASK_BRUSH_MIN}\n max={IMAGE_MASK_BRUSH_MAX}\n step={2}\n disabled={controlsDisabled}\n label={t('imageMaskPainter.brushSize')}\n onChange={setBrushSize}\n />\n </label>\n\n <ControlledButton\n size=\"sm\"\n variant=\"text\"\n tone=\"neutral\"\n disabled={controlsDisabled || strokeCount === 0}\n onClick={handleUndo}\n className=\"flex items-center gap-x-1.5 normal-case\"\n >\n <MaterialSymbol aria-hidden icon=\"undo\" size={14} />\n {t('imageMaskPainter.undo')}\n </ControlledButton>\n <ControlledButton\n size=\"sm\"\n variant=\"text\"\n tone=\"neutral\"\n disabled={controlsDisabled || strokeCount === 0}\n onClick={handleClear}\n className=\"flex items-center gap-x-1.5 normal-case\"\n >\n <MaterialSymbol aria-hidden icon=\"layers_clear\" size={14} />\n {t('imageMaskPainter.clear')}\n </ControlledButton>\n </div>\n\n <div className=\"flex items-center justify-center rounded-xl bg-ink/95 p-2\">\n <div className=\"relative inline-flex max-w-full overflow-hidden\">\n <img\n key={imageUrl}\n src={imageUrl}\n alt=\"\"\n draggable={false}\n onLoad={handleImageLoad}\n className={classNames(\n 'block select-none rounded-lg object-contain',\n imageClassName ?? 'max-h-[38vh] w-auto max-w-full',\n )}\n />\n <canvas\n ref={canvasRef}\n role=\"img\"\n aria-label={t('imageMaskPainter.surfaceLabel')}\n onPointerDown={handlePointerDown}\n onPointerMove={handlePointerMove}\n onPointerUp={finishStroke}\n onPointerCancel={finishStroke}\n onPointerLeave={hideCursor}\n className={classNames(\n 'absolute inset-0 size-full touch-none rounded-lg opacity-50',\n controlsDisabled ? 'cursor-not-allowed' : 'cursor-crosshair',\n )}\n />\n <span\n ref={cursorRef}\n aria-hidden\n style={{ width: brushSize, height: brushSize }}\n className=\"pointer-events-none absolute left-0 top-0 rounded-full border-2 border-brand opacity-0 shadow-sm\"\n />\n </div>\n </div>\n </div>\n );\n};\n\nexport default ImageMaskPainter;\n"],"names":[],"mappings":";;;;;;;;;;;AAwBO,MAAM,uBAAuB;AAC7B,MAAM,uBAAuB;AACpC,MAAM,qBAAqB;AAgB3B,MAAM,mBAAqD,CAAC;AAAA,EAC1D;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EACX,eAAe;AAAA,EACf;AAAA,EACA;AACF,MAAM;AACJ,QAAM,EAAE,EAAA,IAAM,eAAe,IAAI;AACjC,QAAM,gBAAgB,iBAAA;AAEtB,QAAM,YAAY,OAAiC,IAAI;AACvD,QAAM,YAAY,OAA+B,IAAI;AACrD,QAAM,aAAa,OAA2B,EAAE;AAChD,QAAM,kBAAkB,OAA0D,IAAI;AACtF,QAAM,kBAAkB,OAAO,YAAY;AAC3C,QAAM,kBAAkB,OAAO,CAAC;AAEhC,QAAM,CAAC,WAAW,YAAY,IAAI,SAAwB,IAAI;AAC9D,QAAM,QAAQ,cAAc;AAC5B,QAAM,CAAC,MAAM,OAAO,IAAI,SAAyB,OAAO;AACxD,QAAM,CAAC,WAAW,YAAY,IAAI,SAAS,kBAAkB;AAC7D,QAAM,CAAC,aAAa,cAAc,IAAI,SAAS,CAAC;AAEhD,YAAU,MAAM;AACd,oBAAgB,UAAU;AAAA,EAC5B,GAAG,CAAC,YAAY,CAAC;AAEjB,QAAM,eAAe,YAAY,MAAM,eAAe,OAAO,GAAG,CAAA,CAAE;AAElE,QAAM,aAAa,YAAY,MAAM,UAAU,SAAS,WAAW,IAAI,KAAK,MAAM,EAAE;AAEpF,QAAM,UAAU,YAAY,MAAM;AAChC,UAAM,UAAU,WAAA;AAChB,QAAI,QAAS,eAAc,SAAS,WAAW,SAAS,cAAc;AAAA,EACxE,GAAG,CAAC,YAAY,YAAY,CAAC;AAG7B,YAAU,MAAM;AACd,QAAI,MAAO,SAAA;AAAA,EACb,GAAG,CAAC,OAAO,eAAe,OAAO,CAAC;AAElC,QAAM,WAAW,YAAY,YAAY;AACvC,UAAM,WAAW,EAAE,gBAAgB;AACnC,UAAM,SAAS,UAAU;AACzB,QAAI;AACF,YAAM,OAAO,UAAU,gBAAgB,MAAM,IAAI,MAAM,eAAe,QAAQ,YAAY,IAAI;AAC9F,UAAI,aAAa,gBAAgB,QAAS,iBAAgB,QAAQ,IAAI;AAAA,IACxE,SAAS,OAAO;AACd,eAAS,MAAM,mCAAmC,KAAK;AACvD,UAAI,aAAa,gBAAgB,QAAS,iBAAgB,QAAQ,IAAI;AAAA,IACxE;AAAA,EACF,GAAG,CAAC,YAAY,CAAC;AAEjB,QAAM,kBAAkB,CAAC,UAAkD;AACzE,UAAM,SAAS,UAAU;AACzB,UAAM,EAAE,cAAc,cAAA,IAAkB,MAAM;AAC9C,QAAI,CAAC,UAAU,CAAC,gBAAgB,CAAC,cAAe;AAChD,WAAO,QAAQ;AACf,WAAO,SAAS;AAChB,eAAW,UAAU,CAAA;AACrB,oBAAgB,UAAU;AAC1B,mBAAe,CAAC;AAChB,iBAAa,QAAQ;AAAA,EACvB;AAEA,QAAM,iBAAiB,CAAC,UAAuF;AAC7G,UAAM,SAAS,MAAM;AACrB,UAAM,OAAO,OAAO,sBAAA;AACpB,UAAM,QAAQ,KAAK,QAAQ,IAAI,OAAO,QAAQ,KAAK,QAAQ;AAC3D,WAAO;AAAA,MACL,OAAO,EAAE,IAAI,MAAM,UAAU,KAAK,QAAQ,OAAO,IAAI,MAAM,UAAU,KAAK,OAAO,MAAA;AAAA,MACjF;AAAA,IAAA;AAAA,EAEJ;AAEA,QAAM,aAAa,CAAC,UAAiD;AACnE,UAAM,SAAS,UAAU;AACzB,QAAI,CAAC,OAAQ;AACb,QAAI,MAAM,gBAAgB,SAAS;AACjC,aAAO,MAAM,UAAU;AACvB;AAAA,IACF;AACA,UAAM,OAAO,MAAM,cAAc,sBAAA;AACjC,WAAO,MAAM,UAAU;AACvB,WAAO,MAAM,YAAY,aAAa,MAAM,UAAU,KAAK,OAAO,YAAY,CAAC,OAAO,MAAM,UAAU,KAAK,MAAM,YAAY,CAAC;AAAA,EAChI;AAEA,QAAM,aAAa,MAAM;AACvB,QAAI,UAAU,QAAS,WAAU,QAAQ,MAAM,UAAU;AAAA,EAC3D;AAEA,QAAM,oBAAoB,CAAC,UAAiD;AAC1E,QAAI,YAAY,CAAC,SAAS,gBAAgB,QAAS;AACnD,QAAI,MAAM,gBAAgB,WAAW,MAAM,WAAW,EAAG;AACzD,UAAM,UAAU,WAAA;AAChB,QAAI,CAAC,QAAS;AAEd,UAAM,eAAA;AACN,QAAI;AACF,YAAM,cAAc,kBAAkB,MAAM,SAAS;AAAA,IACvD,QAAQ;AAAA,IAER;AACA,UAAM,EAAE,OAAO,UAAU,eAAe,KAAK;AAC7C,UAAM,SAAsB,EAAE,MAAM,MAAM,KAAK,IAAI,GAAG,YAAY,KAAK,GAAG,QAAQ,CAAC,KAAK,EAAA;AACxF,oBAAgB,UAAU,EAAE,WAAW,MAAM,WAAW,OAAA;AACxD,mBAAe,SAAS,QAAQ,cAAc;AAAA,EAChD;AAEA,QAAM,oBAAoB,CAAC,UAAiD;AAC1E,eAAW,KAAK;AAChB,UAAM,SAAS,gBAAgB;AAC/B,QAAI,CAAC,UAAU,OAAO,cAAc,MAAM,UAAW;AACrD,UAAM,UAAU,WAAA;AAChB,QAAI,CAAC,QAAS;AAEd,WAAO,OAAO,OAAO,KAAK,eAAe,KAAK,EAAE,KAAK;AACrD,mBAAe,SAAS,OAAO,QAAQ,aAAA,CAAc;AAAA,EACvD;AAEA,QAAM,eAAe,CAAC,UAAiD;AACrE,UAAM,SAAS,gBAAgB;AAC/B,QAAI,CAAC,UAAU,OAAO,cAAc,MAAM,UAAW;AACrD,oBAAgB,UAAU;AAC1B,eAAW,UAAU,CAAC,GAAG,WAAW,SAAS,OAAO,MAAM;AAC1D,mBAAe,WAAW,QAAQ,MAAM;AACxC,SAAK,SAAA;AAAA,EACP;AAEA,QAAM,aAAa,MAAM;AACvB,QAAI,WAAW,QAAQ,WAAW,EAAG;AACrC,eAAW,UAAU,WAAW,QAAQ,MAAM,GAAG,EAAE;AACnD,mBAAe,WAAW,QAAQ,MAAM;AACxC,YAAA;AACA,SAAK,SAAA;AAAA,EACP;AAEA,QAAM,cAAc,MAAM;AACxB,QAAI,WAAW,QAAQ,WAAW,EAAG;AACrC,eAAW,UAAU,CAAA;AACrB,mBAAe,CAAC;AAChB,YAAA;AACA,SAAK,SAAA;AAAA,EACP;AAEA,QAAM,mBAAmB,YAAY,CAAC;AAEtC,8BACG,OAAA,EAAI,WAAW,WAAW,iCAAiC,SAAS,GACnE,UAAA;AAAA,IAAA,qBAAC,OAAA,EAAI,WAAU,qCACb,UAAA;AAAA,MAAA;AAAA,QAAC;AAAA,QAAA;AAAA,UACC,MAAK;AAAA,UACL,SAAS,SAAS,UAAU,WAAW;AAAA,UACvC,gBAAc,SAAS;AAAA,UACvB,UAAU;AAAA,UACV,SAAS,MAAM,QAAQ,OAAO;AAAA,UAC9B,WAAU;AAAA,UAEV,UAAA;AAAA,YAAA,oBAAC,kBAAe,eAAW,MAAC,MAAK,SAAQ,MAAM,IAAI;AAAA,YAClD,EAAE,wBAAwB;AAAA,UAAA;AAAA,QAAA;AAAA,MAAA;AAAA,MAE7B;AAAA,QAAC;AAAA,QAAA;AAAA,UACC,MAAK;AAAA,UACL,SAAS,SAAS,UAAU,WAAW;AAAA,UACvC,gBAAc,SAAS;AAAA,UACvB,UAAU;AAAA,UACV,SAAS,MAAM,QAAQ,OAAO;AAAA,UAC9B,WAAU;AAAA,UAEV,UAAA;AAAA,YAAA,oBAAC,kBAAe,eAAW,MAAC,MAAK,cAAa,MAAM,IAAI;AAAA,YACvD,EAAE,wBAAwB;AAAA,UAAA;AAAA,QAAA;AAAA,MAAA;AAAA,MAG7B,qBAAC,SAAA,EAAM,WAAU,2EACf,UAAA;AAAA,QAAA,oBAAC,QAAA,EAAK,WAAU,YAAY,UAAA,EAAE,4BAA4B,GAAE;AAAA,QAC5D;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,OAAO;AAAA,YACP,KAAK;AAAA,YACL,KAAK;AAAA,YACL,MAAM;AAAA,YACN,UAAU;AAAA,YACV,OAAO,EAAE,4BAA4B;AAAA,YACrC,UAAU;AAAA,UAAA;AAAA,QAAA;AAAA,MACZ,GACF;AAAA,MAEA;AAAA,QAAC;AAAA,QAAA;AAAA,UACC,MAAK;AAAA,UACL,SAAQ;AAAA,UACR,MAAK;AAAA,UACL,UAAU,oBAAoB,gBAAgB;AAAA,UAC9C,SAAS;AAAA,UACT,WAAU;AAAA,UAEV,UAAA;AAAA,YAAA,oBAAC,kBAAe,eAAW,MAAC,MAAK,QAAO,MAAM,IAAI;AAAA,YACjD,EAAE,uBAAuB;AAAA,UAAA;AAAA,QAAA;AAAA,MAAA;AAAA,MAE5B;AAAA,QAAC;AAAA,QAAA;AAAA,UACC,MAAK;AAAA,UACL,SAAQ;AAAA,UACR,MAAK;AAAA,UACL,UAAU,oBAAoB,gBAAgB;AAAA,UAC9C,SAAS;AAAA,UACT,WAAU;AAAA,UAEV,UAAA;AAAA,YAAA,oBAAC,kBAAe,eAAW,MAAC,MAAK,gBAAe,MAAM,IAAI;AAAA,YACzD,EAAE,wBAAwB;AAAA,UAAA;AAAA,QAAA;AAAA,MAAA;AAAA,IAC7B,GACF;AAAA,wBAEC,OAAA,EAAI,WAAU,6DACb,UAAA,qBAAC,OAAA,EAAI,WAAU,mDACb,UAAA;AAAA,MAAA;AAAA,QAAC;AAAA,QAAA;AAAA,UAEC,KAAK;AAAA,UACL,KAAI;AAAA,UACJ,WAAW;AAAA,UACX,QAAQ;AAAA,UACR,WAAW;AAAA,YACT;AAAA,YACA,kBAAkB;AAAA,UAAA;AAAA,QACpB;AAAA,QARK;AAAA,MAAA;AAAA,MAUP;AAAA,QAAC;AAAA,QAAA;AAAA,UACC,KAAK;AAAA,UACL,MAAK;AAAA,UACL,cAAY,EAAE,+BAA+B;AAAA,UAC7C,eAAe;AAAA,UACf,eAAe;AAAA,UACf,aAAa;AAAA,UACb,iBAAiB;AAAA,UACjB,gBAAgB;AAAA,UAChB,WAAW;AAAA,YACT;AAAA,YACA,mBAAmB,uBAAuB;AAAA,UAAA;AAAA,QAC5C;AAAA,MAAA;AAAA,MAEF;AAAA,QAAC;AAAA,QAAA;AAAA,UACC,KAAK;AAAA,UACL,eAAW;AAAA,UACX,OAAO,EAAE,OAAO,WAAW,QAAQ,UAAA;AAAA,UACnC,WAAU;AAAA,QAAA;AAAA,MAAA;AAAA,IACZ,EAAA,CACF,EAAA,CACF;AAAA,EAAA,GACF;AAEJ;"}
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../../../../src/components/UI/ImageMaskPainter/index.tsx"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react';\nimport classNames from 'classnames';\nimport { useTranslation } from 'react-i18next';\n\nimport ControlledButton from '@/components/Form/ControlledButton';\nimport ControlledSlider from '@/components/UI/ControlledSlider';\nimport { MaterialSymbol } from '@/components/UI/MaterialSymbol';\nimport { readThemeToken } from '@/helpers/themeTokens';\nimport { uiLogger } from '@/helpers/uiLogger';\nimport { useResolvedTheme } from '@/hooks/useResolvedTheme';\n\nimport {\n drawStrokeTail,\n exportMaskFile,\n IMaskPoint,\n IMaskStroke,\n overlayHasPaint,\n redrawStrokes,\n TMaskBrushMode,\n} from './maskStrokes';\n\nexport type { TMaskBrushMode } from './maskStrokes';\n\n/** Brush diameter in on-screen pixels; converted to natural pixels per stroke. */\nexport const IMAGE_MASK_BRUSH_MIN = 8;\nexport const IMAGE_MASK_BRUSH_MAX = 96;\nconst DEFAULT_BRUSH_SIZE = 32;\nconst DEFAULT_MAX_HEIGHT = '70vh';\n\ninterface ILoadedImage {\n url: string;\n width: number;\n height: number;\n}\n\nexport interface IImageMaskPainterProps {\n imageUrl: string;\n /**\n * Called after every stroke, undo and clear with a PNG at the image's natural resolution\n * (white = painted, black = untouched), or null when nothing is painted.\n */\n onMaskChange: (mask: File | null) => void;\n disabled?: boolean;\n maskFileName?: string;\n /**\n * The tallest the image may get, as any CSS length. The image fills the available width up to\n * this height, scaled up when its natural size is smaller.\n */\n maxHeight?: string;\n /** Sizes the image with classes instead, at its natural size at most. Turns the fill off. */\n imageClassName?: string;\n className?: string;\n}\n\nconst ImageMaskPainter: React.FC<IImageMaskPainterProps> = ({\n imageUrl,\n onMaskChange,\n disabled = false,\n maskFileName = 'mask.png',\n maxHeight = DEFAULT_MAX_HEIGHT,\n imageClassName,\n className,\n}) => {\n const { t } = useTranslation('ui');\n const resolvedTheme = useResolvedTheme();\n\n const canvasRef = useRef<HTMLCanvasElement | null>(null);\n const cursorRef = useRef<HTMLSpanElement | null>(null);\n const strokesRef = useRef<Array<IMaskStroke>>([]);\n const activeStrokeRef = useRef<{ pointerId: number; stroke: IMaskStroke } | null>(null);\n const onMaskChangeRef = useRef(onMaskChange);\n const emitSequenceRef = useRef(0);\n\n const [loaded, setLoaded] = useState<ILoadedImage | null>(null);\n const ready = loaded?.url === imageUrl;\n const [mode, setMode] = useState<TMaskBrushMode>('paint');\n const [brushSize, setBrushSize] = useState(DEFAULT_BRUSH_SIZE);\n const [strokeCount, setStrokeCount] = useState(0);\n\n useEffect(() => {\n onMaskChangeRef.current = onMaskChange;\n }, [onMaskChange]);\n\n const overlayColor = useCallback(() => readThemeToken('brand'), []);\n\n const getContext = useCallback(() => canvasRef.current?.getContext('2d') ?? null, []);\n\n const repaint = useCallback(() => {\n const context = getContext();\n if (context) redrawStrokes(context, strokesRef.current, overlayColor());\n }, [getContext, overlayColor]);\n\n // The brand token differs per theme; strokes already on screen follow it.\n useEffect(() => {\n if (ready) repaint();\n }, [ready, resolvedTheme, repaint]);\n\n const emitMask = useCallback(async () => {\n const sequence = ++emitSequenceRef.current;\n const canvas = canvasRef.current;\n try {\n const mask = canvas && overlayHasPaint(canvas) ? await exportMaskFile(canvas, maskFileName) : null;\n if (sequence === emitSequenceRef.current) onMaskChangeRef.current(mask);\n } catch (error) {\n uiLogger.error('Failed to export the image mask', error);\n if (sequence === emitSequenceRef.current) onMaskChangeRef.current(null);\n }\n }, [maskFileName]);\n\n const handleImageLoad = (event: React.SyntheticEvent<HTMLImageElement>) => {\n const canvas = canvasRef.current;\n const { naturalWidth, naturalHeight } = event.currentTarget;\n if (!canvas || !naturalWidth || !naturalHeight) return;\n canvas.width = naturalWidth;\n canvas.height = naturalHeight;\n strokesRef.current = [];\n activeStrokeRef.current = null;\n setStrokeCount(0);\n setLoaded({ url: imageUrl, width: naturalWidth, height: naturalHeight });\n };\n\n const toNaturalPoint = (event: React.PointerEvent<HTMLCanvasElement>): { point: IMaskPoint; scale: number } => {\n const canvas = event.currentTarget;\n const rect = canvas.getBoundingClientRect();\n const scale = rect.width > 0 ? canvas.width / rect.width : 1;\n return {\n point: { x: (event.clientX - rect.left) * scale, y: (event.clientY - rect.top) * scale },\n scale,\n };\n };\n\n const moveCursor = (event: React.PointerEvent<HTMLCanvasElement>) => {\n const cursor = cursorRef.current;\n if (!cursor) return;\n if (event.pointerType !== 'mouse') {\n cursor.style.opacity = '0';\n return;\n }\n const rect = event.currentTarget.getBoundingClientRect();\n cursor.style.opacity = '1';\n cursor.style.transform = `translate(${event.clientX - rect.left - brushSize / 2}px, ${event.clientY - rect.top - brushSize / 2}px)`;\n };\n\n const hideCursor = () => {\n if (cursorRef.current) cursorRef.current.style.opacity = '0';\n };\n\n const handlePointerDown = (event: React.PointerEvent<HTMLCanvasElement>) => {\n if (disabled || !ready || activeStrokeRef.current) return;\n if (event.pointerType === 'mouse' && event.button !== 0) return;\n const context = getContext();\n if (!context) return;\n\n event.preventDefault();\n try {\n event.currentTarget.setPointerCapture(event.pointerId);\n } catch {\n // Synthetic events carry no active pointer; drawing still works without capture.\n }\n const { point, scale } = toNaturalPoint(event);\n const stroke: IMaskStroke = { mode, size: Math.max(1, brushSize * scale), points: [point] };\n activeStrokeRef.current = { pointerId: event.pointerId, stroke };\n drawStrokeTail(context, stroke, overlayColor());\n };\n\n const handlePointerMove = (event: React.PointerEvent<HTMLCanvasElement>) => {\n moveCursor(event);\n const active = activeStrokeRef.current;\n if (!active || active.pointerId !== event.pointerId) return;\n const context = getContext();\n if (!context) return;\n\n active.stroke.points.push(toNaturalPoint(event).point);\n drawStrokeTail(context, active.stroke, overlayColor());\n };\n\n const finishStroke = (event: React.PointerEvent<HTMLCanvasElement>) => {\n const active = activeStrokeRef.current;\n if (!active || active.pointerId !== event.pointerId) return;\n activeStrokeRef.current = null;\n strokesRef.current = [...strokesRef.current, active.stroke];\n setStrokeCount(strokesRef.current.length);\n void emitMask();\n };\n\n const handleUndo = () => {\n if (strokesRef.current.length === 0) return;\n strokesRef.current = strokesRef.current.slice(0, -1);\n setStrokeCount(strokesRef.current.length);\n repaint();\n void emitMask();\n };\n\n const handleClear = () => {\n if (strokesRef.current.length === 0) return;\n strokesRef.current = [];\n setStrokeCount(0);\n repaint();\n void emitMask();\n };\n\n const controlsDisabled = disabled || !ready;\n\n // The frame takes the image's proportions, so the canvas over it always matches the image.\n const fillsFrame = !imageClassName && ready && loaded !== null;\n const frameStyle = fillsFrame\n ? {\n aspectRatio: `${loaded.width} / ${loaded.height}`,\n width: `min(100%, calc(${maxHeight} * ${loaded.width / loaded.height}))`,\n }\n : undefined;\n\n return (\n <div className={classNames('flex min-w-0 flex-col gap-y-2', className)}>\n <div className=\"flex flex-wrap items-center gap-2\">\n <ControlledButton\n size=\"sm\"\n variant={mode === 'paint' ? 'filled' : 'outlined'}\n aria-pressed={mode === 'paint'}\n disabled={controlsDisabled}\n onClick={() => setMode('paint')}\n className=\"flex items-center gap-x-1.5 normal-case\"\n >\n <MaterialSymbol aria-hidden icon=\"brush\" size={14} />\n {t('imageMaskPainter.paint')}\n </ControlledButton>\n <ControlledButton\n size=\"sm\"\n variant={mode === 'erase' ? 'filled' : 'outlined'}\n aria-pressed={mode === 'erase'}\n disabled={controlsDisabled}\n onClick={() => setMode('erase')}\n className=\"flex items-center gap-x-1.5 normal-case\"\n >\n <MaterialSymbol aria-hidden icon=\"ink_eraser\" size={14} />\n {t('imageMaskPainter.erase')}\n </ControlledButton>\n\n <label className=\"flex min-w-32 flex-1 items-center gap-x-2 text-xs text-foreground-muted\">\n <span className=\"shrink-0\">{t('imageMaskPainter.brushSize')}</span>\n <ControlledSlider\n value={brushSize}\n min={IMAGE_MASK_BRUSH_MIN}\n max={IMAGE_MASK_BRUSH_MAX}\n step={2}\n disabled={controlsDisabled}\n label={t('imageMaskPainter.brushSize')}\n onChange={setBrushSize}\n />\n </label>\n\n <ControlledButton\n size=\"sm\"\n variant=\"text\"\n tone=\"neutral\"\n disabled={controlsDisabled || strokeCount === 0}\n onClick={handleUndo}\n className=\"flex items-center gap-x-1.5 normal-case\"\n >\n <MaterialSymbol aria-hidden icon=\"undo\" size={14} />\n {t('imageMaskPainter.undo')}\n </ControlledButton>\n <ControlledButton\n size=\"sm\"\n variant=\"text\"\n tone=\"neutral\"\n disabled={controlsDisabled || strokeCount === 0}\n onClick={handleClear}\n className=\"flex items-center gap-x-1.5 normal-case\"\n >\n <MaterialSymbol aria-hidden icon=\"layers_clear\" size={14} />\n {t('imageMaskPainter.clear')}\n </ControlledButton>\n </div>\n\n <div className=\"flex items-center justify-center rounded-xl bg-ink/95 p-2\">\n <div\n style={frameStyle}\n className={classNames('relative max-w-full overflow-hidden', fillsFrame ? 'min-w-0' : 'inline-flex')}\n >\n <img\n key={imageUrl}\n src={imageUrl}\n alt=\"\"\n draggable={false}\n onLoad={handleImageLoad}\n style={imageClassName || fillsFrame ? undefined : { maxHeight }}\n className={classNames(\n 'block select-none rounded-lg object-contain',\n imageClassName ?? (fillsFrame ? 'size-full' : 'w-auto max-w-full'),\n )}\n />\n <canvas\n ref={canvasRef}\n role=\"img\"\n aria-label={t('imageMaskPainter.surfaceLabel')}\n onPointerDown={handlePointerDown}\n onPointerMove={handlePointerMove}\n onPointerUp={finishStroke}\n onPointerCancel={finishStroke}\n onPointerLeave={hideCursor}\n className={classNames(\n 'absolute inset-0 size-full touch-none rounded-lg opacity-50',\n controlsDisabled ? 'cursor-not-allowed' : 'cursor-crosshair',\n )}\n />\n <span\n ref={cursorRef}\n aria-hidden\n style={{ width: brushSize, height: brushSize }}\n className=\"pointer-events-none absolute left-0 top-0 rounded-full border-2 border-brand opacity-0 shadow-sm\"\n />\n </div>\n </div>\n </div>\n );\n};\n\nexport default ImageMaskPainter;\n"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"objectHelpers.js","sources":["../../src/helpers/objectHelpers.ts"],"sourcesContent":["export const ObjectHelpers = {\n removeProperty: <T extends Record<string, unknown>, K extends keyof T>(obj: T, key: K): Omit<T, K> => {\n const { [key]: _, ...rest } = obj;\n return rest;\n },\n\n removeProperties: <T extends Record<string, unknown>, K extends keyof T>(obj: T, keys: Array<K>): Omit<T, K> => {\n const result = { ...obj };\n keys.forEach((key) => {\n
|
|
1
|
+
{"version":3,"file":"objectHelpers.js","sources":["../../src/helpers/objectHelpers.ts"],"sourcesContent":["export const ObjectHelpers = {\n removeProperty: <T extends Record<string, unknown>, K extends keyof T>(obj: T, key: K): Omit<T, K> => {\n const { [key]: _, ...rest } = obj;\n return rest;\n },\n\n removeProperties: <T extends Record<string, unknown>, K extends keyof T>(obj: T, keys: Array<K>): Omit<T, K> => {\n const result = { ...obj };\n keys.forEach((key) => {\n delete result[key];\n });\n return result;\n },\n\n removePropertyMutating: <T extends Record<string, unknown>, K extends keyof T>(obj: T, key: K): void => {\n if (key in obj) {\n delete obj[key];\n }\n },\n\n removePropertiesMutating: <T extends Record<string, unknown>, K extends keyof T>(obj: T, keys: Array<K>): void => {\n keys.forEach((key) => {\n if (key in obj) {\n delete obj[key];\n }\n });\n },\n\n cleanObject: <T extends Record<string, unknown>>(\n obj: T,\n predicate: (key: keyof T, value: T[keyof T]) => boolean = (_, value) => value !== undefined,\n ): Partial<T> => {\n return Object.fromEntries(\n Object.entries(obj).filter(([key, value]) => predicate(key, value as T[keyof T])),\n ) as Partial<T>;\n },\n\n omitUndefined: <T extends Record<string, unknown>>(obj: T): Partial<T> => {\n return ObjectHelpers.cleanObject(obj, (_, value) => value !== undefined);\n },\n};\n\nexport const CacheUtils = {\n clearCacheEntry: <T>(cache: Record<string, T>, key: string): void => {\n if (key in cache) {\n delete cache[key];\n }\n },\n\n clearCacheEntries: <T>(cache: Record<string, T>, keys: Array<string>): void => {\n keys.forEach((key) => {\n if (key in cache) {\n delete cache[key];\n }\n });\n },\n\n cleanCache: <T>(cache: Record<string, T>, predicate: (key: string, value: T) => boolean): void => {\n Object.keys(cache).forEach((key) => {\n if (!predicate(key, cache[key])) {\n delete cache[key];\n }\n });\n },\n};\n"],"names":[],"mappings":"AAAO,MAAM,gBAAgB;AAAA,EAC3B,gBAAgB,CAAuD,KAAQ,QAAuB;AACpG,UAAM,EAAE,CAAC,GAAG,GAAG,GAAG,GAAG,SAAS;AAC9B,WAAO;AAAA,EACT;AAAA,EAEA,kBAAkB,CAAuD,KAAQ,SAA+B;AAC9G,UAAM,SAAS,EAAE,GAAG,IAAA;AACpB,SAAK,QAAQ,CAAC,QAAQ;AACpB,aAAO,OAAO,GAAG;AAAA,IACnB,CAAC;AACD,WAAO;AAAA,EACT;AAAA,EAEA,wBAAwB,CAAuD,KAAQ,QAAiB;AACtG,QAAI,OAAO,KAAK;AACd,aAAO,IAAI,GAAG;AAAA,IAChB;AAAA,EACF;AAAA,EAEA,0BAA0B,CAAuD,KAAQ,SAAyB;AAChH,SAAK,QAAQ,CAAC,QAAQ;AACpB,UAAI,OAAO,KAAK;AACd,eAAO,IAAI,GAAG;AAAA,MAChB;AAAA,IACF,CAAC;AAAA,EACH;AAAA,EAEA,aAAa,CACX,KACA,YAA0D,CAAC,GAAG,UAAU,UAAU,WACnE;AACf,WAAO,OAAO;AAAA,MACZ,OAAO,QAAQ,GAAG,EAAE,OAAO,CAAC,CAAC,KAAK,KAAK,MAAM,UAAU,KAAK,KAAmB,CAAC;AAAA,IAAA;AAAA,EAEpF;AAAA,EAEA,eAAe,CAAoC,QAAuB;AACxE,WAAO,cAAc,YAAY,KAAK,CAAC,GAAG,UAAU,UAAU,MAAS;AAAA,EACzE;AACF;AAEO,MAAM,aAAa;AAAA,EACxB,iBAAiB,CAAI,OAA0B,QAAsB;AACnE,QAAI,OAAO,OAAO;AAChB,aAAO,MAAM,GAAG;AAAA,IAClB;AAAA,EACF;AAAA,EAEA,mBAAmB,CAAI,OAA0B,SAA8B;AAC7E,SAAK,QAAQ,CAAC,QAAQ;AACpB,UAAI,OAAO,OAAO;AAChB,eAAO,MAAM,GAAG;AAAA,MAClB;AAAA,IACF,CAAC;AAAA,EACH;AAAA,EAEA,YAAY,CAAI,OAA0B,cAAwD;AAChG,WAAO,KAAK,KAAK,EAAE,QAAQ,CAAC,QAAQ;AAClC,UAAI,CAAC,UAAU,KAAK,MAAM,GAAG,CAAC,GAAG;AAC/B,eAAO,MAAM,GAAG;AAAA,MAClB;AAAA,IACF,CAAC;AAAA,EACH;AACF;"}
|