Skip to content

Commit 2d90adf

Browse files
committed
feat: accept bboxes input and add grid snapping to Create Bounding Boxes
1 parent 2e4c9c6 commit 2d90adf

9 files changed

Lines changed: 412 additions & 168 deletions

File tree

src/components/boundingBoxes/WidgetBoundingBoxes.vue

Lines changed: 71 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -4,37 +4,72 @@
44
data-testid="bounding-boxes"
55
@pointerdown.stop
66
>
7-
<div
8-
ref="canvasContainer"
9-
class="relative w-full shrink-0 overflow-hidden rounded-sm border border-component-node-border bg-node-component-surface"
10-
:style="canvasStyle"
11-
>
12-
<canvas
13-
ref="canvasEl"
14-
tabindex="0"
15-
class="absolute inset-0 size-full rounded-sm outline-none"
16-
:style="{ cursor: canvasCursor }"
17-
@pointerdown="onPointerDown"
18-
@pointermove="onCanvasPointerMove"
19-
@pointerup="onDocPointerUp"
20-
@pointercancel="onDocPointerUp"
21-
@pointerleave="onPointerLeave"
22-
@lostpointercapture="onDocPointerUp"
23-
@dblclick="onDoubleClick"
24-
@keydown="onCanvasKeyDown"
25-
@focus="focused = true"
26-
@blur="focused = false"
27-
/>
28-
<textarea
29-
v-if="inlineEditor"
30-
ref="inlineEditorEl"
31-
v-model="inlineEditor.value"
32-
class="absolute box-border resize-none rounded-sm border-2 bg-black/90 p-1 font-mono text-xs text-white outline-none"
33-
:style="inlineEditor.style"
34-
data-capture-wheel="true"
35-
@keydown.stop="onInlineKeyDown"
36-
@blur="commitInlineEditor"
37-
/>
7+
<div class="flex flex-col">
8+
<div
9+
class="flex h-9 items-center gap-1 rounded-t-sm border border-b-0 border-component-node-border bg-component-node-widget-background px-2"
10+
>
11+
<Button
12+
v-tooltip.bottom="{ value: $t('boundingBoxes.grid'), showDelay: 300 }"
13+
variant="textonly"
14+
size="unset"
15+
:aria-pressed="grid"
16+
:class="
17+
cn(
18+
actionBtnClass,
19+
grid && 'bg-component-node-widget-background-selected'
20+
)
21+
"
22+
@click="grid = !grid"
23+
>
24+
<i class="icon-[lucide--grid-3x3] size-4" />
25+
<span>{{ $t('boundingBoxes.grid') }}</span>
26+
</Button>
27+
<Button
28+
v-tooltip.bottom="{
29+
value: $t('boundingBoxes.clearAll'),
30+
showDelay: 300
31+
}"
32+
variant="textonly"
33+
size="unset"
34+
:class="cn(actionBtnClass, 'ml-auto')"
35+
@click="clearAll"
36+
>
37+
<i class="icon-[lucide--undo-2] size-4" />
38+
<span>{{ $t('boundingBoxes.clearAll') }}</span>
39+
</Button>
40+
</div>
41+
<div
42+
ref="canvasContainer"
43+
class="relative w-full shrink-0 overflow-hidden rounded-b-sm border border-t-0 border-component-node-border bg-base-background"
44+
:style="canvasStyle"
45+
>
46+
<canvas
47+
ref="canvasEl"
48+
tabindex="0"
49+
class="absolute inset-0 size-full rounded-sm outline-none"
50+
:style="{ cursor: canvasCursor }"
51+
@pointerdown="onPointerDown"
52+
@pointermove="onCanvasPointerMove"
53+
@pointerup="onDocPointerUp"
54+
@pointercancel="onDocPointerUp"
55+
@pointerleave="onPointerLeave"
56+
@lostpointercapture="onDocPointerUp"
57+
@dblclick="onDoubleClick"
58+
@keydown="onCanvasKeyDown"
59+
@focus="focused = true"
60+
@blur="focused = false"
61+
/>
62+
<textarea
63+
v-if="inlineEditor"
64+
ref="inlineEditorEl"
65+
v-model="inlineEditor.value"
66+
class="absolute box-border resize-none rounded-sm border-2 bg-black/90 p-1 font-mono text-xs text-white outline-none"
67+
:style="inlineEditor.style"
68+
data-capture-wheel="true"
69+
@keydown.stop="onInlineKeyDown"
70+
@blur="commitInlineEditor"
71+
/>
72+
</div>
3873
</div>
3974

