Skip to content
Merged
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
3 changes: 2 additions & 1 deletion app/.prettierrc
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "all",
"printWidth": 80
"printWidth": 80,
"plugins": ["prettier-plugin-svelte"]
}
14 changes: 10 additions & 4 deletions app/src/app.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -816,16 +816,20 @@

{#if AI_ASSISTANT_ENABLED}
<!-- AI assistant (sidebar toggle) -->
<Tooltip content={showAiChat ? 'Close AI assistant' : 'Open AI assistant'} side="bottom" delay={TOOLTIP_DELAY}>
<Tooltip
content={showAiChat ? 'Close AI assistant' : 'Open AI assistant'}
side="bottom"
delay={TOOLTIP_DELAY}
>
<button
onclick={toggleAiChat}
class="hdr-btn hdr-btn-icon shrink-0 cursor-pointer transition-colors
{showAiChat
? 'bg-primary/20 text-primary hover:bg-primary/25'
: 'bg-primary/10 text-primary hover:bg-primary/20 hover:text-primary'}"
aria-label={showAiChat ? 'Close AI assistant' : 'Open AI assistant'}
aria-pressed={showAiChat}
><Sparkles size={12} /></button>
aria-pressed={showAiChat}><Sparkles size={12} /></button
>
</Tooltip>
{/if}

Expand Down Expand Up @@ -1128,7 +1132,9 @@
class="h-[34px] gap-1.5 min-w-[104px] rounded-lg px-4 font-semibold text-white
shadow-[0_4px_14px_-4px_rgba(220,20,60,0.6)] hover:bg-[var(--accent-hover)]
disabled:bg-primary/25 disabled:text-white/60 disabled:opacity-100 disabled:shadow-none
{onboardingStep === 0 && !hasRenderedOnce ? 'onboarding-glow' : ''}"
{onboardingStep === 0 && !hasRenderedOnce
? 'onboarding-glow'
: ''}"
size="sm"
>
<Play size={13} fill="currentColor" strokeWidth={0} />
Expand Down
103 changes: 71 additions & 32 deletions app/src/components/canvas/ElementHandle.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -8,31 +8,36 @@
// Circular-arrow cursor for the rotation handle (clockwise arc + arrowhead).
const ROTATE_CURSOR = `url("data:image/svg+xml,${encodeURIComponent(
'<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20">' +
'<path d="M10 2A8 8 0 1 1 2 10" stroke="white" stroke-width="3" fill="none" stroke-linecap="round"/>' +
'<path d="M10 2A8 8 0 1 1 2 10" stroke="#1a1a1a" stroke-width="1.5" fill="none" stroke-linecap="round"/>' +
'<path d="M13 2L10 0L10 4Z" fill="white"/>' +
'<path d="M12.5 2L10 0.5L10 3.5Z" fill="#1a1a1a"/>' +
'</svg>'
'<path d="M10 2A8 8 0 1 1 2 10" stroke="white" stroke-width="3" fill="none" stroke-linecap="round"/>' +
'<path d="M10 2A8 8 0 1 1 2 10" stroke="#1a1a1a" stroke-width="1.5" fill="none" stroke-linecap="round"/>' +
'<path d="M13 2L10 0L10 4Z" fill="white"/>' +
'<path d="M12.5 2L10 0.5L10 3.5Z" fill="#1a1a1a"/>' +
'</svg>',
)}") 10 10, grab`

const RESIZE_CURSORS = { tl: 'nw-resize', tr: 'ne-resize', bl: 'sw-resize', br: 'se-resize' }
const RESIZE_CURSORS = {
tl: 'nw-resize',
tr: 'ne-resize',
bl: 'sw-resize',
br: 'se-resize',
}
const CORNERS = ['tl', 'tr', 'bl', 'br']

let {
bounds = { x: 0, y: 0, w: 50, h: 30 },
label = '',
selected = false,
rotation = 0,
groupOffset = { dx: 0, dy: 0 }, // live offset when another group member is dragging
groupOffset = { dx: 0, dy: 0 }, // live offset when another group member is dragging
resizable = false,
locked = false,
onselect, // (event) — event carries shiftKey for multi-select
ondrag, // (dx, dy) live, every pointermove
ondragend, // (dx, dy) in scene/overlay coords
onrotate, // (degrees) live, every pointermove
onrotateend, // (degrees) committed on pointerup
onresize, // (corner, dx, dy, shiftKey) live, every pointermove
onresizeend, // (corner, dx, dy, shiftKey) committed on pointerup
onselect, // (event) — event carries shiftKey for multi-select
ondrag, // (dx, dy) live, every pointermove
ondragend, // (dx, dy) in scene/overlay coords
onrotate, // (degrees) live, every pointermove
onrotateend, // (degrees) committed on pointerup
onresize, // (corner, dx, dy, shiftKey) live, every pointermove
onresizeend, // (corner, dx, dy, shiftKey) committed on pointerup
} = $props()

let dragging = $state(false)
Expand All @@ -46,7 +51,10 @@
// bounding box flash to its synthetic fallback for one frame cycle.
const CLICK_PX = 3
function pastClickThreshold(e, origin) {
return Math.abs(e.clientX - origin.mx) > CLICK_PX || Math.abs(e.clientY - origin.my) > CLICK_PX
return (
Math.abs(e.clientX - origin.mx) > CLICK_PX ||
Math.abs(e.clientY - origin.my) > CLICK_PX
)
}

// Display position: base bounds + live drag offset
Expand Down Expand Up @@ -77,7 +85,7 @@
function onpointerdown(e) {
e.stopPropagation()
onselect?.(e)
if (locked) return // select is fine; drag is not
if (locked) return // select is fine; drag is not
dragging = true
dragOrigin = { mx: e.clientX, my: e.clientY }
dragDelta = { dx: 0, dy: 0 }
Expand All @@ -88,14 +96,21 @@
if (!dragging) return
const svg = e.currentTarget.ownerSVGElement
if (!svg) return
dragDelta = screenToOverlayDelta(svg, dragOrigin.mx, dragOrigin.my, e.clientX, e.clientY)
dragDelta = screenToOverlayDelta(
svg,
dragOrigin.mx,
dragOrigin.my,
e.clientX,
e.clientY,
)
ondrag?.(dragDelta.dx, dragDelta.dy)
}

function onpointerup(e) {
if (!dragging) return
dragging = false
if (pastClickThreshold(e, dragOrigin)) ondragend?.(dragDelta.dx, dragDelta.dy)
if (pastClickThreshold(e, dragOrigin))
ondragend?.(dragDelta.dx, dragDelta.dy)
dragDelta = { dx: 0, dy: 0 }
}

Expand All @@ -118,7 +133,11 @@
e.stopPropagation()
rotating = true
rotateStartValue = rotation
rotateStartAngle = sceneAngle(e.currentTarget.ownerSVGElement, e.clientX, e.clientY)
rotateStartAngle = sceneAngle(
e.currentTarget.ownerSVGElement,
e.clientX,
e.clientY,
)
rotateOrigin = { mx: e.clientX, my: e.clientY }
e.currentTarget.setPointerCapture(e.pointerId)
}
Expand Down Expand Up @@ -154,7 +173,13 @@
if (!resizingCorner) return
const svg = e.currentTarget.ownerSVGElement
if (!svg) return
resizeDelta = screenToOverlayDelta(svg, resizeOrigin.mx, resizeOrigin.my, e.clientX, e.clientY)
resizeDelta = screenToOverlayDelta(
svg,
resizeOrigin.mx,
resizeOrigin.my,
e.clientX,
e.clientY,
)
onresize?.(resizingCorner, resizeDelta.dx, resizeDelta.dy, e.shiftKey)
}

