From df7048d51634d60669241383e315dd8a8e50aaa3 Mon Sep 17 00:00:00 2001 From: Miguel Angel Simon Sierra Date: Sun, 9 Aug 2026 01:50:45 -0400 Subject: [PATCH 1/4] feat(studio): show every colour of a mixed selection in the text swatch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Selecting text painted in more than one colour showed a white swatch. The toolbar reads a property only when the whole selection agrees on it, which is right for bold and italic (a toggle is on or off) but wrong for a swatch: with nothing to report it fell back to the default, so a red-and-green selection claimed to be white. The swatch now reads the colours as they run through the selection and draws one band per run, sized by how many characters carry it. Hard stops, not a fade — it reports the colours that are there, and a blend would draw colours that are not. A single-colour selection is a plain swatch, as before, and picking a colour still applies it to everything selected. --- .../editor/InlineTextToolbar.test.tsx | 48 ++++++++++++++++- .../components/editor/InlineTextToolbar.tsx | 31 +++++++++-- .../editor/inlineTextStyleRange.test.ts | 39 +++++++++++++- .../components/editor/inlineTextStyleRange.ts | 54 +++++++++++++++---- 4 files changed, 156 insertions(+), 16 deletions(-) diff --git a/packages/studio/src/components/editor/InlineTextToolbar.test.tsx b/packages/studio/src/components/editor/InlineTextToolbar.test.tsx index 148d786f41..8093bd4633 100644 --- a/packages/studio/src/components/editor/InlineTextToolbar.test.tsx +++ b/packages/studio/src/components/editor/InlineTextToolbar.test.tsx @@ -3,7 +3,7 @@ import React, { act } from "react"; import { createRoot } from "react-dom/client"; import { afterEach, describe, expect, it } from "vitest"; -import { InlineTextToolbar } from "./InlineTextToolbar"; +import { InlineTextToolbar, swatchBackground } from "./InlineTextToolbar"; import type { InlineTextEditSession } from "../../hooks/useInlineTextEdit"; (globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true; @@ -191,4 +191,50 @@ describe("InlineTextToolbar", () => { expect(toolbar.style.left).toBe(`${100 + (20 + 50) * scale}px`); expect(toolbar.style.top).toBe(`${50 + 40 * scale - 10}px`); }); + it("shows the selection's colours in the swatch when they differ", () => { + const { element, session, iframe } = scene( + 'Helloworld', + ); + const { host } = render(session, iframe); + + selectAll(element); + + const swatch = toolbarIn(host)!.querySelector("span[aria-hidden]")!; + expect(swatch.style.background).toBe( + "linear-gradient(90deg, red 0.00% 50.00%, lime 50.00% 100.00%)", + ); + }); + + it("shows a plain swatch when the whole selection is one colour", () => { + const { element, session, iframe } = scene('Hello world'); + const { host } = render(session, iframe); + + selectAll(element); + + const swatch = toolbarIn(host)!.querySelector("span[aria-hidden]")!; + expect(swatch.style.background).toBe("red"); + }); +}); + +describe("swatchBackground", () => { + it("shows every colour in the selection, sized by how much text carries it", () => { + expect( + swatchBackground( + [ + { value: "red", chars: 5 }, + { value: "lime", chars: 15 }, + ], + undefined, + ), + ).toBe("linear-gradient(90deg, red 0.00% 25.00%, lime 25.00% 100.00%)"); + }); + + it("stays a plain swatch when the selection is one colour", () => { + expect(swatchBackground([{ value: "red", chars: 5 }], "red")).toBe("red"); + }); + + it("falls back to the agreed colour when the characters carry none", () => { + expect(swatchBackground([], "rgb(1, 2, 3)")).toBe("rgb(1, 2, 3)"); + expect(swatchBackground([], undefined)).toBe("#ffffff"); + }); }); diff --git a/packages/studio/src/components/editor/InlineTextToolbar.tsx b/packages/studio/src/components/editor/InlineTextToolbar.tsx index 8ecc2b43f7..505d6609db 100644 --- a/packages/studio/src/components/editor/InlineTextToolbar.tsx +++ b/packages/studio/src/components/editor/InlineTextToolbar.tsx @@ -1,5 +1,5 @@ import { useCallback, useEffect, useState } from "react"; -import { applyInlineStyle, readInlineStyle } from "./inlineTextStyleRange"; +import { applyInlineStyle, readInlineStyle, readInlineStyleSpread } from "./inlineTextStyleRange"; import type { InlineTextEditSession } from "../../hooks/useInlineTextEdit"; /** @@ -24,6 +24,7 @@ interface ToolbarPlacement { left: number; top: number; styles: Record; + colours: Array<{ value: string; chars: number }>; } export function InlineTextToolbar({ @@ -90,7 +91,7 @@ export function InlineTextToolbar({