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 000000000..b476d45a1 Binary files /dev/null and b/tests/e2e/properties/shared-styles.spec.ts-snapshots/effects-applied-style-openpencil-darwin.png differ diff --git a/tests/e2e/properties/shared-styles.spec.ts-snapshots/effects-unbound-style-openpencil-darwin.png b/tests/e2e/properties/shared-styles.spec.ts-snapshots/effects-unbound-style-openpencil-darwin.png new file mode 100644 index 000000000..58a61dbd1 Binary files /dev/null and b/tests/e2e/properties/shared-styles.spec.ts-snapshots/effects-unbound-style-openpencil-darwin.png differ 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