From de43eb05d2e43ab31effce3dcca62ad91a556b26 Mon Sep 17 00:00:00 2001 From: Elizabeth Hunt Date: Sun, 5 Oct 2025 16:42:02 -0700 Subject: Init --- src/components/toolbar/ColorSwatch.tsx | 227 +++++++++++++++++++++++++++++++++ src/components/toolbar/Toolbar.tsx | 9 ++ src/components/toolbar/ToolbarItem.tsx | 57 +++++++++ 3 files changed, 293 insertions(+) create mode 100644 src/components/toolbar/ColorSwatch.tsx create mode 100644 src/components/toolbar/Toolbar.tsx create mode 100644 src/components/toolbar/ToolbarItem.tsx (limited to 'src/components/toolbar') 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 = ({ + onSelect, + defaultColors, + onClose, +}) => { + const [foregroundColor, setForegroundColor] = useState(null); + const [backgroundColor, setBackgroundColor] = useState(null); + + // Initialize history from localStorage or defaults + const [history, setHistory] = useState(() => { + 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(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 ( +
+
+
+ {history.map((color, idx) => { + const hasForeground = color.foreground !== null; + const hasBackground = color.background !== null; + + return ( +
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 ? ( + <> +
+
+ + ) : hasForeground ? ( +
+ ) : hasBackground ? ( +
+ ) : ( +
+ )} +
+ ); + })} +
+
+
+
+ +
+ +
+
+ +
+ +
+
+ + +
+ ); +}; + +const butterfly = `| | +| ⠀⠀⠀⠀⊹ | +| ⢶⢻⣑⣒⢤⡀⠀⢄⠀⠀⡠⠀⢀⡤⣆⣊⡿⡷ | +| ⠀⠹⠹⣚⣣⠻⣦⡀⠀⠀⢀⣴⠟⣸⢓⢎⠏⠀ | +| ⠀⠀⢡⣱⣖⣢⡾⢿⣾⣷⡿⢷⣖⣒⣎⡎⠀⠀ | +| ⠀⠀⠀⣠⠓⢬⠅⡺⢻⡟⢗⠨⡥⠚⣄⠀⠀⠀ | +| ⠀⠀⠀⣿⡆⠘⠆⢇⢸⡇⠸⠰⠃⢰⣿⠀⠀⠀ | +| ⠀⠀⠀⠐⡻⣮⣬⠞⠈⠁⠳⣤⣴⢿⠂⠀⠀⠀ | +| ⠀⠀⠀⡜⠀⠁⠉⠀⠀⠀⠀⠈⠈⠀⢣⠀⠀⠀ | +| ⊹ | +| |`; diff --git a/src/components/toolbar/Toolbar.tsx b/src/components/toolbar/Toolbar.tsx new file mode 100644 index 0000000..b2217e0 --- /dev/null +++ b/src/components/toolbar/Toolbar.tsx @@ -0,0 +1,9 @@ +import React from 'react'; + +export interface ToolbarProps { + children?: React.ReactNode; +} + +export const Toolbar: React.FC = ({ children }) => { + return
{children}
; +}; diff --git a/src/components/toolbar/ToolbarItem.tsx b/src/components/toolbar/ToolbarItem.tsx new file mode 100644 index 0000000..76bd615 --- /dev/null +++ b/src/components/toolbar/ToolbarItem.tsx @@ -0,0 +1,57 @@ +import React, { useState } from 'react'; + +export interface ToolbarItemProps { + children: React.ReactNode; + renderContent?: (onClose: () => void) => React.ReactNode; + onClick?: () => void; + disabled?: boolean; +} + +export const ToolbarItem: React.FC = ({ + children, + renderContent, + onClick, + disabled = false, +}) => { + const [isOpen, setIsOpen] = useState(false); + const [isFading, setIsFading] = useState(false); + + const handleClose = () => { + setIsFading(true); + setTimeout(() => { + setIsOpen(false); + setIsFading(false); + }, 200); + }; + + const handleToggle = (e: React.MouseEvent) => { + e.stopPropagation(); + if (disabled) return; + + if (renderContent) { + if (isOpen) { + handleClose(); + } else { + setIsOpen(true); + } + } + + onClick?.(); + }; + + return ( +
+
+ {children} +
+ {renderContent && isOpen && !disabled && ( +
e.stopPropagation()} + > + {renderContent(handleClose)} +
+ )} +
+ ); +}; -- cgit v1.2.3-70-g09d2