4075
<div
@@ -122,16 +157,6 @@
122157
<div v-else-if="hasRegions" class="text-node-text-muted px-1 text-xs">
123158
{{ $t('boundingBoxes.clickRegionToEdit') }}
124159
</div>
125-
126-
<Button
127-
variant="secondary"
128-
size="md"
129-
class="gap-2 rounded-lg border border-component-node-border bg-component-node-background text-xs text-muted-foreground hover:text-base-foreground"
130-
@click="clearAll"
131-
>
132-
<i class="icon-[lucide--undo-2]" />
133-
{{ $t('boundingBoxes.clearAll') }}
134-
</Button>
135160
</div>
136161
</template>
137162

@@ -147,6 +172,9 @@ import { useBoundingBoxes } from '@/composables/boundingBoxes/useBoundingBoxes'
147172
import type { BoundingBox } from '@/types/boundingBoxes'
148173
import type { NodeId } from '@/types/nodeId'
149174
175+
const actionBtnClass =
176+
'flex shrink-0 items-center gap-1.5 rounded-md border-0 bg-transparent px-2 py-1 text-sm text-base-foreground outline-none transition-colors hover:bg-component-node-widget-background-hovered'
177+
150178
const { nodeId } = defineProps<{ nodeId: NodeId }>()
151179
const modelValue = defineModel<BoundingBox[]>({ default: () => [] })
152180
@@ -172,7 +200,8 @@ const {
172200
commitInlineEditor,
173201
setActiveType,
174202
clearAll,
175-
syncState
203+
syncState,
204+
grid
176205
} = useBoundingBoxes(nodeId, {
177206
canvasEl,
178207
canvasContainer,

src/components/palette/PaletteSwatchRow.test.ts

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -47,15 +47,11 @@ describe('PaletteSwatchRow', () => {
4747
expect(screen.queryByRole('button')).toBeNull()
4848
})
4949

50-
it('writes a picked color back through the hidden color input', async () => {
51-
const { container, emitted } = renderRow(['#ff0000', '#00ff00'])
52-
await fireEvent.click(container.querySelector('[data-index="1"]')!)
53-
const input = container.querySelector(
54-
'input[type="color"]'
55-
) as HTMLInputElement
56-
input.value = '#0000ff'
57-
await fireEvent.input(input)
58-
expect(lastEmit(emitted)).toEqual(['#ff0000', '#0000ff'])
50+
it('opens the color picker when a swatch is clicked', async () => {
51+
const { container } = renderRow(['#ff0000'])
52+
const swatch = container.querySelector('[data-index="0"]')!
53+
await userEvent.click(swatch)
54+
expect(swatch.getAttribute('data-state')).toBe('open')
5955
})
6056

6157
it('starts a drag on pointer down without emitting', async () => {
Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,23 @@
11
<template>
22
<div ref="container" class="flex flex-wrap items-center gap-1">
3-
<div
3+
<ColorPicker
44
v-for="(hex, i) in modelValue"
5-
:key="`${i}-${hex}`"
6-
:data-index="i"
7-
:data-hex="hex"
8-
class="relative size-5 cursor-pointer rounded-sm border border-component-node-border"
9-
:style="{ background: hex }"
10-
:title="t('palette.swatchTitle')"
11-
@click="openPicker(i, $event)"
12-
@contextmenu.prevent.stop="remove(i)"
13-
@pointerdown="onPointerDown(i, $event)"
14-
/>
5+
:key="i"
6+
:model-value="hex"
7+
@update:model-value="(value) => updateAt(i, value)"
8+
>
9+
<template #trigger>
10+
<div
11+
:data-index="i"
12+
:data-hex="hex"
13+
class="relative size-5 cursor-pointer rounded-sm border border-component-node-border"
14+
:style="{ background: hex }"
15+
:title="t('palette.swatchTitle')"
16+
@contextmenu.prevent.stop="remove(i)"
17+
@pointerdown="onPointerDown(i, $event)"
18+
/>
19+
</template>
20+
</ColorPicker>
1521
<button
1622
v-if="modelValue.length < max"
1723
type="button"
@@ -21,28 +27,24 @@
2127
>
2228
+
2329
</button>
24-
<input
25-
ref="picker"
26-
type="color"
27-
class="pointer-events-none absolute size-0 opacity-0"
28-
@input="onPickerInput"
29-
/>
3030
</div>
3131
</template>
3232

3333
<script setup lang="ts">
3434
import { useTemplateRef } from 'vue'
3535
import { useI18n } from 'vue-i18n'
3636
37+
import ColorPicker from '@/components/ui/color-picker/ColorPicker.vue'
3738
import { usePaletteSwatchRow } from '@/composables/palette/usePaletteSwatchRow'
3839
3940
const { max = 5 } = defineProps<{ max?: number }>()
4041
const modelValue = defineModel<string[]>({ required: true })
4142
const { t } = useI18n()
4243
4344
const container = useTemplateRef<HTMLDivElement>('container')
44-
const picker = useTemplateRef<HTMLInputElement>('picker')
4545
46-
const { openPicker, onPickerInput, remove, addColor, onPointerDown } =
47-
usePaletteSwatchRow({ modelValue, container, picker })
46+
const { updateAt, remove, addColor, onPointerDown } = usePaletteSwatchRow({
47+
modelValue,
48+
container
49+
})
4850
</script>

src/components/ui/color-picker/ColorPicker.vue

Lines changed: 43 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -65,49 +65,51 @@ const isOpen = ref(false)
6565
<template>
6666
<PopoverRoot v-model:open="isOpen">
6767
<PopoverTrigger as-child>
68-
<button
69-
type="button"
70-
:disabled="$props.disabled"
71-
:class="
72-
cn(
73-
'flex h-8 w-full items-center overflow-clip rounded-lg border border-transparent bg-component-node-widget-background pr-2 outline-none hover:bg-component-node-widget-background-hovered disabled:cursor-not-allowed disabled:opacity-50',
74-
isOpen && 'border-node-stroke',
75-
$props.class
76-
)
77-
"
78-
>
79-
<div class="flex size-8 shrink-0 items-center justify-center">
80-
<div class="relative size-4 overflow-hidden rounded-sm">
81-
<div
82-
class="absolute inset-0"
83-
:style="{
84-
backgroundImage:
85-
'repeating-conic-gradient(#808080 0% 25%, transparent 0% 50%)',
86-
backgroundSize: '4px 4px'
87-
}"
88-
/>
89-
<div
90-
class="absolute inset-0"
91-
:style="{ backgroundColor: previewColor }"
92-
/>
93-
</div>
94-
</div>
95-
<div
96-
class="flex flex-1 items-center justify-between pl-1 text-xs text-component-node-foreground"
68+
<slot name="trigger" :open="isOpen" :preview-color="previewColor">
69+
<button
70+
type="button"
71+
:disabled="$props.disabled"
72+
:class="
73+
cn(
74+
'flex h-8 w-full items-center overflow-clip rounded-lg border border-transparent bg-component-node-widget-background pr-2 outline-none hover:bg-component-node-widget-background-hovered disabled:cursor-not-allowed disabled:opacity-50',
75+
isOpen && 'border-node-stroke',
76+
$props.class
77+
)
78+
"
9779
>
98-
<template v-if="displayMode === 'hex'">
99-
<span>{{ displayHex }}</span>
100-
</template>
101-
<template v-else>
102-
<div class="flex gap-2">
103-
<span>{{ baseRgb.r }}</span>
104-
<span>{{ baseRgb.g }}</span>
105-
<span>{{ baseRgb.b }}</span>
80+
<div class="flex size-8 shrink-0 items-center justify-center">
81+
<div class="relative size-4 overflow-hidden rounded-sm">
82+
<div
83+
class="absolute inset-0"
84+
:style="{
85+
backgroundImage:
86+
'repeating-conic-gradient(#808080 0% 25%, transparent 0% 50%)',
87+
backgroundSize: '4px 4px'
88+
}"
89+
/>
90+
<div
91+
class="absolute inset-0"
92+
:style="{ backgroundColor: previewColor }"
93+
/>
10694
</div>
107-
</template>
108-
<span>{{ hsva.a }}%</span>
109-
</div>
110-
</button>
95+
</div>
96+
<div
97+
class="flex flex-1 items-center justify-between pl-1 text-xs text-component-node-foreground"
98+
>
99+
<template v-if="displayMode === 'hex'">
100+
<span>{{ displayHex }}</span>
101+
</template>
102+
<template v-else>
103+
<div class="flex gap-2">
104+
<span>{{ baseRgb.r }}</span>
105+
<span>{{ baseRgb.g }}</span>
106+
<span>{{ baseRgb.b }}</span>
107+
</div>
108+
</template>
109+
<span>{{ hsva.a }}%</span>
110+
</div>
111+
</button>
112+
</slot>
111113
</PopoverTrigger>
112114
<PopoverPortal>
113115
<PopoverContent

0 commit comments

Comments
 (0)