From de43eb05d2e43ab31effce3dcca62ad91a556b26 Mon Sep 17 00:00:00 2001 From: Elizabeth Hunt Date: Sun, 5 Oct 2025 16:42:02 -0700 Subject: Init --- src/App.tsx | 62 ++++++--- src/components/LoadScreen.tsx | 164 ++++++++++++++++++++++++ src/components/SaveModal.tsx | 79 ++++++++++++ src/components/grid/Cell.tsx | 11 +- src/components/grid/GridComponent.tsx | 37 +++++- src/components/toolbar/ColorSwatch.tsx | 227 +++++++++++++++++++++++++++++++++ src/components/toolbar/Toolbar.tsx | 9 ++ src/components/toolbar/ToolbarItem.tsx | 57 +++++++++ src/pages/Paint.tsx | 156 ++++++++++++++++------ src/styles/styles.css | 107 +++++++++++++++- src/utils/ansi.ts | 12 ++ src/utils/grid.ts | 83 +++++++++++- src/utils/storage.ts | 42 ++++++ 13 files changed, 983 insertions(+), 63 deletions(-) create mode 100644 src/components/LoadScreen.tsx create mode 100644 src/components/SaveModal.tsx create mode 100644 src/components/toolbar/ColorSwatch.tsx create mode 100644 src/components/toolbar/Toolbar.tsx create mode 100644 src/components/toolbar/ToolbarItem.tsx create mode 100644 src/utils/storage.ts (limited to 'src') diff --git a/src/App.tsx b/src/App.tsx index c50d1fc..dd3d76d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,28 +2,54 @@ import { useEffect, useState } from 'react'; import { ChooseArt } from '@/pages/ChooseArt'; import { Paint } from '@/pages/Paint'; +import { LoadScreen } from '@/components/LoadScreen'; -import { gridFromAscii } from '@/utils/grid'; - -const butterfly = `| | -| ⠀⠀⠀⠀⊹ | -| ⢶⢻⣑⣒⢤⡀⠀⢄⠀⠀⡠⠀⢀⡤⣆⣊⡿⡷ | -| ⠀⠹⠹⣚⣣⠻⣦⡀⠀⠀⢀⣴⠟⣸⢓⢎⠏⠀ | -| ⠀⠀⢡⣱⣖⣢⡾⢿⣾⣷⡿⢷⣖⣒⣎⡎⠀⠀ | -| ⠀⠀⠀⣠⠓⢬⠅⡺⢻⡟⢗⠨⡥⠚⣄⠀⠀⠀ | -| ⠀⠀⠀⣿⡆⠘⠆⢇⢸⡇⠸⠰⠃⢰⣿⠀⠀⠀ | -| ⠀⠀⠀⠐⡻⣮⣬⠞⠈⠁⠳⣤⣴⢿⠂⠀⠀⠀ | -| ⠀⠀⠀⡜⠀⠁⠉⠀⠀⠀⠀⠈⠈⠀⢣⠀⠀⠀ | -| ⊹ | -| |`; +import { gridFromAscii, gridFromAnsi } from '@/utils/grid'; +import type { Grid } from '@/types/grid'; export const App: React.FC = () => { - // const [chosenArt, setChosenArt] = useState(undefined); - const [chosenArt, setChosenArt] = useState(butterfly); + const [route, setRoute] = useState(window.location.hash || '#home'); + const [chosenArt, setChosenArt] = useState(null); + const [loadedGrid, setLoadedGrid] = useState(null); + + useEffect(() => { + const handleHashChange = () => { + setRoute(window.location.hash || '#home'); + }; + window.addEventListener('hashchange', handleHashChange); + return () => window.removeEventListener('hashchange', handleHashChange); + }, []); + + const handleLoad = (grid: Grid) => { + setLoadedGrid(grid); + window.location.hash = '#paint'; + }; + + const handleNew = () => { + setChosenArt(''); + window.location.hash = '#paint'; + }; + + const handlePaste = (ansiText: string) => { + const importedGrid = gridFromAnsi(ansiText); + setLoadedGrid(importedGrid); + window.location.hash = '#paint'; + }; + + const handleGoHome = () => { + setLoadedGrid(null); + setChosenArt(null); + window.location.hash = '#home'; + }; - if (chosenArt !== undefined) { - return ; + if (route === '#paint') { + if (loadedGrid !== null) { + return ; + } + if (chosenArt !== null) { + return ; + } } - return ; + return ; }; diff --git a/src/components/LoadScreen.tsx b/src/components/LoadScreen.tsx new file mode 100644 index 0000000..e51ff6a --- /dev/null +++ b/src/components/LoadScreen.tsx @@ -0,0 +1,164 @@ +import React, { useState } from 'react'; +import type { Grid } from '@/types/grid'; +import { getSavedArt, deleteSavedArt, type SavedArt } from '@/utils/storage'; +import { GridComponent } from './grid/GridComponent'; +import { gridFromAscii } from '@/utils/grid'; + +const demoArt = [ + { name: '🎨 Butterfly', art: `| | +| ⠀⠀⠀⠀⊹ | +| ⢶⢻⣑⣒⢤⡀⠀⢄⠀⠀⡠⠀⢀⡤⣆⣊⡿⡷ | +| ⠀⠹⠹⣚⣣⠻⣦⡀⠀⠀⢀⣴⠟⣸⢓⢎⠏⠀ | +| ⠀⠀⢡⣱⣖⣢⡾⢿⣾⣷⡿⢷⣖⣒⣎⡎⠀⠀ | +| ⠀⠀⠀⣠⠓⢬⠅⡺⢻⡟⢗⠨⡥⠚⣄⠀⠀⠀ | +| ⠀⠀⠀⣿⡆⠘⠆⢇⢸⡇⠸⠰⠃⢰⣿⠀⠀⠀ | +| ⠀⠀⠀⠐⡻⣮⣬⠞⠈⠁⠳⣤⣴⢿⠂⠀⠀⠀ | +| ⠀⠀⠀⡜⠀⠁⠉⠀⠀⠀⠀⠈⠈⠀⢣⠀⠀⠀ | +| ⊹ | +| |` }, + { name: '😊 Smiley', art: ` ████████ + ██ ██ + ██ ██ ██ ██ +██ ██ +██ ██ ██ ██ +██ ██ + ██ ██████████ ██ + ██ ██ + ████████ ` }, + { name: '🌟 Star', art: ` ★ + ███ + █████ + ███████ +█████████ + ███████ + █████ + ███ + █ ` }, +]; + +interface LoadScreenProps { + onLoad: (grid: Grid) => void; + onNew: () => void; + onPaste: (ansiText: string) => void; +} + +export const LoadScreen: React.FC = ({ onLoad, onNew, onPaste }) => { + const [saves] = useState(getSavedArt()); + const [pasteText, setPasteText] = useState(''); + + const handleDemoLoad = (art: string) => { + const grid = gridFromAscii(art); + onLoad(grid); + }; + + const formatDate = (timestamp: number) => { + const date = new Date(timestamp); + return date.toLocaleString(); + }; + + const handleDelete = (id: string, e: React.MouseEvent) => { + e.stopPropagation(); + deleteSavedArt(id); + window.location.reload(); + }; + + const handlePaste = () => { + if (pasteText.trim()) { + onPaste(pasteText.trim()); + } + }; + + return ( +
+

ANSI Color Paint

+ +
+

Demo Templates

+
+ {demoArt.map((demo, idx) => ( + + ))} +
+
+ +
+

Recent Saves

+ {saves.length > 0 ? ( +
+ {saves.map((save) => ( +
onLoad(save.grid)} + style={{ + border: '2px solid var(--regular3)', + borderRadius: '4px', + padding: '1rem', + cursor: 'pointer', + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + transition: 'border-color 0.2s', + }} + onMouseEnter={(e) => { + e.currentTarget.style.borderColor = 'var(--regular6)'; + }} + onMouseLeave={(e) => { + e.currentTarget.style.borderColor = 'var(--regular3)'; + }} + > +
+
+ {save.name} +
+
+ {formatDate(save.timestamp)} +
+
+ +
+ ))} +
+ ) : ( +

No saves yet

+ )} +
+ +
+

Import from ANSI Text

+