diff --git a/app/playground/page.tsx b/app/playground/page.tsx
index 485a8af..458dcb3 100644
--- a/app/playground/page.tsx
+++ b/app/playground/page.tsx
@@ -1,15 +1,26 @@
'use client';
-import { useState } from 'react';
+import { useState, useEffect, Suspense } from 'react';
+import { useSearchParams, useRouter } from 'next/navigation';
import { ColorPicker } from '@/components/color/ColorPicker';
import { ColorDisplay } from '@/components/color/ColorDisplay';
import { ColorInfo } from '@/components/color/ColorInfo';
import { ManipulationPanel } from '@/components/tools/ManipulationPanel';
import { useColorInfo } from '@/lib/api/queries';
-import { Loader2 } from 'lucide-react';
+import { useKeyboard } from '@/lib/hooks/useKeyboard';
+import { useColorHistory } from '@/lib/stores/historyStore';
+import { Loader2, Share2, History, X } from 'lucide-react';
+import { Button } from '@/components/ui/button';
+import { toast } from 'sonner';
-export default function PlaygroundPage() {
- const [color, setColor] = useState('#ff0099');
+function PlaygroundContent() {
+ const searchParams = useSearchParams();
+ const router = useRouter();
+ const [color, setColor] = useState(() => {
+ // Initialize from URL if available
+ const urlColor = searchParams.get('color');
+ return urlColor ? `#${urlColor.replace('#', '')}` : '#ff0099';
+ });
const { data, isLoading, isError, error } = useColorInfo({
colors: [color],
@@ -17,6 +28,58 @@ export default function PlaygroundPage() {
const colorInfo = data?.colors[0];
+ // Color history
+ const { history, addColor, removeColor, clearHistory, getRecent } = useColorHistory();
+ const recentColors = getRecent(10);
+
+ // Update URL and history when color changes
+ useEffect(() => {
+ const hex = color.replace('#', '');
+ router.push(`/playground?color=${hex}`, { scroll: false });
+ addColor(color);
+ }, [color, router, addColor]);
+
+ // Share color via URL
+ const handleShare = () => {
+ const url = `${window.location.origin}/playground?color=${color.replace('#', '')}`;
+ navigator.clipboard.writeText(url);
+ toast.success('Link copied to clipboard!');
+ };
+
+ // Copy color to clipboard
+ const handleCopyColor = () => {
+ navigator.clipboard.writeText(color);
+ toast.success('Color copied to clipboard!');
+ };
+
+ // Random color generation
+ const handleRandomColor = () => {
+ const randomHex = '#' + Math.floor(Math.random() * 16777215).toString(16).padStart(6, '0');
+ setColor(randomHex);
+ };
+
+ // Keyboard shortcuts
+ useKeyboard([
+ {
+ key: 'c',
+ meta: true,
+ handler: handleCopyColor,
+ description: 'Copy color',
+ },
+ {
+ key: 's',
+ meta: true,
+ handler: handleShare,
+ description: 'Share color',
+ },
+ {
+ key: 'r',
+ meta: true,
+ handler: handleRandomColor,
+ description: 'Random color',
+ },
+ ]);
+
return (
@@ -25,6 +88,14 @@ export default function PlaygroundPage() {
Interactive color manipulation and analysis tool
+
+ ⌘C
+ Copy
+ ⌘S
+ Share
+ ⌘R
+ Random
+
@@ -36,11 +107,60 @@ export default function PlaygroundPage() {
-
Preview
+
+
Preview
+
+
+
+ {recentColors.length > 0 && (
+
+
+
+
+
Recent Colors
+
+
+
+
+ {recentColors.map((entry) => (
+
+ ))}
+
+
+ )}
{/* Right Column: Color Information */}
@@ -74,3 +194,17 @@ export default function PlaygroundPage() {
);
}
+
+export default function PlaygroundPage() {
+ return (
+
+
+
+
+
+ }>
+
+
+ );
+}
diff --git a/lib/hooks/useKeyboard.ts b/lib/hooks/useKeyboard.ts
new file mode 100644
index 0000000..60e6a98
--- /dev/null
+++ b/lib/hooks/useKeyboard.ts
@@ -0,0 +1,91 @@
+import { useEffect } from 'react';
+
+export interface KeyboardShortcut {
+ key: string;
+ ctrl?: boolean;
+ shift?: boolean;
+ alt?: boolean;
+ meta?: boolean;
+ handler: (event: KeyboardEvent) => void;
+ description?: string;
+}
+
+/**
+ * Hook to register keyboard shortcuts
+ *
+ * @example
+ * ```tsx
+ * useKeyboard([
+ * {
+ * key: 'c',
+ * meta: true, // Cmd on Mac, Ctrl on Windows
+ * handler: () => copyToClipboard(),
+ * description: 'Copy color',
+ * },
+ * {
+ * key: 'k',
+ * meta: true,
+ * handler: () => openCommandPalette(),
+ * description: 'Open command palette',
+ * },
+ * ]);
+ * ```
+ */
+export function useKeyboard(shortcuts: KeyboardShortcut[]) {
+ useEffect(() => {
+ const handleKeyDown = (event: KeyboardEvent) => {
+ for (const shortcut of shortcuts) {
+ const keyMatches = event.key.toLowerCase() === shortcut.key.toLowerCase();
+ const ctrlMatches = shortcut.ctrl ? event.ctrlKey : !event.ctrlKey;
+ const shiftMatches = shortcut.shift ? event.shiftKey : !event.shiftKey;
+ const altMatches = shortcut.alt ? event.altKey : !event.altKey;
+ const metaMatches = shortcut.meta ? event.metaKey : !event.metaKey;
+
+ // On Windows/Linux, treat meta as ctrl for convenience
+ const modifierMatches = shortcut.meta
+ ? event.metaKey || (event.ctrlKey && !navigator.platform.includes('Mac'))
+ : metaMatches;
+
+ if (
+ keyMatches &&
+ ctrlMatches &&
+ shiftMatches &&
+ altMatches &&
+ modifierMatches
+ ) {
+ event.preventDefault();
+ shortcut.handler(event);
+ break;
+ }
+ }
+ };
+
+ window.addEventListener('keydown', handleKeyDown);
+
+ return () => {
+ window.removeEventListener('keydown', handleKeyDown);
+ };
+ }, [shortcuts]);
+}
+
+/**
+ * Hook to register a single keyboard shortcut (convenience wrapper)
+ */
+export function useKeyboardShortcut(
+ key: string,
+ handler: (event: KeyboardEvent) => void,
+ modifiers?: {
+ ctrl?: boolean;
+ shift?: boolean;
+ alt?: boolean;
+ meta?: boolean;
+ }
+) {
+ useKeyboard([
+ {
+ key,
+ ...modifiers,
+ handler,
+ },
+ ]);
+}
diff --git a/lib/stores/historyStore.ts b/lib/stores/historyStore.ts
new file mode 100644
index 0000000..e6fb3c5
--- /dev/null
+++ b/lib/stores/historyStore.ts
@@ -0,0 +1,68 @@
+import { create } from 'zustand';
+import { persist, createJSONStorage } from 'zustand/middleware';
+
+export interface ColorHistoryEntry {
+ color: string;
+ timestamp: number;
+}
+
+interface ColorHistoryState {
+ history: ColorHistoryEntry[];
+ addColor: (color: string) => void;
+ removeColor: (color: string) => void;
+ clearHistory: () => void;
+ getRecent: (limit?: number) => ColorHistoryEntry[];
+}
+
+/**
+ * Color history store with localStorage persistence
+ *
+ * Tracks up to 50 most recent colors with timestamps
+ * Automatically removes duplicates (keeps most recent)
+ * Persists across browser sessions
+ */
+export const useColorHistory = create()(
+ persist(
+ (set, get) => ({
+ history: [],
+
+ addColor: (color) => {
+ const normalizedColor = color.toLowerCase();
+ set((state) => {
+ // Remove existing entry if present
+ const filtered = state.history.filter(
+ (entry) => entry.color.toLowerCase() !== normalizedColor
+ );
+
+ // Add new entry at the beginning
+ const newHistory = [
+ { color: normalizedColor, timestamp: Date.now() },
+ ...filtered,
+ ].slice(0, 50); // Keep only 50 most recent
+
+ return { history: newHistory };
+ });
+ },
+
+ removeColor: (color) => {
+ const normalizedColor = color.toLowerCase();
+ set((state) => ({
+ history: state.history.filter(
+ (entry) => entry.color.toLowerCase() !== normalizedColor
+ ),
+ }));
+ },
+
+ clearHistory: () => set({ history: [] }),
+
+ getRecent: (limit = 10) => {
+ const { history } = get();
+ return history.slice(0, limit);
+ },
+ }),
+ {
+ name: 'pastel-color-history',
+ storage: createJSONStorage(() => localStorage),
+ }
+ )
+);