Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
147 changes: 147 additions & 0 deletions src/components/imagecrop/WidgetImageCrop.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
<template>
<div
class="widget-expands relative flex h-full w-full flex-col gap-1"
@pointerdown.stop
@pointermove.stop
@pointerup.stop
>
<!-- Number inputs row -->
<div class="flex shrink-0 gap-1 px-1">
<div class="flex flex-1 items-center gap-1">
<label class="w-6 text-xs text-muted">X</label>
<input
v-model.number="cropX"
type="number"
:min="0"
class="h-6 w-full rounded border border-border bg-input px-1 text-xs"
@change="handleInputChange"
/>
</div>
<div class="flex flex-1 items-center gap-1">
<label class="w-6 text-xs text-muted">Y</label>
<input
v-model.number="cropY"
type="number"
:min="0"
class="h-6 w-full rounded border border-border bg-input px-1 text-xs"
@change="handleInputChange"
/>
</div>
<div class="flex flex-1 items-center gap-1">
<label class="w-6 text-xs text-muted">W</label>
<input
v-model.number="cropWidth"
type="number"
:min="1"
class="h-6 w-full rounded border border-border bg-input px-1 text-xs"
@change="handleInputChange"
/>
</div>
<div class="flex flex-1 items-center gap-1">
<label class="w-6 text-xs text-muted">H</label>
<input
v-model.number="cropHeight"
type="number"
:min="1"
class="h-6 w-full rounded border border-border bg-input px-1 text-xs"
@change="handleInputChange"
/>
Comment on lines +11 to +48
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Improve accessibility by associating labels with inputs.

The <label> elements are not programmatically associated with their corresponding inputs. Use id and for attributes or wrap inputs in labels for proper accessibility.

🔎 Proposed fix for one input (apply pattern to all)
       <div class="flex flex-1 items-center gap-1">
-        <label class="w-6 text-xs text-muted">X</label>
+        <label for="crop-x" class="w-6 text-xs text-muted">X</label>
         <input
+          id="crop-x"
           v-model.number="cropX"
           type="number"
           :min="0"
           class="h-6 w-full rounded border border-border bg-input px-1 text-xs"
           @change="handleInputChange"
         />
       </div>

Apply the same pattern for Y (id="crop-y"), W (id="crop-width"), and H (id="crop-height").

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<label class="w-6 text-xs text-muted">X</label>
<input
v-model.number="cropX"
type="number"
:min="0"
class="h-6 w-full rounded border border-border bg-input px-1 text-xs"
@change="handleInputChange"
/>
</div>
<div class="flex flex-1 items-center gap-1">
<label class="w-6 text-xs text-muted">Y</label>
<input
v-model.number="cropY"
type="number"
:min="0"
class="h-6 w-full rounded border border-border bg-input px-1 text-xs"
@change="handleInputChange"
/>
</div>
<div class="flex flex-1 items-center gap-1">
<label class="w-6 text-xs text-muted">W</label>
<input
v-model.number="cropWidth"
type="number"
:min="1"
class="h-6 w-full rounded border border-border bg-input px-1 text-xs"
@change="handleInputChange"
/>
</div>
<div class="flex flex-1 items-center gap-1">
<label class="w-6 text-xs text-muted">H</label>
<input
v-model.number="cropHeight"
type="number"
:min="1"
class="h-6 w-full rounded border border-border bg-input px-1 text-xs"
@change="handleInputChange"
/>
<label for="crop-x" class="w-6 text-xs text-muted">X</label>
<input
id="crop-x"
v-model.number="cropX"
type="number"
:min="0"
class="h-6 w-full rounded border border-border bg-input px-1 text-xs"
@change="handleInputChange"
/>
</div>
<div class="flex flex-1 items-center gap-1">
<label for="crop-y" class="w-6 text-xs text-muted">Y</label>
<input
id="crop-y"
v-model.number="cropY"
type="number"
:min="0"
class="h-6 w-full rounded border border-border bg-input px-1 text-xs"
@change="handleInputChange"
/>
</div>
<div class="flex flex-1 items-center gap-1">
<label for="crop-width" class="w-6 text-xs text-muted">W</label>
<input
id="crop-width"
v-model.number="cropWidth"
type="number"
:min="1"
class="h-6 w-full rounded border border-border bg-input px-1 text-xs"
@change="handleInputChange"
/>
</div>
<div class="flex flex-1 items-center gap-1">
<label for="crop-height" class="w-6 text-xs text-muted">H</label>
<input
id="crop-height"
v-model.number="cropHeight"
type="number"
:min="1"
class="h-6 w-full rounded border border-border bg-input px-1 text-xs"
@change="handleInputChange"
/>

</div>
</div>

