From f4db716e2eb75195ae1935f4a164cb5c85761fed Mon Sep 17 00:00:00 2001 From: Matt Gros <3311227+mpge@users.noreply.github.com> Date: Sun, 5 Apr 2026 14:38:02 -0400 Subject: [PATCH] fix: clip color name text to swatch boundaries Add clipPath elements to each swatch so text never overflows into adjacent swatches. Also scale down the name font size when the estimated text width exceeds the available swatch width, with a minimum font size of 8px. Closes #1 --- src/render/svg.ts | 38 ++++++++++++++++++++++++++++++++------ 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/src/render/svg.ts b/src/render/svg.ts index 026bb2f..ea7abe9 100644 --- a/src/render/svg.ts +++ b/src/render/svg.ts @@ -14,7 +14,17 @@ function escapeXml(str: string): string { } /** - * Render a single swatch with color, name, and hex + * Estimate text width using average character width ratios. + * This is an approximation since SVG doesn't have built-in text measurement. + */ +function estimateTextWidth(text: string, fontSize: number): number { + // Average character width is roughly 0.6x the font size for sans-serif + return text.length * fontSize * 0.6; +} + +/** + * Render a single swatch with color, name, and hex. + * Uses clipPath to prevent text from overflowing swatch boundaries. */ function renderSwatch( color: PaletteColor, @@ -23,12 +33,21 @@ function renderSwatch( showName: boolean; showHex: boolean; font: string; - } + }, + swatchIndex: number ): string { const { x, y, width, height, radius, nameFontSize, hexFontSize } = swatch; const { showName, showHex, font } = options; const lines: string[] = []; + const clipId = `swatch-clip-${swatchIndex}`; + + // Define a clipPath for this swatch so text never overflows + lines.push( + ` `, + ` `, + ` ` + ); // Swatch background lines.push( @@ -38,20 +57,27 @@ function renderSwatch( // Calculate text positions - bottom-left aligned with padding const textPadding = Math.min(12, width * 0.08); const textX = x + textPadding; + const availableTextWidth = width - textPadding * 2; let textY = y + height - textPadding; // Hex code (bottom line) if (showHex) { lines.push( - ` ${color.hex}` + ` ${color.hex}` ); textY -= hexFontSize + 4; } - // Color name (above hex) + // Color name (above hex) - scale down font if name is too wide if (showName && color.name) { + let adjustedNameSize = nameFontSize; + const estimatedWidth = estimateTextWidth(color.name, adjustedNameSize); + if (estimatedWidth > availableTextWidth && availableTextWidth > 0) { + adjustedNameSize = Math.max(8, adjustedNameSize * (availableTextWidth / estimatedWidth)); + } + lines.push( - ` ${escapeXml(color.name)}` + ` ${escapeXml(color.name)}` ); } @@ -152,7 +178,7 @@ export function renderSvg( showName: opts.showName, showHex: opts.showHex, font, - }) + }, i) ); } });