diff options
Diffstat (limited to 'src/components/toolbar/ColorSwatch.tsx')
| -rw-r--r-- | src/components/toolbar/ColorSwatch.tsx | 227 |
1 files changed, 227 insertions, 0 deletions
diff --git a/src/components/toolbar/ColorSwatch.tsx b/src/components/toolbar/ColorSwatch.tsx new file mode 100644 index 0000000..6f33483 --- /dev/null +++ b/src/components/toolbar/ColorSwatch.tsx @@ -0,0 +1,227 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import type { AnsiTermColor, Grid } from '@/types/grid'; +import { hexToAnsi } from '@/utils/ansi'; +import { gridFromAscii } from '@/utils/grid'; +import { HexColorPicker } from 'react-colorful'; +import { GridComponent } from '../grid/GridComponent'; + +interface AnsiColorSwatchProps { + onSelect: (color: AnsiTermColor) => void; + defaultColors: AnsiTermColor[]; + onClose?: () => void; +} + +const STORAGE_KEY = 'ansicolor-history'; + +export const ColorSwatch: React.FC<AnsiColorSwatchProps> = ({ + onSelect, + defaultColors, + onClose, +}) => { + const [foregroundColor, setForegroundColor] = useState<string | null>(null); + const [backgroundColor, setBackgroundColor] = useState<string | null>(null); + + // Initialize history from localStorage or defaults + const [history, setHistory] = useState<AnsiTermColor[]>(() => { + const savedHistory = localStorage.getItem(STORAGE_KEY); + if (savedHistory) { + try { + return JSON.parse(savedHistory); + } catch (e) { + console.error( + 'Failed to parse color history from localStorage', + e, + ); + } + } + return defaultColors; + }); + useEffect(() => { + localStorage.setItem(STORAGE_KEY, JSON.stringify(history)); + }, [history]); + + const [selectedHistory, setSelectedHistory] = useState<number>(0); + + const handleHistorySelection = (selected: number) => { + const color = history.at(selected); + if (color === undefined) return; + setSelectedHistory(selected); + + // Update the hex color pickers + if (color.foreground) { + const hex = `#${[color.foreground.r, color.foreground.g, color.foreground.b] + .map(c => Math.round(c * 51).toString(16).padStart(2, '0')) + .join('')}`; + setForegroundColor(hex); + } else { + setForegroundColor(null); + } + + if (color.background) { + const hex = `#${[color.background.r, color.background.g, color.background.b] + .map(c => Math.round(c * 51).toString(16).padStart(2, '0')) + .join('')}`; + setBackgroundColor(hex); + } else { + setBackgroundColor(null); + } + + onSelect(color); + }; + + const handleColorSelect = () => { + // Only add to history if it has at least one non-null color + const hasColor = ansiColor.foreground !== null || ansiColor.background !== null; + + if (hasColor) { + // Add to history if not already present + const colorExists = history.some( + (c) => + JSON.stringify(c.foreground) === + JSON.stringify(ansiColor.foreground) && + JSON.stringify(c.background) === + JSON.stringify(ansiColor.background), + ); + if (!colorExists) { + setHistory((prev) => [ansiColor, ...prev]); + } + } + + onSelect(ansiColor); + }; + + const ansiColor = useMemo((): AnsiTermColor => { + const [fg, bg] = [foregroundColor, backgroundColor].map((h) => + h ? hexToAnsi(h) : null, + ); + return { foreground: fg, background: bg }; + }, [foregroundColor, backgroundColor]); + const previewGrid = useMemo( + (): Grid => gridFromAscii(butterfly, ansiColor), + [ansiColor], + ); + + return ( + <div> + <div style={{ marginBottom: '0.75rem' }}> + <div style={{ display: 'flex', gap: '0.35rem', flexWrap: 'wrap', maxWidth: '350px', justifyContent: 'center' }}> + {history.map((color, idx) => { + const hasForeground = color.foreground !== null; + const hasBackground = color.background !== null; + + return ( + <div + key={idx} + onClick={() => handleHistorySelection(idx)} + style={{ + cursor: 'pointer', + border: + selectedHistory === idx + ? '2px solid var(--regular6)' + : '1px solid var(--regular3)', + borderRadius: '3px', + padding: '4px', + width: '28px', + height: '28px', + position: 'relative', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + fontSize: '16px', + lineHeight: 1, + overflow: 'hidden', + }} + > + {hasForeground && hasBackground ? ( + <> + <div style={{ + position: 'absolute', + top: 0, + left: 0, + width: '100%', + height: '50%', + backgroundColor: `rgb(${color.foreground!.r * 51}, ${color.foreground!.g * 51}, ${color.foreground!.b * 51})`, + }} /> + <div style={{ + position: 'absolute', + bottom: 0, + left: 0, + width: '100%', + height: '50%', + backgroundColor: `rgb(${color.background!.r * 51}, ${color.background!.g * 51}, ${color.background!.b * 51})`, + }} /> + </> + ) : hasForeground ? ( + <div style={{ + width: '100%', + height: '100%', + backgroundColor: `rgb(${color.foreground!.r * 51}, ${color.foreground!.g * 51}, ${color.foreground!.b * 51})`, + }} /> + ) : hasBackground ? ( + <div style={{ + width: '100%', + height: '100%', + backgroundColor: `rgb(${color.background!.r * 51}, ${color.background!.g * 51}, ${color.background!.b * 51})`, + }} /> + ) : ( + <div style={{ + width: '100%', + height: '100%', + background: 'linear-gradient(135deg, transparent 45%, var(--regular3) 45%, var(--regular3) 55%, transparent 55%)', + }} /> + )} + </div> + ); + })} + </div> + </div> + <div + style={{ + display: 'flex', + flexDirection: 'row', + marginBottom: '1rem', + }} + > + <div style={{ padding: '1rem' }}> + <HexColorPicker + color={foregroundColor ?? ''} + onChange={setForegroundColor} + /> + <br /> + <button onClick={() => setForegroundColor(null)}> + clear + </button> + </div> + <div style={{ padding: '1rem' }}> + <HexColorPicker + color={backgroundColor ?? ''} + onChange={setBackgroundColor} + /> + <br /> + <button onClick={() => setBackgroundColor(null)}> + clear + </button> + </div> + </div> + <button + style={{ marginBottom: '1rem' }} + onClick={handleColorSelect} + > + use + </button> + <GridComponent onCellInteract={undefined} grid={previewGrid} /> + </div> + ); +}; + +const butterfly = `| | +| ⠀⠀⠀⠀⊹ | +| ⢶⢻⣑⣒⢤⡀⠀⢄⠀⠀⡠⠀⢀⡤⣆⣊⡿⡷ | +| ⠀⠹⠹⣚⣣⠻⣦⡀⠀⠀⢀⣴⠟⣸⢓⢎⠏⠀ | +| ⠀⠀⢡⣱⣖⣢⡾⢿⣾⣷⡿⢷⣖⣒⣎⡎⠀⠀ | +| ⠀⠀⠀⣠⠓⢬⠅⡺⢻⡟⢗⠨⡥⠚⣄⠀⠀⠀ | +| ⠀⠀⠀⣿⡆⠘⠆⢇⢸⡇⠸⠰⠃⢰⣿⠀⠀⠀ | +| ⠀⠀⠀⠐⡻⣮⣬⠞⠈⠁⠳⣤⣴⢿⠂⠀⠀⠀ | +| ⠀⠀⠀⡜⠀⠁⠉⠀⠀⠀⠀⠈⠈⠀⢣⠀⠀⠀ | +| ⊹ | +| |`; |
