From 93889ab9bdc2203528d3159eabac8e1e7fca4789 Mon Sep 17 00:00:00 2001
From: valknarness
Date: Fri, 7 Nov 2025 14:33:38 +0100
Subject: [PATCH] fix: correct API integration and complete missing features
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Fix API response format mismatches and implement all remaining features:
**API Integration Fixes:**
- Fix ManipulationPanel to use `colors` instead of `results` from API responses
- Fix gradient endpoint to use `gradient` array from API response
- Fix color blindness simulator to use correct field names (`input`/`output` vs `original`/`simulated`)
- Fix text color optimizer request field (`backgrounds` vs `background_colors`)
- Fix method name casing: `simulateColorBlindness` (capital B)
- Add palette generation endpoint integration
**Type Definition Updates:**
- Update GradientData to match API structure with `gradient` array
- Update ColorBlindnessData to use `colors` with `input`/`output`/`difference_percentage`
- Update TextColorData to use `colors` with `textcolor`/`wcag_aa`/`wcag_aaa` fields
- Add PaletteGenerateRequest and PaletteGenerateData types
**Completed Features:**
- Harmony Palettes: Now uses dedicated `/palettes/generate` API endpoint
- Simplified from 80 lines of manual color theory to single API call
- Supports 6 harmony types: monochromatic, analogous, complementary, split-complementary, triadic, tetradic
- Text Color Optimizer: Full implementation with WCAG compliance checking
- Automatic black/white text color selection
- Live preview with contrast ratios
- AA/AAA compliance indicators
- Color Blindness Simulator: Fixed and working
- Shows difference percentage for each simulation
- Side-by-side comparison view
- Gradient Creator: Fixed to use correct API response structure
- Batch Operations: Fixed to extract output colors correctly
**UI Improvements:**
- Enable all accessibility tool cards (remove "Coming Soon" badges)
- Enable harmony palettes card
- Add safety check for gradient state to prevent undefined errors
All features now fully functional and properly integrated with Pastel API.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude
---
app/accessibility/colorblind/page.tsx | 14 +-
app/accessibility/page.tsx | 2 -
app/accessibility/textcolor/page.tsx | 221 +++++++++++++++++++++++++
app/batch/page.tsx | 6 +-
app/palettes/gradient/page.tsx | 6 +-
app/palettes/harmony/page.tsx | 86 ++--------
app/palettes/page.tsx | 1 -
components/tools/ManipulationPanel.tsx | 24 +--
lib/api/client.ts | 12 +-
lib/api/queries.ts | 15 ++
lib/api/types.ts | 39 ++++-
11 files changed, 314 insertions(+), 112 deletions(-)
create mode 100644 app/accessibility/textcolor/page.tsx
diff --git a/app/accessibility/colorblind/page.tsx b/app/accessibility/colorblind/page.tsx
index 7131d3d..2b644a0 100644
--- a/app/accessibility/colorblind/page.tsx
+++ b/app/accessibility/colorblind/page.tsx
@@ -15,7 +15,7 @@ export default function ColorBlindPage() {
const [colors, setColors] = useState(['#ff0099']);
const [blindnessType, setBlindnessType] = useState('protanopia');
const [simulations, setSimulations] = useState<
- Array<{ original: string; simulated: string }>
+ Array<{ input: string; output: string; difference_percentage: number }>
>([]);
const simulateMutation = useSimulateColorBlindness();
@@ -26,7 +26,7 @@ export default function ColorBlindPage() {
colors,
type: blindnessType,
});
- setSimulations(result.simulations);
+ setSimulations(result.colors);
toast.success(`Simulated ${blindnessType}`);
} catch (error) {
toast.error('Failed to simulate color blindness');
@@ -169,18 +169,18 @@ export default function ColorBlindPage() {
Original
-
- {sim.original}
+
+ {sim.input}
- As Seen
+ As Seen ({sim.difference_percentage.toFixed(1)}% difference)
-
- {sim.simulated}
+
+ {sim.output}
diff --git a/app/accessibility/page.tsx b/app/accessibility/page.tsx
index da9346f..230a45f 100644
--- a/app/accessibility/page.tsx
+++ b/app/accessibility/page.tsx
@@ -16,7 +16,6 @@ export default function AccessibilityPage() {
href: '/accessibility/colorblind',
icon: Eye,
features: ['Protanopia', 'Deuteranopia', 'Tritanopia'],
- comingSoon: true,
},
{
title: 'Text Color Optimizer',
@@ -24,7 +23,6 @@ export default function AccessibilityPage() {
href: '/accessibility/textcolor',
icon: Palette,
features: ['Automatic optimization', 'WCAG guaranteed', 'Light/dark options'],
- comingSoon: true,
},
];
diff --git a/app/accessibility/textcolor/page.tsx b/app/accessibility/textcolor/page.tsx
new file mode 100644
index 0000000..b7886da
--- /dev/null
+++ b/app/accessibility/textcolor/page.tsx
@@ -0,0 +1,221 @@
+'use client';
+
+import { useState } from 'react';
+import { ColorPicker } from '@/components/color/ColorPicker';
+import { ColorDisplay } from '@/components/color/ColorDisplay';
+import { Button } from '@/components/ui/button';
+import { useTextColor } from '@/lib/api/queries';
+import { Loader2, Palette, Plus, X, CheckCircle2, XCircle } from 'lucide-react';
+import { toast } from 'sonner';
+
+export default function TextColorPage() {
+ const [backgrounds, setBackgrounds] = useState(['#ff0099']);
+ const [results, setResults] = useState<
+ Array<{
+ background: string;
+ textcolor: string;
+ contrast_ratio: number;
+ wcag_aa: boolean;
+ wcag_aaa: boolean;
+ }>
+ >([]);
+
+ const textColorMutation = useTextColor();
+
+ const handleOptimize = async () => {
+ try {
+ const result = await textColorMutation.mutateAsync({
+ backgrounds,
+ });
+ setResults(result.colors);
+ toast.success(`Optimized text colors for ${result.colors.length} background(s)`);
+ } catch (error) {
+ toast.error('Failed to optimize text colors');
+ console.error(error);
+ }
+ };
+
+ const addBackground = () => {
+ if (backgrounds.length < 10) {
+ setBackgrounds([...backgrounds, '#000000']);
+ }
+ };
+
+ const removeBackground = (index: number) => {
+ if (backgrounds.length > 1) {
+ setBackgrounds(backgrounds.filter((_, i) => i !== index));
+ }
+ };
+
+ const updateBackground = (index: number, color: string) => {
+ const newBackgrounds = [...backgrounds];
+ newBackgrounds[index] = color;
+ setBackgrounds(newBackgrounds);
+ };
+
+ return (
+
+
+
+
Text Color Optimizer
+
+ Automatically find the best text color (black or white) for any background color
+
+
+
+
+ {/* Input */}
+
+
+
+
Background Colors
+
= 10}
+ variant="outline"
+ size="sm"
+ >
+
+ Add
+
+
+
+
+ {backgrounds.map((color, index) => (
+
+
+ updateBackground(index, newColor)}
+ />
+
+ {backgrounds.length > 1 && (
+
removeBackground(index)}
+ variant="ghost"
+ size="icon"
+ >
+
+
+ )}
+
+ ))}
+
+
+
+ {textColorMutation.isPending ? (
+ <>
+
+ Optimizing...
+ >
+ ) : (
+ <>
+
+ Optimize Text Colors
+ >
+ )}
+
+
+
+
+
How it works
+
+ This tool analyzes each background color and automatically selects either black
+ or white text to ensure maximum readability. The algorithm guarantees WCAG AA
+ compliance (4.5:1 contrast ratio) for normal text.
+
+
+
+
+ {/* Results */}
+
+ {results.length > 0 ? (
+ <>
+
+
Optimized Results
+
+ {results.map((result, index) => (
+
+
+
+
+
+ {result.background}
+
+
+
+
+
+
+ Sample Text Preview
+
+
+ The quick brown fox jumps over the lazy dog. This is how your text
+ will look on this background color.
+
+
+
+
+
+ Text Color:
+ {result.textcolor}
+
+
+ Contrast:
+
+ {result.contrast_ratio.toFixed(2)}:1
+
+
+
+ {result.wcag_aa ? (
+
+ ) : (
+
+ )}
+
+ WCAG AA
+
+
+
+ {result.wcag_aaa ? (
+
+ ) : (
+
+ )}
+
+ WCAG AAA
+
+
+
+
+ ))}
+
+
+ >
+ ) : (
+
+
+
Add background colors and click Optimize to see results
+
+ )}
+
+
+
+
+ );
+}
diff --git a/app/batch/page.tsx b/app/batch/page.tsx
index 4d0c45d..607982e 100644
--- a/app/batch/page.tsx
+++ b/app/batch/page.tsx
@@ -66,8 +66,10 @@ export default function BatchPage() {
break;
}
- setOutputColors(result.colors);
- toast.success(`Processed ${result.colors.length} colors`);
+ // Extract output colors from the result
+ const processed = result.colors.map((c) => c.output);
+ setOutputColors(processed);
+ toast.success(`Processed ${processed.length} colors`);
} catch (error) {
toast.error('Failed to process colors');
console.error(error);
diff --git a/app/palettes/gradient/page.tsx b/app/palettes/gradient/page.tsx
index 94d72fe..008dc4f 100644
--- a/app/palettes/gradient/page.tsx
+++ b/app/palettes/gradient/page.tsx
@@ -28,8 +28,8 @@ export default function GradientPage() {
count,
colorspace,
});
- setGradient(result.colors);
- toast.success(`Generated ${result.colors.length} colors`);
+ setGradient(result.gradient);
+ toast.success(`Generated ${result.gradient.length} colors`);
} catch (error) {
toast.error('Failed to generate gradient');
}
@@ -156,7 +156,7 @@ export default function GradientPage() {
{/* Preview */}
- {gradient.length > 0 && (
+ {gradient && gradient.length > 0 && (
<>
Gradient Preview
diff --git a/app/palettes/harmony/page.tsx b/app/palettes/harmony/page.tsx
index 8164a44..67d156d 100644
--- a/app/palettes/harmony/page.tsx
+++ b/app/palettes/harmony/page.tsx
@@ -6,7 +6,7 @@ import { PaletteGrid } from '@/components/color/PaletteGrid';
import { ExportMenu } from '@/components/tools/ExportMenu';
import { Button } from '@/components/ui/button';
import { Select } from '@/components/ui/select';
-import { useComplement, useRotate } from '@/lib/api/queries';
+import { useGeneratePalette } from '@/lib/api/queries';
import { Loader2, Palette } from 'lucide-react';
import { toast } from 'sonner';
@@ -23,83 +23,17 @@ export default function HarmonyPage() {
const [harmonyType, setHarmonyType] = useState
('complementary');
const [palette, setPalette] = useState([]);
- const complementMutation = useComplement();
- const rotateMutation = useRotate();
+ const paletteMutation = useGeneratePalette();
const generateHarmony = async () => {
try {
- let colors: string[] = [baseColor];
-
- switch (harmonyType) {
- case 'monochromatic':
- // Base color with lightness variations
- colors = [baseColor];
- toast.info('Monochromatic harmony uses variations of the base color');
- break;
-
- case 'analogous':
- // Base + 30° and -30°
- const analog1 = await rotateMutation.mutateAsync({
- colors: [baseColor],
- amount: 30,
- });
- const analog2 = await rotateMutation.mutateAsync({
- colors: [baseColor],
- amount: -30,
- });
- colors = [analog2.colors[0], baseColor, analog1.colors[0]];
- break;
-
- case 'complementary':
- // Base + opposite (180°)
- const complement = await complementMutation.mutateAsync([baseColor]);
- colors = [baseColor, complement.colors[0]];
- break;
-
- case 'split-complementary':
- // Base + 150° and 210° (flanking the complement)
- const split1 = await rotateMutation.mutateAsync({
- colors: [baseColor],
- amount: 150,
- });
- const split2 = await rotateMutation.mutateAsync({
- colors: [baseColor],
- amount: 210,
- });
- colors = [baseColor, split1.colors[0], split2.colors[0]];
- break;
-
- case 'triadic':
- // Base + 120° and 240° (evenly spaced)
- const tri1 = await rotateMutation.mutateAsync({
- colors: [baseColor],
- amount: 120,
- });
- const tri2 = await rotateMutation.mutateAsync({
- colors: [baseColor],
- amount: 240,
- });
- colors = [baseColor, tri1.colors[0], tri2.colors[0]];
- break;
-
- case 'tetradic':
- // Base + 90°, 180°, 270° (square)
- const tet1 = await rotateMutation.mutateAsync({
- colors: [baseColor],
- amount: 90,
- });
- const tet2 = await rotateMutation.mutateAsync({
- colors: [baseColor],
- amount: 180,
- });
- const tet3 = await rotateMutation.mutateAsync({
- colors: [baseColor],
- amount: 270,
- });
- colors = [baseColor, tet1.colors[0], tet2.colors[0], tet3.colors[0]];
- break;
- }
+ const result = await paletteMutation.mutateAsync({
+ base: baseColor,
+ scheme: harmonyType,
+ });
+ // Combine primary and secondary colors into flat array
+ const colors = [result.palette.primary, ...result.palette.secondary];
setPalette(colors);
toast.success(`Generated ${harmonyType} harmony palette`);
} catch (error) {
@@ -157,10 +91,10 @@ export default function HarmonyPage() {
- {complementMutation.isPending || rotateMutation.isPending ? (
+ {paletteMutation.isPending ? (
<>
Generating...
diff --git a/app/palettes/page.tsx b/app/palettes/page.tsx
index cb9ae2c..6c98720 100644
--- a/app/palettes/page.tsx
+++ b/app/palettes/page.tsx
@@ -23,7 +23,6 @@ export default function PalettesPage() {
href: '/palettes/harmony',
icon: Palette,
features: ['Color theory', 'Multiple schemes', 'Instant generation'],
- comingSoon: true,
},
];
diff --git a/components/tools/ManipulationPanel.tsx b/components/tools/ManipulationPanel.tsx
index 54f910e..6ae7344 100644
--- a/components/tools/ManipulationPanel.tsx
+++ b/components/tools/ManipulationPanel.tsx
@@ -38,8 +38,8 @@ export function ManipulationPanel({ color, onColorChange }: ManipulationPanelPro
colors: [color],
amount: lightenAmount,
});
- if (result.results[0]) {
- onColorChange(result.results[0].output);
+ if (result.colors[0]) {
+ onColorChange(result.colors[0].output);
toast.success(`Lightened by ${(lightenAmount * 100).toFixed(0)}%`);
}
} catch (error) {
@@ -53,8 +53,8 @@ export function ManipulationPanel({ color, onColorChange }: ManipulationPanelPro
colors: [color],
amount: darkenAmount,
});
- if (result.results[0]) {
- onColorChange(result.results[0].output);
+ if (result.colors[0]) {
+ onColorChange(result.colors[0].output);
toast.success(`Darkened by ${(darkenAmount * 100).toFixed(0)}%`);
}
} catch (error) {
@@ -68,8 +68,8 @@ export function ManipulationPanel({ color, onColorChange }: ManipulationPanelPro
colors: [color],
amount: saturateAmount,
});
- if (result.results[0]) {
- onColorChange(result.results[0].output);
+ if (result.colors[0]) {
+ onColorChange(result.colors[0].output);
toast.success(`Saturated by ${(saturateAmount * 100).toFixed(0)}%`);
}
} catch (error) {
@@ -83,8 +83,8 @@ export function ManipulationPanel({ color, onColorChange }: ManipulationPanelPro
colors: [color],
amount: desaturateAmount,
});
- if (result.results[0]) {
- onColorChange(result.results[0].output);
+ if (result.colors[0]) {
+ onColorChange(result.colors[0].output);
toast.success(`Desaturated by ${(desaturateAmount * 100).toFixed(0)}%`);
}
} catch (error) {
@@ -98,8 +98,8 @@ export function ManipulationPanel({ color, onColorChange }: ManipulationPanelPro
colors: [color],
amount: rotateAmount,
});
- if (result.results[0]) {
- onColorChange(result.results[0].output);
+ if (result.colors[0]) {
+ onColorChange(result.colors[0].output);
toast.success(`Rotated hue by ${rotateAmount}°`);
}
} catch (error) {
@@ -110,8 +110,8 @@ export function ManipulationPanel({ color, onColorChange }: ManipulationPanelPro
const handleComplement = async () => {
try {
const result = await complementMutation.mutateAsync([color]);
- if (result.results[0]) {
- onColorChange(result.results[0].output);
+ if (result.colors[0]) {
+ onColorChange(result.colors[0].output);
toast.success('Generated complementary color');
}
} catch (error) {
diff --git a/lib/api/client.ts b/lib/api/client.ts
index 219ff5d..c012101 100644
--- a/lib/api/client.ts
+++ b/lib/api/client.ts
@@ -27,6 +27,8 @@ import type {
NamedColorSearchData,
HealthData,
CapabilitiesData,
+ PaletteGenerateRequest,
+ PaletteGenerateData,
} from './types';
export class PastelAPIClient {
@@ -186,7 +188,7 @@ export class PastelAPIClient {
}
// Accessibility
- async simulateColorblindness(request: ColorBlindnessRequest): Promise> {
+ async simulateColorBlindness(request: ColorBlindnessRequest): Promise> {
return this.request('/colors/colorblind', {
method: 'POST',
body: JSON.stringify(request),
@@ -226,6 +228,14 @@ export class PastelAPIClient {
method: 'GET',
});
}
+
+ // Palette Generation
+ async generatePalette(request: PaletteGenerateRequest): Promise> {
+ return this.request('/palettes/generate', {
+ method: 'POST',
+ body: JSON.stringify(request),
+ });
+ }
}
// Export singleton instance
diff --git a/lib/api/queries.ts b/lib/api/queries.ts
index 8131c5a..75aeaf2 100644
--- a/lib/api/queries.ts
+++ b/lib/api/queries.ts
@@ -18,6 +18,8 @@ import type {
GradientRequest,
GradientData,
ColorBlindnessRequest,
+ PaletteGenerateRequest,
+ PaletteGenerateData,
ColorBlindnessData,
TextColorRequest,
TextColorData,
@@ -234,3 +236,16 @@ export const useHealth = () => {
refetchInterval: 60000, // Check every minute
});
};
+
+// Palette Generation
+export const useGeneratePalette = () => {
+ return useMutation({
+ mutationFn: async (request: PaletteGenerateRequest) => {
+ const response = await pastelAPI.generatePalette(request);
+ if (!response.success) {
+ throw new Error(response.error.message);
+ }
+ return response.data;
+ },
+ });
+};
diff --git a/lib/api/types.ts b/lib/api/types.ts
index ae00579..9ace93c 100644
--- a/lib/api/types.ts
+++ b/lib/api/types.ts
@@ -114,7 +114,9 @@ export interface ColorManipulationRequest {
}
export interface ColorManipulationData {
- results: Array<{
+ operation?: string;
+ amount?: number;
+ colors: Array<{
input: string;
output: string;
}>;
@@ -165,7 +167,10 @@ export interface GradientRequest {
}
export interface GradientData {
- colors: string[];
+ stops: string[];
+ count: number;
+ colorspace: string;
+ gradient: string[];
}
export interface ColorDistanceRequest {
@@ -196,21 +201,25 @@ export interface ColorBlindnessRequest {
}
export interface ColorBlindnessData {
- simulations: Array<{
- original: string;
- simulated: string;
+ type: string;
+ colors: Array<{
+ input: string;
+ output: string;
+ difference_percentage: number;
}>;
}
export interface TextColorRequest {
- background_colors: string[];
+ backgrounds: string[];
}
export interface TextColorData {
- results: Array<{
+ colors: Array<{
background: string;
- text_color: string;
+ textcolor: string;
contrast_ratio: number;
+ wcag_aa: boolean;
+ wcag_aaa: boolean;
}>;
}
@@ -243,3 +252,17 @@ export interface CapabilitiesData {
distance_metrics: string[];
colorblindness_types: string[];
}
+
+export interface PaletteGenerateRequest {
+ base: string;
+ scheme: 'monochromatic' | 'analogous' | 'complementary' | 'split-complementary' | 'triadic' | 'tetradic';
+}
+
+export interface PaletteGenerateData {
+ base: string;
+ scheme: string;
+ palette: {
+ primary: string;
+ secondary: string[];
+ };
+}