# 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 (
)
}
render(, document.getElementById('app')!)
```
**Step 2: Run to verify it works**
Run: `bun run dev`
Expected: Visit http://localhost:3000, upload a spritesheet, see it animate, adjust values, copy code
**Step 3: Commit**
```bash
git add src/dev/app.tsx
git commit -m "feat: add dev tool client component with hono jsx"
```
---
### Task 6: Final Verification
**Step 1: Run all tests**
Run: `bun test`
Expected: All tests pass
**Step 2: Start dev server and verify**
Run: `bun run dev`
Expected: Server starts, UI loads, upload works, preview animates, code generates
**Step 3: Create final commit**
```bash
git add -A
git commit -m "chore: tiny-sprites v1 complete"
```