From 0dd1014c2a10efda98aeb8798dc64d8f9ad67a83 Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Fri, 11 Sep 2026 03:27:27 +0300 Subject: [PATCH] fix(properties): simplify Effect styles and equivalent corners --- CHANGELOG.md | 1 + .../vue/src/controls/appearance/helpers.ts | 15 ++++++++- src/components/properties/EffectsSection.vue | 30 ++++++++++++++++-- tests/e2e/properties/shared-styles.spec.ts | 5 +-- ...ffects-applied-style-openpencil-darwin.png | Bin 0 -> 4201 bytes ...ffects-unbound-style-openpencil-darwin.png | Bin 0 -> 1340 bytes tests/engine/vue/controls/appearance.test.ts | 24 ++++++++++++++ 7 files changed, 70 insertions(+), 5 deletions(-) create mode 100644 tests/e2e/properties/shared-styles.spec.ts-snapshots/effects-applied-style-openpencil-darwin.png create mode 100644 tests/e2e/properties/shared-styles.spec.ts-snapshots/effects-unbound-style-openpencil-darwin.png diff --git a/CHANGELOG.md b/CHANGELOG.md index bdbb35306..e0ba67f55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,7 @@ ### Changed +- Keep applied and available Effect styles concise, and collapse equal independent corner fields when all four use the same variable. - Keep pixel-grid rounding invisible while showing alignment guides only for real geometry, objects, and canvas/layout guides. - Copy selections as Figma-compatible clipboard data while preserving typed geometry, text sizing, images, components, variables, modes, and shared styles for lossless in-app paste. - Choose whether interface animations follow the system motion preference or stay off, with live updates and a persistent override. diff --git a/packages/vue/src/controls/appearance/helpers.ts b/packages/vue/src/controls/appearance/helpers.ts index 6edabad45..31ef6c86e 100644 --- a/packages/vue/src/controls/appearance/helpers.ts +++ b/packages/vue/src/controls/appearance/helpers.ts @@ -26,6 +26,16 @@ type AppearanceActionOptions = AppearanceStateOptions & { editor: Editor } +function cornersHaveEquivalentBindings(node: SceneNode): boolean { + const first = node.boundVariables.topLeftRadius + if (!first) return false + return [ + node.boundVariables.topRightRadius, + node.boundVariables.bottomRightRadius, + node.boundVariables.bottomLeftRadius + ].every((id) => id === first) +} + function hasUnequalCorners(node: SceneNode) { return !( node.topLeftRadius === node.topRightRadius && @@ -48,7 +58,10 @@ export function createAppearanceState({ node, nodes, isMulti, merged }: Appearan const showIndependentCorners = computed(() => { if (isMulti.value) return false const selected = node.value - return selected ? selected.independentCorners || hasUnequalCorners(selected) : false + return selected + ? hasUnequalCorners(selected) || + (selected.independentCorners && !cornersHaveEquivalentBindings(selected)) + : false }) const cornerRadiusValue = computed(() => { diff --git a/src/components/properties/EffectsSection.vue b/src/components/properties/EffectsSection.vue index a9d478853..7d2ed983b 100644 --- a/src/components/properties/EffectsSection.vue +++ b/src/components/properties/EffectsSection.vue @@ -10,7 +10,7 @@ import { } from '@/components/properties/blend-mode/use' import PropertyItemRow from '@/components/properties/item-list/PropertyItemRow.vue' import PropertyListRoot from '@/components/properties/PropertyListRoot.vue' -import SharedStyleField from '@/components/properties/shared-style/SharedStyleField.vue' +import { useSharedStylePicker } from '@/components/properties/shared-style/useSharedStylePicker' import IconButton from '@/components/ui/button/IconButton.vue' import Tip from '@/components/ui/overlay/Tip.vue' import FillSwatch from '@/components/ui/paint/FillSwatch.vue' @@ -21,6 +21,13 @@ import AppSelect from '@/components/ui/select/AppSelect.vue' const effectsCtx = useEffectsControls() const { panels } = useI18n() const blendModeOptions = useBlendModeOptions() +const { + visible: stylesVisible, + hasStyle, + value: styleValue, + options: styleOptions, + update: updateStyle +} = useSharedStylePicker('effect') function effectPreview(effect: Effect): Fill { return { @@ -40,6 +47,17 @@ function effectPreview(effect: Effect): Fill { > - +

{{ panels.mixedEffectsHelp }}

diff --git a/tests/e2e/properties/shared-styles.spec.ts b/tests/e2e/properties/shared-styles.spec.ts index ea141fdd5..8e781bbf8 100644 --- a/tests/e2e/properties/shared-styles.spec.ts +++ b/tests/e2e/properties/shared-styles.spec.ts @@ -113,7 +113,8 @@ async function targetStyles(id = targetId) { test('unbound paint styles use one header action without redundant field labels', async () => { for (const [section, label] of [ ['Fill', 'Fill style'], - ['Stroke', 'Stroke style'] + ['Stroke', 'Stroke style'], + ['Effects', 'Effect style'] ]) { const panel = propertySection(page, section) await expect(panel.getByRole('combobox', { name: label })).toHaveCount(1) @@ -129,7 +130,7 @@ test('applies fill, stroke, effect, and grid styles from local definitions', asy await chooseStyle('Effects', 'Effect style', 'Effects/Card') await chooseStyle('Layout guide', 'Grid style', 'Grid/12 columns') await expect(propertySection(page, 'Fill')).toHaveScreenshot('fill-applied-style.png') - await expect(propertySection(page, 'Stroke')).toHaveScreenshot('stroke-applied-style.png') + await expect(propertySection(page, 'Effects')).toHaveScreenshot('effects-applied-style.png') expect(await targetStyles()).toMatchObject({ fillStyleId: '1:100', diff --git a/tests/e2e/properties/shared-styles.spec.ts-snapshots/effects-applied-style-openpencil-darwin.png b/tests/e2e/properties/shared-styles.spec.ts-snapshots/effects-applied-style-openpencil-darwin.png new file mode 100644 index 0000000000000000000000000000000000000000..b476d45a1568a01c369a40b71113d04a3d022bc4 GIT binary patch literal 4201 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 TAMD8`Qv`qtIT|boN-X#XNg0Sh literal 0 HcmV?d00001 diff --git a/tests/engine/vue/controls/appearance.test.ts b/tests/engine/vue/controls/appearance.test.ts index 90946ad4a..f2d780c48 100644 --- a/tests/engine/vue/controls/appearance.test.ts +++ b/tests/engine/vue/controls/appearance.test.ts @@ -47,6 +47,30 @@ describe('appearance control state', () => { expect(state.showIndependentCorners.value).toBe(true) }) + test('collapses equal independent corners when they share one binding', () => { + const node = rectangle() + node.independentCorners = true + node.boundVariables = { + topLeftRadius: 'radius', + topRightRadius: 'radius', + bottomRightRadius: 'radius', + bottomLeftRadius: 'radius' + } + expect(appearanceState(node).showIndependentCorners.value).toBe(false) + }) + + test('keeps equal independent corners expanded for distinct bindings', () => { + const node = rectangle() + node.independentCorners = true + node.boundVariables = { + topLeftRadius: 'radius-a', + topRightRadius: 'radius-b', + bottomRightRadius: 'radius-a', + bottomLeftRadius: 'radius-a' + } + expect(appearanceState(node).showIndependentCorners.value).toBe(true) + }) + test('expands imported unequal corners when the explicit flag is stale', () => { const node = rectangle() node.independentCorners = false