Skip to content

feat: preview state styles during canvas drags - #684

Open
stravo1 wants to merge 25 commits into
frappe:developfrom
stravo1:variant-preview
Open

feat: preview state styles during canvas drags#684
stravo1 wants to merge 25 commits into
frappe:developfrom
stravo1:variant-preview

Conversation

@stravo1

@stravo1 stravo1 commented Jul 20, 2026

Copy link
Copy Markdown
Member

When a state variant is active, canvas drag handles now read and write the active style key instead of falling back to the base style. This keeps rotation, resize, spacing, movement and border radius previews aligned with the selected state.

Screen.Recording.2026-07-20.at.4.50.17.PM.mov

When a state variant is active, canvas drag handles now read and write the active style key instead of falling back to the base style. This keeps rotation, resize, spacing, movement and border radius previews aligned with the selected state.
@stravo1
stravo1 marked this pull request as draft July 20, 2026 11:22
@stravo1 stravo1 changed the title fix: preview state styles during canvas drags feat: preview state styles during canvas drags Jul 20, 2026
@greptile-apps

greptile-apps Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Confidence Score: 5/5

  • Safe to merge. The changes are well-scoped canvas interaction improvements with correct fallback logic throughout.
  • All drag handlers correctly delegate to the new getActiveStyleValue/setActiveStyle methods, the unit-fallback logic in getNumberInUnit is sound, and the useStatePopover composable handles edge cases like prevented mousdowns and focus reclaim. The only non-trivial concern is the document.querySelector panel fallback in VariantControl, which is a minor UI glitch risk rather than a correctness or data-integrity issue.
  • frontend/src/components/Controls/VariantControl.vue — the panel-selector fallback in handleFocusOut warrants a second look.

Reviews (13): Last reviewed commit: "Revert "feat: add extension SDK bundle"" | Re-trigger Greptile

Comment thread frontend/src/components/BlockEditor.vue Outdated
Comment thread frontend/src/components/BlockEditor.vue Outdated
# Conflicts:
#	frontend/src/components/BlockEditor.vue
#	frontend/src/components/BorderRadiusHandler.vue
#	frontend/src/components/BuilderBlock.vue
#	frontend/src/composables/useSpacingHandler.ts
@stravo1
stravo1 marked this pull request as ready for review August 12, 2026 11:43
@codecov

codecov Bot commented Aug 12, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 66.22%. Comparing base (70cb6fd) to head (e249d22).
⚠️ Report is 521 commits behind head on develop.

Additional details and impacted files
@@             Coverage Diff             @@
##           develop     #684      +/-   ##
===========================================
+ Coverage    66.05%   66.22%   +0.16%     
===========================================
  Files           37       90      +53     
  Lines         4982    10015    +5033     
===========================================
+ Hits          3291     6632    +3341     
- Misses        1691     3383    +1692     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

stravo1 and others added 2 commits August 12, 2026 19:42
Canvas drag handlers paired a "has a value" guard with getNumberFromPx.
getNumberFromPx strips only "px", so a percentage or em value silently
became 0 and snapped the block to the origin on the first drag frame.

Add getNumberInUnit next to removeDefaultUnit and normalizeValueWithUnits
in cssUtils. It applies the same default-unit rule the property controls
use: a unitless number takes the given unit, another unit or a value that
is not a plain number gives back null. It uses the same anchored regex,
so negatives, uppercase units, `auto` and `calc(...)` all read correctly.

The three handlers now fall back to the rendered value when the style
cannot be read as pixels. This also removes the hasStyleValue copies from
BoxResizer and useSpacingHandler.

Co-Authored-By: Claude <noreply@anthropic.com>
Picking a state (hover, active, focus) makes the canvas preview it, but
nothing said which control drove that preview, and the caret stayed where
it was. Move focus into the active state's field, so its focus styling
marks the row and the value is ready to type. Leaving the field ends the
preview, so the canvas and the panel read normally again.

A state picked for a property with no value yet had nothing to show, and
the row only rendered for a truthy value, so it never appeared. Track the
states the user adds and keep their rows until a value arrives or the user
removes the row. Tying the row to focus does not work here, because the
field commits its value 100ms after the change event, well after the next
click would have dropped the row.

The label dropdown returns focus to its trigger as it closes, and that
trigger is a span, so focus fell to <body> a few frames after the row
appeared. Take focus back while nothing else holds it.

The clear button no longer takes focus or marks its state active, so
removing a state does not flash the row first.

Co-Authored-By: Claude <noreply@anthropic.com>
stravo1 and others added 3 commits August 12, 2026 22:40
The focus-claim guard suppressed every focusout while it ran, including
the user clicking another control. The row then kept activeState set with
no later event to clear it, so canvas drags wrote to the state style
instead of the base style.