<!-- Image preview container -->
<div
ref="containerEl"
class="relative min-h-0 flex-1 overflow-hidden rounded-[5px] bg-node-component-surface"
>
<div v-if="isLoading" class="flex size-full items-center justify-center">
<span class="text-sm">{{ $t('imageCrop.loading') }}</span>
</div>

<div
v-else-if="!imageUrl"
class="flex size-full flex-col items-center justify-center text-center"
>
<i class="mb-2 icon-[lucide--image] h-12 w-12" />
<p class="text-sm">{{ $t('imageCrop.noInputImage') }}</p>
</div>

<img
v-else
ref="imageEl"
:src="imageUrl"
:alt="$t('imageCrop.cropPreviewAlt')"
draggable="false"
class="block size-full object-contain select-none brightness-50"
@load="handleImageLoad"
@error="handleImageError"
@dragstart.prevent
/>

<div
v-if="imageUrl && !isLoading"
class="absolute box-border cursor-move overflow-hidden border-2 border-white"
:style="cropBoxStyle"
@pointerdown="handleDragStart"
@pointermove="handleDragMove"
@pointerup="handleDragEnd"
>
<div class="pointer-events-none size-full" :style="cropImageStyle" />
</div>

<div
v-for="handle in resizeHandles"
v-show="imageUrl && !isLoading"
:key="handle.direction"
:class="['absolute', handle.class]"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Use cn() utility instead of array class syntax.

Per coding guidelines, use the cn() utility function from @/utils/tailwindUtil for merging Tailwind class names instead of :class="[]" syntax.

🔎 Proposed refactor

Import the utility at the top of the script:

+import { cn } from '@/utils/tailwindUtil'

Then update the binding:

-        :class="['absolute', handle.class]"
+        :class="cn('absolute', handle.class)"

Based on coding guidelines.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
:class="['absolute', handle.class]"
import { cn } from '@/utils/tailwindUtil'
:class="cn('absolute', handle.class)"
🤖 Prompt for AI Agents
In @src/components/imagecrop/WidgetImageCrop.vue around line 96, The Vue
template uses array class binding :class="['absolute', handle.class]" in
WidgetImageCrop.vue; replace this with the cn() utility from
@/utils/tailwindUtil: import cn in the script (or setup) and change the binding
to use :class="cn('absolute', handle.class)" so Tailwind class merging follows
project conventions; ensure the import name is cn and update any other similar
bindings in this component to use cn() for consistency.

:style="handle.style"
@pointerdown="(e) => handleResizeStart(e, handle.direction)"
@pointermove="handleResizeMove"
@pointerup="handleResizeEnd"
/>
</div>
</div>
</template>

<script setup lang="ts">
import { useTemplateRef } from 'vue'

import { useImageCrop } from '@/composables/useImageCrop'
import type { CropRegionValue } from '@/lib/litegraph/src/types/widgets'
import type { NodeId } from '@/platform/workflow/validation/schemas/workflowSchema'

const props = defineProps<{
nodeId: NodeId
}>()
Comment on lines +113 to +115
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Use Vue 3.5 style props destructuring.

Per coding guidelines, prefer reactive props destructuring in Vue 3.5+.

🔎 Proposed refactor
-const props = defineProps<{
+const { nodeId } = defineProps<{
   nodeId: NodeId
 }>()

 ...

-} = useImageCrop(props.nodeId, { imageEl, containerEl, modelValue })
+} = useImageCrop(nodeId, { imageEl, containerEl, modelValue })

Based on learnings and coding guidelines.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const props = defineProps<{
nodeId: NodeId
}>()
const { nodeId } = defineProps<{
nodeId: NodeId
}>()
🤖 Prompt for AI Agents
In @src/components/imagecrop/WidgetImageCrop.vue around lines 113-115, The
current use of defineProps should be converted to Vue 3.5 reactive props
destructuring: keep the typed defineProps call (defineProps<{ nodeId: NodeId
}>()), then call toRefs on the returned props and extract nodeId (e.g., const {
nodeId } = toRefs(props)), and update usages to read nodeId.value where needed;
change references to the original props object to use the destructured ref
instead.


const modelValue = defineModel<CropRegionValue>({
default: () => ({ x: 0, y: 0, width: 512, height: 512 })
})

const imageEl = useTemplateRef<HTMLImageElement>('imageEl')
const containerEl = useTemplateRef<HTMLDivElement>('containerEl')

const {
imageUrl,
isLoading,

cropX,
cropY,
cropWidth,
cropHeight,

cropBoxStyle,
cropImageStyle,
resizeHandles,

handleImageLoad,
handleImageError,
handleInputChange,
handleDragStart,
handleDragMove,
handleDragEnd,
handleResizeStart,
handleResizeMove,
handleResizeEnd
} = useImageCrop(props.nodeId, { imageEl, containerEl, modelValue })
</script>
Loading