diff --git a/app/playground/page.tsx b/app/playground/page.tsx
new file mode 100644
index 0000000..4b33cb2
--- /dev/null
+++ b/app/playground/page.tsx
@@ -0,0 +1,95 @@
+'use client';
+
+import { useState } from 'react';
+import { ColorPicker } from '@/components/color/ColorPicker';
+import { ColorDisplay } from '@/components/color/ColorDisplay';
+import { ColorInfo } from '@/components/color/ColorInfo';
+import { Button } from '@/components/ui/button';
+import { useColorInfo } from '@/lib/api/queries';
+import { Loader2 } from 'lucide-react';
+
+export default function PlaygroundPage() {
+ const [color, setColor] = useState('#ff0099');
+
+ const { data, isLoading, isError, error } = useColorInfo({
+ colors: [color],
+ });
+
+ const colorInfo = data?.colors[0];
+
+ return (
+
+
+
+
Color Playground
+
+ Interactive color manipulation and analysis tool
+
+
+
+
+ {/* Left Column: Color Picker and Display */}
+
+
+
Color Picker
+
+
+
+
+
+
+ {/* Right Column: Color Information */}
+
+
+
Color Information
+
+ {isLoading && (
+
+
+
+ )}
+
+ {isError && (
+
+
Error loading color information
+
{error?.message || 'Unknown error'}
+
+ )}
+
+ {colorInfo &&
}
+
+
+
+
Quick Actions
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/components/color/ColorDisplay.tsx b/components/color/ColorDisplay.tsx
new file mode 100644
index 0000000..c7c3615
--- /dev/null
+++ b/components/color/ColorDisplay.tsx
@@ -0,0 +1,38 @@
+'use client';
+
+import { cn } from '@/lib/utils/cn';
+
+interface ColorDisplayProps {
+ color: string;
+ size?: 'sm' | 'md' | 'lg' | 'xl';
+ className?: string;
+ showBorder?: boolean;
+}
+
+export function ColorDisplay({
+ color,
+ size = 'lg',
+ className,
+ showBorder = true,
+}: ColorDisplayProps) {
+ const sizeClasses = {
+ sm: 'h-16 w-16',
+ md: 'h-32 w-32',
+ lg: 'h-48 w-48',
+ xl: 'h-64 w-64',
+ };
+
+ return (
+
+ );
+}
diff --git a/components/color/ColorInfo.tsx b/components/color/ColorInfo.tsx
new file mode 100644
index 0000000..08ea95a
--- /dev/null
+++ b/components/color/ColorInfo.tsx
@@ -0,0 +1,92 @@
+'use client';
+
+import { ColorInfo as ColorInfoType } from '@/lib/api/types';
+import { Button } from '@/components/ui/button';
+import { Copy } from 'lucide-react';
+import { toast } from 'sonner';
+import { cn } from '@/lib/utils/cn';
+
+interface ColorInfoProps {
+ info: ColorInfoType;
+ className?: string;
+}
+
+export function ColorInfo({ info, className }: ColorInfoProps) {
+ const copyToClipboard = (value: string, label: string) => {
+ navigator.clipboard.writeText(value);
+ toast.success(`Copied ${label} to clipboard`);
+ };
+
+ const formatRgb = (rgb: { r: number; g: number; b: number; a?: number }) => {
+ if (rgb.a !== undefined && rgb.a < 1) {
+ return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${rgb.a})`;
+ }
+ return `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`;
+ };
+
+ const formatHsl = (hsl: { h: number; s: number; l: number; a?: number }) => {
+ if (hsl.a !== undefined && hsl.a < 1) {
+ return `hsla(${Math.round(hsl.h)}°, ${Math.round(hsl.s * 100)}%, ${Math.round(hsl.l * 100)}%, ${hsl.a})`;
+ }
+ return `hsl(${Math.round(hsl.h)}°, ${Math.round(hsl.s * 100)}%, ${Math.round(hsl.l * 100)}%)`;
+ };
+
+ const formatLab = (lab: { l: number; a: number; b: number }) => {
+ return `lab(${lab.l.toFixed(1)}, ${lab.a.toFixed(1)}, ${lab.b.toFixed(1)})`;
+ };
+
+ const formats = [
+ { label: 'Hex', value: info.hex },
+ { label: 'RGB', value: formatRgb(info.rgb) },
+ { label: 'HSL', value: formatHsl(info.hsl) },
+ { label: 'Lab', value: formatLab(info.lab) },
+ { label: 'OkLab', value: formatLab(info.oklab) },
+ ];
+
+ return (
+
+
+ {formats.map((format) => (
+
+
+
{format.label}
+
{format.value}
+
+
+
+ ))}
+
+
+
+
+
Brightness
+
{(info.brightness * 100).toFixed(1)}%
+
+
+
Luminance
+
{(info.luminance * 100).toFixed(1)}%
+
+
+
Type
+
{info.is_light ? 'Light' : 'Dark'}
+
+ {info.name && (
+
+ )}
+
+
+ );
+}
diff --git a/components/color/ColorPicker.tsx b/components/color/ColorPicker.tsx
new file mode 100644
index 0000000..b089b0c
--- /dev/null
+++ b/components/color/ColorPicker.tsx
@@ -0,0 +1,38 @@
+'use client';
+
+import { HexColorPicker } from 'react-colorful';
+import { Input } from '@/components/ui/input';
+import { cn } from '@/lib/utils/cn';
+
+interface ColorPickerProps {
+ color: string;
+ onChange: (color: string) => void;
+ className?: string;
+}
+
+export function ColorPicker({ color, onChange, className }: ColorPickerProps) {
+ const handleInputChange = (e: React.ChangeEvent) => {
+ const value = e.target.value;
+ // Allow partial input while typing
+ onChange(value);
+ };
+
+ return (
+
+
+
+
+
+
+
+ );
+}
diff --git a/lib/api/queries.ts b/lib/api/queries.ts
new file mode 100644
index 0000000..235549e
--- /dev/null
+++ b/lib/api/queries.ts
@@ -0,0 +1,206 @@
+'use client';
+
+import { useQuery, useMutation, UseQueryOptions } from '@tanstack/react-query';
+import { pastelAPI } from './client';
+import type {
+ ColorInfoRequest,
+ ColorInfoData,
+ ConvertFormatRequest,
+ ConvertFormatData,
+ ColorManipulationRequest,
+ ColorManipulationData,
+ ColorMixRequest,
+ ColorMixData,
+ RandomColorsRequest,
+ RandomColorsData,
+ DistinctColorsRequest,
+ DistinctColorsData,
+ GradientRequest,
+ GradientData,
+ NamedColorsData,
+ HealthData,
+} from './types';
+
+// Color Information
+export const useColorInfo = (
+ request: ColorInfoRequest,
+ options?: Omit, 'queryKey' | 'queryFn'>
+) => {
+ return useQuery({
+ queryKey: ['colorInfo', request.colors],
+ queryFn: async () => {
+ const response = await pastelAPI.getColorInfo(request);
+ if (!response.success) {
+ throw new Error(response.error.message);
+ }
+ return response.data;
+ },
+ enabled: request.colors.length > 0 && request.colors.every((c) => c.length > 0),
+ ...options,
+ });
+};
+
+// Format Conversion
+export const useConvertFormat = () => {
+ return useMutation({
+ mutationFn: async (request: ConvertFormatRequest) => {
+ const response = await pastelAPI.convertFormat(request);
+ if (!response.success) {
+ throw new Error(response.error.message);
+ }
+ return response.data;
+ },
+ });
+};
+
+// Color Manipulation
+export const useLighten = () => {
+ return useMutation({
+ mutationFn: async (request: ColorManipulationRequest) => {
+ const response = await pastelAPI.lighten(request);
+ if (!response.success) {
+ throw new Error(response.error.message);
+ }
+ return response.data;
+ },
+ });
+};
+
+export const useDarken = () => {
+ return useMutation({
+ mutationFn: async (request: ColorManipulationRequest) => {
+ const response = await pastelAPI.darken(request);
+ if (!response.success) {
+ throw new Error(response.error.message);
+ }
+ return response.data;
+ },
+ });
+};
+
+export const useSaturate = () => {
+ return useMutation({
+ mutationFn: async (request: ColorManipulationRequest) => {
+ const response = await pastelAPI.saturate(request);
+ if (!response.success) {
+ throw new Error(response.error.message);
+ }
+ return response.data;
+ },
+ });
+};
+
+export const useDesaturate = () => {
+ return useMutation({
+ mutationFn: async (request: ColorManipulationRequest) => {
+ const response = await pastelAPI.desaturate(request);
+ if (!response.success) {
+ throw new Error(response.error.message);
+ }
+ return response.data;
+ },
+ });
+};
+
+export const useRotate = () => {
+ return useMutation({
+ mutationFn: async (request: ColorManipulationRequest) => {
+ const response = await pastelAPI.rotate(request);
+ if (!response.success) {
+ throw new Error(response.error.message);
+ }
+ return response.data;
+ },
+ });
+};
+
+export const useComplement = () => {
+ return useMutation({
+ mutationFn: async (colors: string[]) => {
+ const response = await pastelAPI.complement(colors);
+ if (!response.success) {
+ throw new Error(response.error.message);
+ }
+ return response.data;
+ },
+ });
+};
+
+export const useMixColors = () => {
+ return useMutation({
+ mutationFn: async (request: ColorMixRequest) => {
+ const response = await pastelAPI.mix(request);
+ if (!response.success) {
+ throw new Error(response.error.message);
+ }
+ return response.data;
+ },
+ });
+};
+
+// Color Generation
+export const useGenerateRandom = () => {
+ return useMutation({
+ mutationFn: async (request: RandomColorsRequest) => {
+ const response = await pastelAPI.generateRandom(request);
+ if (!response.success) {
+ throw new Error(response.error.message);
+ }
+ return response.data;
+ },
+ });
+};
+
+export const useGenerateDistinct = () => {
+ return useMutation({
+ mutationFn: async (request: DistinctColorsRequest) => {
+ const response = await pastelAPI.generateDistinct(request);
+ if (!response.success) {
+ throw new Error(response.error.message);
+ }
+ return response.data;
+ },
+ });
+};
+
+export const useGenerateGradient = () => {
+ return useMutation({
+ mutationFn: async (request: GradientRequest) => {
+ const response = await pastelAPI.generateGradient(request);
+ if (!response.success) {
+ throw new Error(response.error.message);
+ }
+ return response.data;
+ },
+ });
+};
+
+// Named Colors
+export const useNamedColors = () => {
+ return useQuery({
+ queryKey: ['namedColors'],
+ queryFn: async () => {
+ const response = await pastelAPI.getNamedColors();
+ if (!response.success) {
+ throw new Error(response.error.message);
+ }
+ return response.data;
+ },
+ staleTime: Infinity, // Named colors never change
+ });
+};
+
+// Health Check
+export const useHealth = () => {
+ return useQuery({
+ queryKey: ['health'],
+ queryFn: async () => {
+ const response = await pastelAPI.getHealth();
+ if (!response.success) {
+ throw new Error(response.error.message);
+ }
+ return response.data;
+ },
+ refetchInterval: 60000, // Check every minute
+ });
+};