summaryrefslogtreecommitdiff
path: root/src/components/toolbar
diff options
context:
space:
mode:
Diffstat (limited to 'src/components/toolbar')
-rw-r--r--src/components/toolbar/ColorSwatch.tsx227
-rw-r--r--src/components/toolbar/Toolbar.tsx9
-rw-r--r--src/components/toolbar/ToolbarItem.tsx57
3 files changed, 293 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 = `| |
+| ⠀⠀⠀⠀⊹ |
+| ⢶⢻⣑⣒⢤⡀⠀⢄⠀⠀⡠⠀⢀⡤⣆⣊⡿⡷ |
+| ⠀⠹⠹⣚⣣⠻⣦⡀⠀⠀⢀⣴⠟⣸⢓⢎⠏⠀ |
+| ⠀⠀⢡⣱⣖⣢⡾⢿⣾⣷⡿⢷⣖⣒⣎⡎⠀⠀ |
+| ⠀⠀⠀⣠⠓⢬⠅⡺⢻⡟⢗⠨⡥⠚⣄⠀⠀⠀ |
+| ⠀⠀⠀⣿⡆⠘⠆⢇⢸⡇⠸⠰⠃⢰⣿⠀⠀⠀ |
+| ⠀⠀⠀⠐⡻⣮⣬⠞⠈⠁⠳⣤⣴⢿⠂⠀⠀⠀ |
+| ⠀⠀⠀⡜⠀⠁⠉⠀⠀⠀⠀⠈⠈⠀⢣⠀⠀⠀ |
+| ⊹ |
+| |`;
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<ToolbarProps> = ({ children }) => {
+ return <div className="toolbar">{children}</div>;
+};
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<ToolbarItemProps> = ({
+ 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 (
+ <div className={`toolbar-item ${disabled ? 'disabled' : ''}`} onClick={handleToggle}>
+ <div className='toolbar-item-content'>
+ {children}
+ </div>
+ {renderContent && isOpen && !disabled && (
+ <div
+ className={`toolbar-item-content-panel ${isFading ? 'fading' : ''}`}
+ onClick={(e) => e.stopPropagation()}
+ >
+ {renderContent(handleClose)}
+ </div>
+ )}
+ </div>
+ );
+};