From 310cd3aeff05bddba33aa8d50a93fe4be7236228 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Feb 2026 06:06:58 +0000 Subject: [PATCH 1/3] Initial plan From a86e13ea6667788e7ef8602a80073c969b9541ad Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Feb 2026 06:15:52 +0000 Subject: [PATCH 2/3] Add failing tests for stripes preset span position corruption Co-authored-by: argyleink <1134620+argyleink@users.noreply.github.com> --- package-lock.json | 13 ++++++++ src/lib/stripesIssue.test.ts | 65 ++++++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+) create mode 100644 src/lib/stripesIssue.test.ts diff --git a/package-lock.json b/package-lock.json index 201ded7..a8d92bc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -177,6 +177,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -200,6 +201,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -1068,6 +1070,7 @@ "integrity": "sha512-xgKtpjQ6Ry4mdShd01ht5AODUsW7+K1iValPDq7QX8zI1hWOKREH9GjG8SRCN5tC4K7UXmMhuQam7gbLByVcnw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@standard-schema/spec": "^1.0.0", "@sveltejs/acorn-typescript": "^1.0.5", @@ -1114,6 +1117,7 @@ "integrity": "sha512-3pppgIeIZs6nrQLazzKcdnTJ2IWiui/UucEPXKyFG35TKaHQrfkWBnv6hyJcLxFuR90t+LaoecrqTs8rJKWfSQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", "debug": "^4.4.1", @@ -1279,6 +1283,7 @@ "integrity": "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.10.0" } @@ -1404,6 +1409,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1883,6 +1889,7 @@ "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "cssstyle": "^4.2.1", "data-urls": "^5.0.0", @@ -2080,6 +2087,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -2122,6 +2130,7 @@ "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -2391,6 +2400,7 @@ "integrity": "sha512-Fn2mCc3XX0gnnbBYzWOTrZHi5WnF9KvqmB1+KGlUWoJkdioPmFYtg2ALBr6xl2dcnFTz3Vi7/mHpbKSVg/imVg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", @@ -2578,6 +2588,7 @@ "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -2599,6 +2610,7 @@ "integrity": "sha512-X5QFK4SGynAeeIt+A7ZWnApdUyHYm+pzv/8/A57LqSGcI88U6R6ipOs3uCesdc6yl7nl+zNO0t8LmqAdXcQihw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -2717,6 +2729,7 @@ "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", diff --git a/src/lib/stripesIssue.test.ts b/src/lib/stripesIssue.test.ts new file mode 100644 index 0000000..588be93 --- /dev/null +++ b/src/lib/stripesIssue.test.ts @@ -0,0 +1,65 @@ +import { describe, it, expect } from 'vitest' +import { updateStops } from '../utils/stops' +import { buildGradientStrings } from '../utils/gradientString' + +describe('stripes preset round-trip', () => { + function makeStripeStops() { + const presetStops = [ + {color: '#fff'}, + {kind: 'hint', auto: null, percentage: null}, + {color: '#000', position1: '0', position2: '20'}, + {kind: 'hint', auto: null, percentage: null}, + {color: '#fff', position1: '0', position2: '40'}, + {kind: 'hint', auto: null, percentage: null}, + {color: '#000', position1: '0', position2: '60'}, + {kind: 'hint', auto: null, percentage: null}, + {color: '#fff', position1: '0', position2: '80'}, + {kind: 'hint', auto: null, percentage: null}, + {color: '#000', position1: '0', position2: '100'}, + ] + return presetStops.map(stop => { + if (stop.kind !== 'hint') { + return { + kind: 'stop', + color: (stop as any).color, + auto: null, + position1: ((stop as any).position1 ?? null), + position2: ((stop as any).position2 ?? null), + } + } else return stop + }) + } + + it('preserves span positions after multiple updateStops calls', () => { + let stops = updateStops(makeStripeStops() as any) + stops = updateStops(stops) // second call simulates user interaction + stops = updateStops(stops) // third call + + const colorStops = stops.filter((s: any) => s.kind === 'stop') + expect(colorStops[1].position2).toBe('20') + expect(colorStops[2].position2).toBe('40') + expect(colorStops[3].position2).toBe('60') + expect(colorStops[4].position2).toBe('80') + expect(colorStops[5].position2).toBe('100') + }) + + it('produces correct CSS after multiple updateStops calls', () => { + let stops = updateStops(makeStripeStops() as any) + stops = updateStops(stops) // second call simulates user interaction + + const layer = { + type: 'linear', space: 'oklab', interpolation: 'shorter', + stops, + linear: { named_angle: 'to top right', angle: null }, + radial: { shape: 'circle', size: 'farthest-corner', named_position: 'center', position: { x: null, y: null } }, + conic: { angle: 0, named_position: 'center', position: { x: null, y: null } }, + } + const { modern } = buildGradientStrings(layer as any) + const flat = modern.replace(/\s+/g, ' ') + expect(flat).toContain('#000 0% 20%') + expect(flat).toContain('#fff 0% 40%') + expect(flat).toContain('#000 0% 60%') + expect(flat).toContain('#fff 0% 80%') + expect(flat).toContain('#000 0% 100%') + }) +}) From da91b38cb036e4ebfdd55ae61b92a669dcbf6419 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Feb 2026 06:16:34 +0000 Subject: [PATCH 3/3] Fix hadLinked condition in updateStops to preserve explicit span positions Co-authored-by: argyleink <1134620+argyleink@users.noreply.github.com> --- src/utils/stops.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/utils/stops.ts b/src/utils/stops.ts index b6d50f4..6b2dab0 100644 --- a/src/utils/stops.ts +++ b/src/utils/stops.ts @@ -25,10 +25,13 @@ export function updateStops(stops: Stop[]): Stop[] { const p1Unset = (stop.position1 == null) || (prevAuto != null && String(stop.position1) == String(prevAuto)) const p2Unset = (stop.position2 == null) - // Detect whether position2 was effectively "linked" to position1 or to the prior auto value + // Detect whether position2 was effectively "linked" to position1 or to the prior auto value. + // The prevAuto condition only applies when position1 is also auto-managed (p1Unset), to + // avoid treating explicit span end-positions (e.g. 0% 20%) as linked when they coincidentally + // match the prior auto value. const hadLinked = (!p2Unset) && ( String(stop.position2) === String(stop.position1) || - (prevAuto != null && String(stop.position2) === String(prevAuto)) + (p1Unset && prevAuto != null && String(stop.position2) === String(prevAuto)) ) // Only assign auto for position1 when it is unset or previously auto-managed