summaryrefslogtreecommitdiff
path: root/src/pages
diff options
context:
space:
mode:
authorElizabeth Hunt <me@liz.coffee>2025-10-05 16:42:02 -0700
committerElizabeth Hunt <me@liz.coffee>2025-10-05 23:11:41 -0700
commitde43eb05d2e43ab31effce3dcca62ad91a556b26 (patch)
tree47a62b61bfc97dda639dea70627ecf3005ba7b02 /src/pages
parent35add63ec4dce39710095f17abd86777de9e5b49 (diff)
downloadansicolor-main.tar.gz
ansicolor-main.zip
Diffstat (limited to 'src/pages')
-rw-r--r--src/pages/Paint.tsx156
1 files changed, 119 insertions, 37 deletions
diff --git a/src/pages/Paint.tsx b/src/pages/Paint.tsx
index 136a347..995bcc3 100644
--- a/src/pages/Paint.tsx
+++ b/src/pages/Paint.tsx
@@ -1,16 +1,34 @@
import { GridComponent } from '@/components/grid/GridComponent';
+import { ColorSwatch } from '@/components/toolbar/ColorSwatch';
+import { Toolbar } from '@/components/toolbar/Toolbar';
+import { ToolbarItem } from '@/components/toolbar/ToolbarItem';
+import { SaveModal } from '@/components/SaveModal';
import type { AnsiTermColor, Grid, GridCell } from '@/types/grid';
-import {
- type IZipper,
- ListZipper,
-} from '@emprespresso/pengueno';
-import { useCallback, useState } from 'react';
+import { gridToAnsi } from '@/utils/grid';
+import { saveArt } from '@/utils/storage';
+import { type IZipper, ListZipper } from '@emprespresso/pengueno';
+import { useCallback, useEffect, useState } from 'react';
+import { flushSync } from 'react-dom';
export interface ChooseArtProps {
grid: Grid;
+ onGoHome?: () => void;
}
-export const Paint: React.FC<ChooseArtProps> = ({ grid }) => {
+// Gruvbox theme colors converted to ANSI RGB values
+const defaultColors: AnsiTermColor[] = [
+ { foreground: { r: 5, g: 5, b: 5 }, background: null }, // bright7 #ebdbb2
+ { foreground: { r: 5, g: 1, b: 1 }, background: null }, // regular1 #cc241d
+ { foreground: { r: 4, g: 4, b: 1 }, background: null }, // regular2 #98971a
+ { foreground: { r: 5, g: 4, b: 1 }, background: null }, // regular3 #d79921
+ { foreground: { r: 2, g: 3, b: 4 }, background: null }, // regular4 #458588
+ { foreground: { r: 4, g: 2, b: 3 }, background: null }, // regular5 #b16286
+ { foreground: { r: 3, g: 4, b: 3 }, background: null }, // regular6 #689d6a
+ { foreground: { r: 4, g: 4, b: 3 }, background: null }, // regular7 #a89984
+];
+
+export const Paint: React.FC<ChooseArtProps> = ({ grid, onGoHome }) => {
+ const [showSaveModal, setShowSaveModal] = useState(false);
const [selectedColor, setSelectedColor] = useState<AnsiTermColor>({
foreground: { r: 5, g: 5, b: 5 },
background: null,
@@ -18,49 +36,113 @@ export const Paint: React.FC<ChooseArtProps> = ({ grid }) => {
const [history, setHistory] = useState<IZipper<Grid>>(
ListZipper.from([grid]),
);
+ const [isDragging, setIsDragging] = useState(false);
+ const [workingGrid, setWorkingGrid] = useState<Grid>(grid);
+
+ const handleDragStart = useCallback(() => {
+ setIsDragging(true);
+ // Don't reset workingGrid here - let it accumulate changes
+ }, []);
+
+ const handleDragEnd = useCallback(() => {
+ if (isDragging) {
+ // Commit the working grid to history as a single atomic operation
+ setHistory((currentHistory) => {
+ return currentHistory.prepend(workingGrid).previous().get();
+ });
+ setIsDragging(false);
+ }
+ }, [isDragging, workingGrid]);
const cellInteractionCallback = useCallback(
(cell: GridCell) => {
- setHistory((currentHistory) => {
- const currentGrid = currentHistory.read().get();
- const newGrid = currentGrid.map((row) => [...row]); // Deep copy for current state
- newGrid[cell.y][cell.x] = { ...cell, color: selectedColor };
- return currentHistory.prepend(newGrid).previous().get();
+ flushSync(() => {
+ setWorkingGrid((currentGrid) => {
+ const newGrid = currentGrid.map((row) => [...row]);
+ newGrid[cell.y][cell.x] = { ...cell, color: selectedColor };
+ return newGrid;
+ });
});
},
[selectedColor],
);
+ const handleSave = (name: string) => {
+ const currentGrid = history.read().get();
+ saveArt(name, currentGrid);
+ };
+
+ const currentGrid = workingGrid;
+ const ansiOutput = gridToAnsi(currentGrid);
+
return (
- <div>
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
+ {showSaveModal && (
+ <SaveModal
+ ansiOutput={ansiOutput}
+ onSave={handleSave}
+ onClose={() => setShowSaveModal(false)}
+ />
+ )}
+ <Toolbar>
+ <ToolbarItem
+ renderContent={(onClose) => (
+ <ColorSwatch
+ onSelect={setSelectedColor}
+ onClose={onClose}
+ defaultColors={defaultColors}
+ ></ColorSwatch>
+ )}
+ >
+ 🎨
+ </ToolbarItem>
+ <ToolbarItem
+ disabled={
+ !history
+ .previous()
+ .flatMap((it) => it.read())
+ .present()
+ }
+ onClick={() => {
+ setHistory((history) => {
+ const newHistory = history.previous().get();
+ setWorkingGrid(newHistory.read().get());
+ return newHistory;
+ });
+ }}
+ >
+ ↷
+ </ToolbarItem>
+ <ToolbarItem
+ disabled={
+ !history
+ .next()
+ .flatMap((it) => it.read())
+ .present()
+ }
+ onClick={() => {
+ setHistory((history) => {
+ const newHistory = history.next().get();
+ setWorkingGrid(newHistory.read().get());
+ return newHistory;
+ });
+ }}
+ >
+ ↶
+ </ToolbarItem>
+ <ToolbarItem onClick={() => setShowSaveModal(true)}>
+ 💾
+ </ToolbarItem>
+ <ToolbarItem onClick={onGoHome}>
+ 🏠
+ </ToolbarItem>
+ </Toolbar>
<GridComponent
- grid={history.read().get()}
+ grid={currentGrid}
onCellInteract={cellInteractionCallback}
+ onDragStart={handleDragStart}
+ onDragEnd={handleDragEnd}
/>
- <button
- disabled={
- !history
- .next()
- .flatMap((it) => it.read())
- .present()
- }
- onClick={() => setHistory((history) => history.next().get())}
- >
- Undo
- </button>
- <button
- disabled={
- !history
- .previous()
- .flatMap((it) => it.read())
- .present()
- }
- onClick={() =>
- setHistory((history) => history.previous().get())
- }
- >
- Redo
- </button>
</div>
);
};