Expand All @@ -168,10 +193,14 @@

function cornerPos(corner) {
switch (corner) {
case 'tl': return { x: d.x, y: d.y }
case 'tr': return { x: d.x + d.w, y: d.y }
case 'bl': return { x: d.x, y: d.y + d.h }
case 'br': return { x: d.x + d.w, y: d.y + d.h }
case 'tl':
return { x: d.x, y: d.y }
case 'tr':
return { x: d.x + d.w, y: d.y }
case 'bl':
return { x: d.x, y: d.y + d.h }
case 'br':
return { x: d.x + d.w, y: d.y + d.h }
}
}
</script>
Expand All @@ -184,7 +213,11 @@
width={Math.max(d.w + 8, 24)}
height={Math.max(d.h + 8, 24)}
fill="transparent"
style="cursor: {locked ? 'not-allowed' : dragging ? 'grabbing' : 'grab'}; pointer-events: all; outline: none"
style="cursor: {locked
? 'not-allowed'
: dragging
? 'grabbing'
: 'grab'}; pointer-events: all; outline: none"
role="button"
aria-label="{locked ? 'Locked' : 'Move'} {label}"
tabindex="0"
Expand All @@ -203,7 +236,11 @@
stroke-width={selected ? 1.5 : 1}
stroke-dasharray={selected ? 'none' : '4 3'}
rx="2"
style="pointer-events: none; stroke: {selected ? (locked ? '#F59E0B' : 'var(--primary)') : 'rgba(255,255,255,0.25)'}"
style="pointer-events: none; stroke: {selected
? locked
? '#F59E0B'
: 'var(--primary)'
: 'rgba(255,255,255,0.25)'}"
/>

<!-- Label tag (only when selected) -->
Expand All @@ -222,8 +259,8 @@
font-size="10"
fill="white"
font-family="system-ui"
style="pointer-events: none; user-select: none"
>{label}</text>
style="pointer-events: none; user-select: none">{label}</text
>

<!-- Corner handles -->
{#each CORNERS as corner (corner)}
Expand Down Expand Up @@ -258,8 +295,10 @@

<!-- Rotation stem -->
<line
x1={cx} y1={d.y}
x2={cx} y2={d.y - ROTATE_HANDLE_OFFSET}
x1={cx}
y1={d.y}
x2={cx}
y2={d.y - ROTATE_HANDLE_OFFSET}
stroke-width="1"
style="pointer-events: none; stroke: var(--primary)"
/>
Expand All @@ -269,7 +308,7 @@
role="button"
aria-label="Rotate element"
tabindex="0"
cx={cx}
{cx}
cy={d.y - ROTATE_HANDLE_OFFSET}
r="14"
fill="transparent"
Expand All @@ -279,7 +318,7 @@
onpointerup={rotatePointerUp}
/>
<circle
cx={cx}
{cx}
cy={d.y - ROTATE_HANDLE_OFFSET}
r="6"
stroke="white"
Expand Down
Loading
Loading