Focus that goes nowhere is the dropdown closing. Focus that goes to
another element is the user leaving, so end the claim and the preview.

Reported by Greptile on frappe#684.

Co-Authored-By: Claude <noreply@anthropic.com>
The resize edge handles and the radius handle did not prevent the default
mousedown, so the browser moved focus off the property field. That ended
the state preview mid-drag. The rotation handles and the resize corners
already prevent it.

Co-Authored-By: Claude <noreply@anthropic.com>
@surajshetty3416

Copy link
Copy Markdown
Member

@mergify update

@mergify

mergify Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

update

✅ Branch has been successfully updated

stravo1 and others added 13 commits August 13, 2026 14:49
An open options list stopped the input from reading a new value. A drag on
the label slider or a canvas resize handle changed the block but left the
old text in the field.

Watch the value and the options apart. Only the options wait for the list
to close, because an open list can hold text that the user typed.

Co-Authored-By: Claude <noreply@anthropic.com>
A mousedown on a state label moved focus off the field. The row read that
as a departure and ended the preview, so the drag cleared the state it set.

Co-Authored-By: Claude <noreply@anthropic.com>
Resize and rotation read the base style at drag start and restored it on
escape, but the drag writes to the active state key. Escape left the state
style at the dragged value.

Read and restore through getActiveStyleProperty, as BorderRadiusHandler does.

Co-Authored-By: Claude <noreply@anthropic.com>
The key was resolved twice, so a state change during the drag could send the
escape restore to a different key than the one the drag wrote to.

Co-Authored-By: Claude <noreply@anthropic.com>
A state row keeps its field focused during a drag, and the row stops keydown
so builder shortcuts do not fire while typing. Escape died there and the drag
never cancelled.

Listen in the capture phase, which runs before the target sees the key.

Co-Authored-By: Claude <noreply@anthropic.com>
The arrows stopped their mousedown, so the row never heard it and the state
stayed inactive. The canvas kept previewing nothing while the state value
changed.

Let the mousedown reach the row. It sets the state active, and the row then
puts focus in its field.

Co-Authored-By: Claude <noreply@anthropic.com>
Controls such as Background and Color open a popover from the row. The panel
is portalled out of the row and takes the focus with it, so the row read this
as a departure and ended the preview. The value went to the state, but the
canvas showed the base style.

Treat an open popover as the row still being in use.

Co-Authored-By: Claude <noreply@anthropic.com>
A closing popover hands focus back to the row, so a click on the canvas could
leave the state previewed. Watch for a pointer press outside the row and its
popover, which says the user is done whatever the focus does next.

Co-Authored-By: Claude <noreply@anthropic.com>
A closing popover hands focus back to the row, so focus alone could leave a
state previewed after the user went back to the canvas.

Clear the state on the canvas mousedown. The drag handles stop that event, so
a drag that edits the state still keeps it.

Co-Authored-By: Claude <noreply@anthropic.com>
The row skipped the blur while a popover was open, but focusout fires once. After
the panel closed, nothing asked again, so the preview never ended.

Watch the panel instead. When it leaves the DOM, the focus and the press that
closed it decide. Reka gives the focus back to the row when the panel closes on
its own. A press inside the row, or one that prevents its default the way the
drag handles do, goes on editing the state, so the row takes the focus back.
Anything else ends the preview.

Drop the canvas mousedown clear. It could not hold, because the focus returning
to the row set the state again.

Co-Authored-By: Claude <noreply@anthropic.com>
Cut the restatements of what the code already shows. Keep only the reason
each block exists: the dropdown returning focus to a span, the panel living
outside the row, and what a press means for the preview.

Co-Authored-By: Claude <noreply@anthropic.com>
The top and left label placements used two root elements, and each one
held the same control block. One root now serves both placements. The
root class changes with the placement, and v-if keeps the parts that
differ.

This drops the second copy of the control component and its slot loop.
The rendered output stays the same. The class shrink-1 did nothing,
because the parent of the control is not a flex container. The class
h-3 w-3 becomes size-3, which the other clear button already uses.

The helper panelFor had one caller. Its body now sits in handleFocusOut.

Co-Authored-By: Claude <noreply@anthropic.com>
stravo1 and others added 4 commits August 16, 2026 17:19
Extract the swatch popover behavior into useStatePopover. The composable
owns which state a popover edits, whether a click closes it, and when the
canvas preview ends. ColorInput, BackgroundHandler and ShadowHandler now
share it.

A row lost its canvas preview when the user reopened a picker from the
swatch while the field still held focus. Because the field was focused,
the isActive watch skipped keepFocus and left claimingFocus false. The
opening panel then dropped focus with no relatedTarget, and the row read
that as the user leaving. A press inside the row now keeps the preview.

An editor also cleared a state that another control had just claimed.
endStatePreview now clears only the state it previewed.

Co-Authored-By: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants