# Tiny Sprites Implementation Plan > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. **Goal:** Build a CSS-based sprite library with Hono JSX component and dev tuning server. **Architecture:** A `` component renders a div with inline styles and a scoped `@keyframes` animation. The dev server provides a UI using Hono's client-side JSX to load spritesheets, adjust parameters, and copy generated code. **Tech Stack:** Bun, Hono, Hono JSX (server + client), Pico CSS --- ### Task 1: Project Setup **Files:** - Create: `package.json` - Create: `tsconfig.json` **Step 1: Create package.json** ```json { "name": "tiny-sprites", "type": "module", "exports": { ".": "./src/sprite.tsx" }, "bin": { "tiny-sprites": "src/dev/server.tsx" }, "scripts": { "dev": "bun --hot src/dev/server.tsx", "test": "bun test" }, "dependencies": { "hono": "^4" }, "devDependencies": { "bun-types": "latest" } } ``` **Step 2: Create tsconfig.json** ```json { "compilerOptions": { "target": "ESNext", "module": "ESNext", "moduleResolution": "bundler", "jsx": "react-jsx", "jsxImportSource": "hono/jsx", "strict": true, "skipLibCheck": true, "types": ["bun-types"] }, "include": ["src/**/*"] } ``` **Step 3: Install dependencies** Run: `bun install` Expected: Resolves hono and bun-types **Step 4: Commit** ```bash git add package.json tsconfig.json bun.lockb git commit -m "chore: project setup with bun and hono" ``` --- ### Task 2: Download Pico CSS **Files:** - Create: `public/pico.min.css` **Step 1: Download pico.min.css** Run: `mkdir -p public && curl -o public/pico.min.css https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css` Expected: File downloaded to public/pico.min.css **Step 2: Commit** ```bash git add public/pico.min.css git commit -m "chore: add pico css" ``` --- ### Task 3: Sprite Component **Files:** - Create: `src/sprite.tsx` - Create: `src/sprite.test.tsx` **Step 1: Write the failing test** ```tsx import { test, expect } from "bun:test" import { Sprite } from "./sprite" test("Sprite renders div with correct dimensions", () => { const html = ().toString() expect(html).toContain('width:32px') expect(html).toContain('height:32px') }) test("Sprite sets background-image", () => { const html = ().toString() expect(html).toContain("background-image:url('/warrior.png')") }) test("Sprite calculates background-size for horizontal strip", () => { const html = ().toString() expect(html).toContain('background-size:128px 32px') }) test("Sprite generates keyframes animation", () => { const html = ().toString() expect(html).toContain('@keyframes sprite-') expect(html).toContain('steps(4)') expect(html).toContain('400ms') }) test("Sprite keyframes animate background-position", () => { const html = ().toString() expect(html).toContain('from{background-position:0 0}') expect(html).toContain('to{background-position:-128px 0}') }) test("Sprite with columns calculates grid background-size", () => { const html = ().toString() expect(html).toContain('background-size:128px 64px') }) test("Sprite with columns generates grid keyframes", () => { const html = ().toString() expect(html).toContain('0%{background-position:0px 0px}') expect(html).toContain('25%{background-position:-32px 0px}') expect(html).toContain('50%{background-position:0px -32px}') expect(html).toContain('75%{background-position:-32px -32px}') }) test("Sprite passes through class", () => { const html = ().toString() expect(html).toContain('class="my-sprite"') }) test("Sprite passes through style", () => { const html = ().toString() expect(html).toContain('opacity:0.5') }) test("Sprite pauses when playing is false", () => { const html = ().toString() expect(html).toContain('animation-play-state:paused') }) ``` **Step 2: Run test to verify it fails** Run: `bun test src/sprite.test.tsx` Expected: FAIL with "Cannot find module" **Step 3: Write implementation** ```tsx type SpriteProps = { src: string width: number height: number frames: number frameDuration: number columns?: number class?: string style?: string playing?: boolean } export const Sprite = ({ src, width, height, frames, frameDuration, columns, class: className, style, playing = true, }: SpriteProps) => { const isGrid = columns !== undefined const rows = isGrid ? Math.ceil(frames / columns!) : 1 const cols = isGrid ? columns! : frames const sheetWidth = cols * width const sheetHeight = rows * height const totalDuration = frames * frameDuration const keyframeId = `sprite-${Bun.hash(`${src}-${width}-${height}-${frames}-${frameDuration}-${columns}`).toString(36)}` const keyframes = isGrid ? generateGridKeyframes(keyframeId, width, height, frames, columns!) : generateStripKeyframes(keyframeId, sheetWidth) const divStyle = [ `width:${width}px`, `height:${height}px`, `background-image:url('${src}')`, `background-size:${sheetWidth}px ${sheetHeight}px`, `animation:${keyframeId} ${totalDuration}ms steps(${frames}) infinite`, playing ? '' : 'animation-play-state:paused', style, ].filter(Boolean).join(';') return ( <>
) } const generateStripKeyframes = (id: string, sheetWidth: number): string => `@keyframes ${id}{from{background-position:0 0}to{background-position:-${sheetWidth}px 0}}` const generateGridKeyframes = (id: string, width: number, height: number, frames: number, columns: number): string => { const steps: string[] = [] for (let i = 0; i < frames; i++) { const col = i % columns const row = Math.floor(i / columns) const x = -col * width const y = -row * height const percent = (i / frames) * 100 steps.push(`${percent}%{background-position:${x}px ${y}px}`) } return `@keyframes ${id}{${steps.join('')}}` } ``` **Step 4: Run test to verify it passes** Run: `bun test src/sprite.test.tsx` Expected: 10 passing tests **Step 5: Commit** ```bash git add src/sprite.tsx src/sprite.test.tsx git commit -m "feat: add Sprite component" ``` --- ### Task 4: Dev Server **Files:** - Create: `src/dev/server.tsx` - Create: `src/dev/index.html` **Step 1: Create dev server** ```tsx #!/usr/bin/env bun import { Hono } from "hono" import { serveStatic } from "hono/bun" import index from "./index.html" const app = new Hono() app.use("/public/*", serveStatic({ root: "./" })) app.get("/", (c) => c.html(index)) export default { port: 3000, fetch: app.fetch, } ``` **Step 2: Create index.html** ```html Tiny Sprites

Tiny Sprites

``` **Step 3: Commit** ```bash git add src/dev/server.tsx src/dev/index.html git commit -m "feat: add dev server" ``` --- ### Task 5: Dev Tool Client Component **Files:** - Create: `src/dev/app.tsx` **Step 1: Create client component using Hono JSX** ```tsx import { useState } from "hono/jsx" import { render } from "hono/jsx/dom" const App = () => { const [spriteUrl, setSpriteUrl] = useState('') const [width, setWidth] = useState(32) const [height, setHeight] = useState(32) const [frames, setFrames] = useState(4) const [columns, setColumns] = useState() const [frameDuration, setFrameDuration] = useState(100) const handleFileChange = (e: Event) => { const file = (e.target as HTMLInputElement).files?.[0] if (!file) return setSpriteUrl(URL.createObjectURL(file)) } const isGrid = columns !== undefined const cols = isGrid ? columns : frames const rows = isGrid ? Math.ceil(frames / columns!) : 1 const sheetWidth = cols * width const sheetHeight = rows * height const totalDuration = frames * frameDuration const keyframeId = 'sprite-preview' let keyframes: string if (isGrid) { const steps: string[] = [] for (let i = 0; i < frames; i++) { const col = i % columns! const row = Math.floor(i / columns!) const x = -col * width const y = -row * height const percent = (i / frames) * 100 steps.push(`${percent}%{background-position:${x}px ${y}px}`) } keyframes = `@keyframes ${keyframeId}{${steps.join('')}}` } else { keyframes = `@keyframes ${keyframeId}{from{background-position:0 0}to{background-position:-${sheetWidth}px 0}}` } const previewStyle = { width: `${width}px`, height: `${height}px`, backgroundImage: `url('${spriteUrl}')`, backgroundSize: `${sheetWidth}px ${sheetHeight}px`, animation: `${keyframeId} ${totalDuration}ms steps(${frames}) infinite`, } const columnsAttr = columns ? `\n columns={${columns}}` : '' const code = `` const copyCode = () => navigator.clipboard.writeText(code) return (
{spriteUrl ? ( <>
) : ( Upload a spritesheet to preview )}