fix(app): align variable-enabled panel fields

- Keep Apply variable actions visible with restrained hover treatment
- Preserve the shared 26px NumberField height in column layouts
- Anchor variable pickers to their trigger instead of the zero-size contents wrapper
- Cover control dimensions, action visibility, and popover placement end to end
This commit is contained in:
Danila Poyarkov 2026-07-18 04:35:52 +03:00
parent 86d6a63d0f
commit fc5cc2d4b8
5 changed files with 43 additions and 9 deletions

View file

@ -18,6 +18,7 @@
- Add saved per-node export settings for repeat exports.
- Add Design panel controls for layer blend modes and alpha, vector, and luminance masks.
- Refine Design panel foundations with 26px controls, consistently aligned action rails, shared Tailwind themes, and Storybook component states.
- Fix variable binding action visibility, picker anchoring, hover treatment, and Design panel number-field heights.
- Scale the Layers panel to 5,000-node documents with virtualized rows, indexed updates, scroll-to-selection, range selection, and focus-aware themed states.
- Add Figma-style horizontal and vertical constraint controls with pin interactions, mixed-selection editing, undo, and responsive frame resizing.
- Add mixed-selection stroke cap, join, and miter-limit controls with CanvasKit rendering and `.fig` roundtrip support.

View file

@ -99,7 +99,7 @@ defineOptions({ inheritAttrs: false })
<template>
<BindableValuePicker v-slot="picker">
<ComboboxAnchor class="contents" data-slot="anchor">
<ComboboxAnchor class="inline-flex shrink-0 items-center" data-slot="anchor">
<Tip :label="triggerLabel">
<ComboboxTrigger as-child>
<BindingTrigger

View file

@ -1,14 +1,10 @@
import { panelIconButtonBase } from './panel/field'
const bindingFieldTheme = {
slots: {
root: 'group/binding min-w-0',
root: 'min-w-0',
pill: 'flex min-w-0 flex-1 items-center overflow-hidden rounded-sm px-1 text-component outline-none',
pillLabel: 'min-w-0 flex-1 truncate text-[11px] font-medium',
trigger: [
panelIconButtonBase,
'size-5 rounded-sm transition-opacity data-[state=unbound]:opacity-0 data-[state=mixed]:opacity-0 group-hover/binding:opacity-100 group-focus-within/binding:opacity-100 data-[open]:bg-hover data-[open]:text-component disabled:opacity-0 data-[disabled]:opacity-0'
],
trigger:
'flex size-5 shrink-0 cursor-pointer items-center justify-center rounded-sm border border-transparent bg-transparent text-muted outline-none transition-colors hover:text-surface focus-visible:border-panel-focus data-[open]:bg-hover data-[open]:text-component disabled:opacity-0 data-[disabled]:opacity-0',
pickerContent:
'z-[100] w-56 overflow-hidden rounded-lg border border-border bg-panel text-surface shadow-xl',
pickerSearch:

View file

@ -4,7 +4,7 @@ const numberFieldTheme = {
slots: {
root: [
panelFieldBase,
'group flex flex-1 cursor-ew-resize items-center tabular-nums data-[disabled]:cursor-auto data-[editing]:cursor-auto'
'group flex flex-auto cursor-ew-resize items-center tabular-nums data-[disabled]:cursor-auto data-[editing]:cursor-auto'
],
leading:
'flex shrink-0 items-center justify-center self-stretch px-[5px] text-muted select-none [&>*]:pointer-events-none',

View file

@ -20,6 +20,43 @@ test('property sections collapse and reopen from their title', async () => {
await expect(blendMode).toBeVisible()
})
test('appearance fields share control height and show variable actions', async () => {
await editor.canvas.clearCanvas()
await editor.canvas.drawRect(200, 200, 80, 80)
const section = propertySection(editor.page, 'Appearance')
const controls = [
section.getByRole('combobox', { name: 'Blend mode' }),
section.getByRole('spinbutton', { name: 'Opacity' }),
section.getByRole('spinbutton', { name: 'Radius' }),
section.getByRole('spinbutton', { name: 'Corner smoothing' })
]
for (const control of controls) {
await expect(control).toHaveCSS('height', '26px')
}
const applyVariable = section.getByRole('button', { name: 'Apply variable' }).first()
await expect(applyVariable).toBeVisible()
await expect(applyVariable).toHaveCSS('opacity', '1')
await expect(applyVariable).toHaveCSS('width', '20px')
await expect(applyVariable).toHaveCSS('height', '20px')
await applyVariable.hover()
await expect(applyVariable).toHaveCSS('background-color', 'rgba(0, 0, 0, 0)')
await applyVariable.click()
const search = editor.page.getByPlaceholder('Search')
const picker = editor.page.locator('[data-slot=content]').filter({ has: search })
await expect(picker).toBeVisible()
const triggerBox = expectDefined(await applyVariable.boundingBox(), 'variable trigger bounds')
const pickerBox = expectDefined(await picker.boundingBox(), 'variable picker bounds')
expect(pickerBox.x + pickerBox.width).toBeLessThan(triggerBox.x)
expect(
Math.abs(pickerBox.y + pickerBox.height / 2 - (triggerBox.y + triggerBox.height / 2))
).toBeLessThan(4)
await search.press('Escape')
})
test('NumberField drag changes X position', async () => {
await editor.canvas.clearCanvas()
await editor.canvas.drawRect(100, 100, 80, 80)