From 66ceb8ef0e77dedee8ec6b32cd97c157a8065d64 Mon Sep 17 00:00:00 2001 From: Cayman Date: Wed, 18 Mar 2026 09:24:53 -0400 Subject: [PATCH 01/37] docs: add design spec for complete app overhaul Co-Authored-By: Claude Opus 4.6 (1M context) --- .../2026-03-18-complete-overhaul-design.md | 134 ++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 docs/superpowers/specs/2026-03-18-complete-overhaul-design.md diff --git a/docs/superpowers/specs/2026-03-18-complete-overhaul-design.md b/docs/superpowers/specs/2026-03-18-complete-overhaul-design.md new file mode 100644 index 0000000..8691eb3 --- /dev/null +++ b/docs/superpowers/specs/2026-03-18-complete-overhaul-design.md @@ -0,0 +1,134 @@ +# simpleserialize.com Complete Overhaul + +## Summary + +Modernize simpleserialize.com from a clunky form-based SSZ tool into a polished, reactive developer workbench. Replace the entire tech stack, redesign the UI, and add an interactive structure view for SSZ data visualization. + +## Tech Stack + +### Replacing +| Old | New | Why | +|-----|-----|-----| +| Webpack 5 + Babel + loaders | Vite | Zero-config, fast HMR, native ESM | +| React 17 (class components) | React 19 (hooks) | Modern patterns, smaller bundles | +| Bulma + SCSS | Tailwind CSS 4 | Utility-first, better custom design | +| `threads` library | `comlink` | Tiny, modern worker RPC | +| `react-alert` + `react-loading-overlay` + `react-spinners` | `sonner` | Single modern toast library | +| `eyzy-tree` | Custom tree component | Unused lib, need custom behavior | +| `file-saver` | Native blob download | No library needed | +| `bn.js`, `core-js` | Native BigInt, modern browser APIs | No polyfills needed | + +### Keeping (updated to latest) +- `@chainsafe/ssz` — core SSZ library +- `@lodestar/types` — Ethereum consensus types +- `js-yaml` — YAML parsing +- TypeScript — latest version + +## UI Design + +### Layout +Side-by-side workbench: input panel (left), output + structure view (right). + +### Header +- Title: "SSZ Playground" +- Fork selector and SSZ type selector always visible in header bar +- Clean, minimal + +### Input Panel (left) +- Format tabs: YAML | JSON | Hex +- Monospace textarea/code area for data entry +- Action bar: Upload file, Generate random value +- Live processing — no submit button, debounced reactive updates + +### Output Panel (right top) +- Mode tabs: Serialize | Deserialize (determines direction of processing) +- Format tabs for output: Hex | Base64 (serialize) or YAML | JSON (deserialize) +- HashTreeRoot displayed when serializing +- Action bar: Copy to clipboard, Download file +- Read-only monospace display + +### Structure View (right bottom) +- Interactive collapsible tree of the SSZ type structure +- Each node shows: field name, type annotation, current value +- Color-coded by SSZ type category: + - Blue: uint types + - Green: bytes/ByteVector/ByteList + - Purple: containers + - Orange: lists/vectors + - Gray: boolean/bit types +- Click to expand/collapse containers and collections +- Generalized index shown on hover +- Subtree highlighting on node selection + +### Visual Design +- Dark theme (slate-900/slate-950 background) +- Blue/cyan accent color for interactive elements +- Monospace font (JetBrains Mono or system monospace) for all data +- Sans-serif (Inter or system) for UI labels +- Clean panel borders with subtle separation +- Desktop-first, responsive down to tablet + +## Architecture + +``` +src/ +├── app.tsx # Root layout, lifted state +├── main.tsx # Entry point, render root +├── index.html # Vite HTML +├── components/ +│ ├── header.tsx # Title + fork/type selectors +│ ├── footer.tsx # Credits, versions, links +│ ├── input-panel.tsx # Input editor + format tabs + actions +│ ├── output-panel.tsx # Output display + format tabs + actions +│ ├── structure-view/ +│ │ ├── structure-view.tsx # Tree container +│ │ ├── tree-node.tsx # Recursive node renderer +│ │ └── utils.ts # Node expansion, type color, generalized index +│ ├── format-tabs.tsx # Reusable tab bar component +│ └── ui/ +│ ├── copy-button.tsx # Copy to clipboard with feedback +│ └── file-upload.tsx # File upload handler +├── hooks/ +│ ├── use-ssz.ts # Orchestrates serialize/deserialize via worker +│ ├── use-worker.ts # Comlink worker lifecycle +│ └── use-debounce.ts # Debounced value for live processing +├── workers/ +│ └── ssz-worker.ts # Comlink-exposed: serialize, deserialize, randomValue +├── lib/ +│ ├── types.ts # Fork definitions, type registry, typeNames() +│ ├── formats.ts # Input/output format parse/dump +│ └── yaml.ts # Custom YAML schema for BigInt etc. +└── index.css # Tailwind directives + minimal custom styles +``` + +### State Flow +1. User selects fork + SSZ type (header) → stored in app state +2. User enters data in input panel → debounced, sent to worker +3. Worker serializes/deserializes → returns result + parsed structure +4. Output panel displays formatted result +5. Structure view renders parsed type tree + +### Worker Design +Single web worker exposed via Comlink with three methods: +- `serialize(typeName, forkName, input, inputFormat)` → `{ serialized, hashTreeRoot }` +- `deserialize(typeName, forkName, data)` → `{ deserialized }` +- `randomValue(typeName, forkName)` → `{ value }` + +### Error Handling +- Parse errors shown inline below input (not toast/alert) +- Worker errors shown inline in output panel +- Toast notifications only for user actions (copied, downloaded, etc.) + +## What Gets Deleted +- All class components (rewritten as functional) +- webpack.config.js, .babelrc, .prettierrc.js +- All Bulma/SCSS files +- node_modules packages: threads, threads-webpack-plugin, workerize-loader, react-alert, react-alert-template-basic, react-loading-overlay, react-spinners, eyzy-tree, file-saver, bn.js, core-js, all webpack/babel dev deps +- TreeView.tsx (unused, replaced by structure-view) +- ForkMe.tsx (GitHub link moves to footer) + +## Migration Notes +- The `lib/types.ts` module preserves the fork/type registry logic from `util/types.ts` +- The `lib/yaml.ts` module preserves custom YAML schema from `util/yaml/` +- The `lib/formats.ts` module consolidates `util/input_types.ts` and `util/output_types.ts` +- Worker logic from `components/worker/` is simplified into `workers/ssz-worker.ts` From c988636f420be7a194397b2014245e871184d571 Mon Sep 17 00:00:00 2001 From: Cayman Date: Wed, 18 Mar 2026 09:28:09 -0400 Subject: [PATCH 02/37] docs: update design spec with review feedback Address critical gaps: patchSszTypes preservation, worker data contracts, mode-specific input behavior, structure view data source, dead YAML schema code, and biome config retention. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../2026-03-18-complete-overhaul-design.md | 41 +++++++++++++------ 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/docs/superpowers/specs/2026-03-18-complete-overhaul-design.md b/docs/superpowers/specs/2026-03-18-complete-overhaul-design.md index 8691eb3..32c410a 100644 --- a/docs/superpowers/specs/2026-03-18-complete-overhaul-design.md +++ b/docs/superpowers/specs/2026-03-18-complete-overhaul-design.md @@ -21,7 +21,8 @@ Modernize simpleserialize.com from a clunky form-based SSZ tool into a polished, ### Keeping (updated to latest) - `@chainsafe/ssz` — core SSZ library - `@lodestar/types` — Ethereum consensus types -- `js-yaml` — YAML parsing +- `js-yaml` — YAML parsing (default schema only; custom int.js/schema.js are dead code and will be removed) +- `@biomejs/biome` + `@chainsafe/biomejs-config` — linting (already modern, keep as-is) - TypeScript — latest version ## UI Design @@ -32,12 +33,13 @@ Side-by-side workbench: input panel (left), output + structure view (right). ### Header - Title: "SSZ Playground" - Fork selector and SSZ type selector always visible in header bar +- Consensus spec version badge (links to Ethereum spec) - Clean, minimal ### Input Panel (left) -- Format tabs: YAML | JSON | Hex +- Format tabs: YAML | JSON | Hex (in serialize mode); Hex only in deserialize mode (SSZ bytes input) - Monospace textarea/code area for data entry -- Action bar: Upload file, Generate random value +- Action bar: Upload file (reads as text in serialize mode, as binary→hex in deserialize mode), Generate default value - Live processing — no submit button, debounced reactive updates ### Output Panel (right top) @@ -48,7 +50,8 @@ Side-by-side workbench: input panel (left), output + structure view (right). - Read-only monospace display ### Structure View (right bottom) -- Interactive collapsible tree of the SSZ type structure +- Interactive collapsible tree built from SSZ type schema + actual parsed/deserialized values +- Built on the main thread from the type definition (walks `type.fields`, `type.elementType`, etc.) using the modern @chainsafe/ssz API (ContainerType, ListBasicType, etc.) - Each node shows: field name, type annotation, current value - Color-coded by SSZ type category: - Blue: uint types @@ -74,7 +77,7 @@ Side-by-side workbench: input panel (left), output + structure view (right). src/ ├── app.tsx # Root layout, lifted state ├── main.tsx # Entry point, render root -├── index.html # Vite HTML +├── index.css # Tailwind directives ├── components/ │ ├── header.tsx # Title + fork/type selectors │ ├── footer.tsx # Credits, versions, links @@ -97,8 +100,8 @@ src/ ├── lib/ │ ├── types.ts # Fork definitions, type registry, typeNames() │ ├── formats.ts # Input/output format parse/dump -│ └── yaml.ts # Custom YAML schema for BigInt etc. -└── index.css # Tailwind directives + minimal custom styles +│ └── yaml.ts # js-yaml dump/load (default schema, no custom BigInt handling needed) +index.html # Vite HTML entry (project root, not src/) ``` ### State Flow @@ -110,9 +113,15 @@ src/ ### Worker Design Single web worker exposed via Comlink with three methods: -- `serialize(typeName, forkName, input, inputFormat)` → `{ serialized, hashTreeRoot }` -- `deserialize(typeName, forkName, data)` → `{ deserialized }` -- `randomValue(typeName, forkName)` → `{ value }` +- `serialize(typeName, forkName, input, inputFormat)` → `{ serialized: Uint8Array, hashTreeRoot: Uint8Array }` +- `deserialize(typeName, forkName, data: Uint8Array)` → `{ deserialized: unknown }` (input is raw SSZ bytes; caller converts hex string to Uint8Array before calling) +- `defaultValue(typeName, forkName)` → `{ value: unknown }` (returns `type.defaultValue()`, not random) + +Data transfer: Comlink uses structured clone by default. BigInt values in deserialized objects are structured-cloneable, so they transfer correctly. Uint8Array results use Comlink's `transfer()` for zero-copy. + +The worker imports `lib/types.ts` which includes the `patchSszTypes` function — this recursively replaces all 8-byte UintNumberType fields with UintBigintType to support full uint64 range. This patching is critical and must be preserved. + +Worker instantiation uses Vite's native worker support: `new Worker(new URL('./workers/ssz-worker.ts', import.meta.url), { type: 'module' })` ### Error Handling - Parse errors shown inline below input (not toast/alert) @@ -128,7 +137,13 @@ Single web worker exposed via Comlink with three methods: - ForkMe.tsx (GitHub link moves to footer) ## Migration Notes -- The `lib/types.ts` module preserves the fork/type registry logic from `util/types.ts` -- The `lib/yaml.ts` module preserves custom YAML schema from `util/yaml/` -- The `lib/formats.ts` module consolidates `util/input_types.ts` and `util/output_types.ts` +- `lib/types.ts` preserves fork/type registry logic from `util/types.ts`, including the critical `patchSszTypes` function that replaces UintNumberType(8) with UintBigintType. `sszTypesFor` and `gloas` from `@lodestar/types/ssz` are intentionally dropped — they are not used by the app. +- `lib/yaml.ts` is a thin wrapper around `js-yaml` using default schema. The custom `util/yaml/int.js` and `util/yaml/schema.js` are dead code (never imported) and will be deleted. BigInt values are converted to strings before YAML dumping. +- `lib/formats.ts` consolidates `util/input_types.ts` and `util/output_types.ts` - Worker logic from `components/worker/` is simplified into `workers/ssz-worker.ts` +- GitHub repo link fixed to `https://github.com/chainsafe/simpleserialize.com` (was pointing to wrong monorepo path) + +## Out of Scope +- URL state / deep linking (can be added later) +- Testing (separate effort after rewrite stabilizes) +- Mobile layout (desktop and tablet only) From ae32366437f60c3e051e5d6e4c600d4b962affb6 Mon Sep 17 00:00:00 2001 From: Cayman Date: Wed, 18 Mar 2026 09:36:46 -0400 Subject: [PATCH 03/37] docs: add implementation plan for complete overhaul 13 tasks covering: Vite scaffold, core lib migration, worker setup, hooks, UI components, structure view, app wiring, cleanup, and Dockerfile update. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../plans/2026-03-18-complete-overhaul.md | 1810 +++++++++++++++++ 1 file changed, 1810 insertions(+) create mode 100644 docs/superpowers/plans/2026-03-18-complete-overhaul.md diff --git a/docs/superpowers/plans/2026-03-18-complete-overhaul.md b/docs/superpowers/plans/2026-03-18-complete-overhaul.md new file mode 100644 index 0000000..51d51a5 --- /dev/null +++ b/docs/superpowers/plans/2026-03-18-complete-overhaul.md @@ -0,0 +1,1810 @@ +# simpleserialize.com Complete Overhaul — Implementation Plan + +> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Rebuild simpleserialize.com as a modern, reactive SSZ developer workbench with Vite, React 19, Tailwind CSS 4, and an interactive structure view. + +**Architecture:** Single-page app with lifted state in app.tsx. Input panel (left) and output+structure panels (right) in a side-by-side layout. Web worker via Comlink handles all SSZ operations. Dark theme, live processing with debounced updates. + +**Tech Stack:** Vite, React 19, TypeScript, Tailwind CSS 4, Comlink, sonner, js-yaml, @chainsafe/ssz, @lodestar/types + +**Spec:** `docs/superpowers/specs/2026-03-18-complete-overhaul-design.md` + +--- + +## File Structure + +``` +index.html # Vite HTML entry (project root) +vite.config.ts # Vite configuration +tsconfig.json # Updated TypeScript config for Vite +biome.jsonc # Updated to include .ts files +package.json # New dependencies + +src/ +├── vite-env.d.ts # Vite type declarations (import.meta.url, etc.) +├── main.tsx # Entry: createRoot + render App +├── index.css # Tailwind directives + custom CSS vars +├── app.tsx # Root layout, all lifted state, orchestration +├── components/ +│ ├── header.tsx # Title, spec badge, fork selector, type selector +│ ├── footer.tsx # Credits, versions, GitHub link +│ ├── input-panel.tsx # Format tabs, textarea, upload, default value +│ ├── output-panel.tsx # Mode tabs, output display, copy, download +│ ├── format-tabs.tsx # Reusable tab bar +│ ├── structure-view/ +│ │ ├── structure-view.tsx # Tree container, builds tree from type+value +│ │ ├── tree-node.tsx # Recursive collapsible node renderer +│ │ └── utils.ts # Type introspection, color mapping, generalized index +│ └── ui/ +│ ├── copy-button.tsx # Clipboard copy with toast feedback +│ └── file-upload.tsx # Mode-aware file upload +├── hooks/ +│ ├── use-ssz.ts # Orchestrates worker calls, manages result state +│ ├── use-worker.ts # Comlink worker lifecycle (init/terminate) +│ └── use-debounce.ts # Debounced value hook +├── workers/ +│ └── ssz-worker.ts # Comlink-exposed: serialize, deserialize, defaultValue +└── lib/ + ├── types.ts # Fork registry, patchSszTypes, typeNames() + ├── formats.ts # Input/output format parse/dump + └── yaml.ts # js-yaml wrapper (dump/load) +``` + +--- + +## Task 1: Scaffold Vite + React 19 + Tailwind CSS 4 + +**Files:** +- Create: `index.html` +- Create: `vite.config.ts` +- Create: `src/main.tsx` +- Create: `src/index.css` +- Create: `src/app.tsx` (minimal shell) +- Modify: `package.json` (complete rewrite) +- Modify: `tsconfig.json` (update for Vite) +- Modify: `biome.jsonc` (update file includes) +- Delete: `webpack.config.js`, `.babelrc`, `.prettierrc.js`, `src/index.tsx`, `src/index.html`, `src/styles.scss`, `src/App.tsx` + +- [ ] **Step 1: Delete old build config and entry files** + +```bash +rm -f webpack.config.js .babelrc .prettierrc.js src/index.tsx src/index.html src/styles.scss src/App.tsx yarn.lock +``` + +- [ ] **Step 2: Write new package.json** + +Replace the entire package.json with new dependencies. Keep `@chainsafe/ssz`, `@lodestar/types`, `js-yaml`. Add `react@^19`, `react-dom@^19`, `comlink`, `sonner`. Add dev deps: `vite`, `@vitejs/plugin-react`, `tailwindcss@^4`, `@tailwindcss/vite`, `typescript`, `@types/react@^19`, `@types/react-dom@^19`, `@biomejs/biome`, `@chainsafe/biomejs-config`. Remove everything else. + +```json +{ + "private": true, + "name": "simpleserialize.com", + "version": "1.0.0", + "type": "module", + "repository": "https://github.com/chainsafe/simpleserialize.com", + "author": "Chainsafe Systems", + "license": "MIT", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview", + "check-types": "tsc", + "lint": "biome check", + "lint:fix": "biome check --write" + }, + "dependencies": { + "@chainsafe/ssz": "^1.2.1", + "@lodestar/types": "^1.34.0", + "comlink": "^4.4.2", + "js-yaml": "^4.1.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "sonner": "^2.0.0" + }, + "devDependencies": { + "@biomejs/biome": "^1.9.4", + "@chainsafe/biomejs-config": "^0.1.0", + "@tailwindcss/vite": "^4.0.0", + "@types/js-yaml": "^4.0.9", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "tailwindcss": "^4.0.0", + "typescript": "~5.8.3", + "vite": "^6.0.0", + "@vitejs/plugin-react": "^4.0.0" + } +} +``` + +- [ ] **Step 3: Write vite.config.ts** + +```typescript +import tailwindcss from "@tailwindcss/vite"; +import react from "@vitejs/plugin-react"; +import { defineConfig } from "vite"; + +export default defineConfig({ + plugins: [react(), tailwindcss()], + worker: { + format: "es", + }, +}); +``` + +- [ ] **Step 4: Write index.html at project root** + +```html + + + + + + SSZ Playground | Chainsafe Systems + + +
+ + + +``` + +- [ ] **Step 5: Write src/index.css** + +```css +@import "tailwindcss"; + +@theme { + --font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace; + --font-sans: Inter, ui-sans-serif, system-ui, -apple-system, sans-serif; + + --color-ssz-uint: #60a5fa; + --color-ssz-bytes: #34d399; + --color-ssz-container: #a78bfa; + --color-ssz-list: #fb923c; + --color-ssz-boolean: #94a3b8; +} +``` + +- [ ] **Step 6: Write src/main.tsx** + +```tsx +import { createRoot } from "react-dom/client"; +import { Toaster } from "sonner"; +import App from "./app"; +import "./index.css"; + +createRoot(document.getElementById("root")!).render( + <> + + + +); +``` + +- [ ] **Step 7: Write src/app.tsx (minimal shell)** + +Minimal app that renders "SSZ Playground" in a dark page to verify the setup works. + +```tsx +export default function App() { + return ( +
+

SSZ Playground

+
+ ); +} +``` + +- [ ] **Step 8: Update tsconfig.json for Vite** + +```json +{ + "include": ["src"], + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "jsx": "react-jsx", + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "noEmit": true, + "strict": true, + "skipLibCheck": true, + "isolatedModules": true, + "esModuleInterop": true, + "resolveJsonModule": true + } +} +``` + +- [ ] **Step 9: Write src/vite-env.d.ts** + +```typescript +/// +``` + +- [ ] **Step 10: Update biome.jsonc — include .ts and .tsx files** + +Update the `files.include` to `["src/**/*.ts", "src/**/*.tsx"]` so it lints all source files, not just .tsx. + +- [ ] **Step 11: Delete node_modules and reinstall** + +```bash +rm -rf node_modules dist package-lock.json +npm install +``` + +- [ ] **Step 12: Verify dev server starts** + +```bash +npx vite --host 0.0.0.0 +``` + +Expected: Dev server starts on localhost:5173, page shows "SSZ Playground" in white on dark background. + +- [ ] **Step 13: Commit** + +```bash +git add -A +git commit -m "feat: scaffold Vite + React 19 + Tailwind CSS 4" +``` + +--- + +## Task 2: Port Core Library Modules + +**Files:** +- Create: `src/lib/types.ts` (from `src/util/types.ts`) +- Create: `src/lib/yaml.ts` (from `src/util/yaml/index.ts`) +- Create: `src/lib/formats.ts` (from `src/util/input_types.ts` + `src/util/output_types.ts`) +- Delete: `src/util/` (entire directory) +- Delete: `src/types/` (entire directory) +- Delete: `src/components/worker/` (entire directory) +- Delete: `src/components/TreeView.tsx`, `src/components/ForkMe.tsx` +- Delete: `src/components/Tabs.tsx`, `src/components/Serialize.tsx` +- Delete: `src/components/Input.tsx`, `src/components/Output.tsx` +- Delete: `src/components/Header.tsx`, `src/components/Footer.tsx` +- Delete: `src/components/display/NamedOutput.tsx`, `src/components/display/ErrorBox.tsx` + +- [ ] **Step 1: Write src/lib/types.ts** + +Port from `src/util/types.ts`. Keep `patchSszTypes` and `replaceUintTypeWithUintBigintType` exactly as-is. Drop `sszTypesFor` and `gloas` from the destructuring. Keep the fork accumulation pattern. + +```typescript +import { + ContainerType, + ListBasicType, + ListCompositeType, + type Type, + UintBigintType, + UintNumberType, + VectorBasicType, + VectorCompositeType, +} from "@chainsafe/ssz"; +import { ssz } from "@lodestar/types"; + +let { + phase0, + altair, + bellatrix, + capella, + deneb, + electra, + fulu, + // intentionally dropped: sszTypesFor, gloas — not used by the app + ...primitive +} = ssz; + +phase0 = patchSszTypes(phase0); +altair = patchSszTypes(altair); +bellatrix = patchSszTypes(bellatrix); +capella = patchSszTypes(capella); +deneb = patchSszTypes(deneb); +electra = patchSszTypes(electra); +fulu = patchSszTypes(fulu); +primitive = patchSszTypes(primitive); + +export const forks = { + phase0: { ...phase0, ...primitive }, + altair: { ...phase0, ...altair, ...primitive }, + bellatrix: { ...phase0, ...altair, ...bellatrix, ...primitive }, + capella: { ...phase0, ...altair, ...bellatrix, ...capella, ...primitive }, + deneb: { ...phase0, ...altair, ...bellatrix, ...capella, ...deneb, ...primitive }, + electra: { ...phase0, ...altair, ...bellatrix, ...capella, ...deneb, ...electra, ...primitive }, + fulu: { ...phase0, ...altair, ...bellatrix, ...capella, ...deneb, ...electra, ...fulu, ...primitive }, +} as Record>>; + +export type ForkName = keyof typeof forks; + +export const forkNames = Object.keys(forks); + +export function typeNames(types: Record>): string[] { + return Object.keys(types).sort(); +} + +/** + * Patch SSZ types to support the full uint64 range on the website. + * Recursively replaces all 8-byte UintNumberType with UintBigintType. + */ +function patchSszTypes>>(sszTypes: T): T { + const types = { ...sszTypes }; + for (const key of Object.keys(types) as (keyof typeof types)[]) { + types[key] = replaceUintTypeWithUintBigintType(types[key]); + } + return types; +} + +function replaceUintTypeWithUintBigintType>(type: T): T { + if (type instanceof UintNumberType && type.byteLength === 8) { + return new UintBigintType(type.byteLength) as unknown as T; + } + if (type instanceof ContainerType) { + const fields = { ...type.fields }; + for (const key of Object.keys(fields) as (keyof typeof fields)[]) { + fields[key] = replaceUintTypeWithUintBigintType(fields[key]); + } + return new ContainerType(fields, type.opts) as unknown as T; + } + if (type instanceof ListBasicType) { + return new ListBasicType(replaceUintTypeWithUintBigintType(type.elementType), type.limit) as unknown as T; + } + if (type instanceof VectorBasicType) { + return new VectorBasicType(replaceUintTypeWithUintBigintType(type.elementType), type.length) as unknown as T; + } + if (type instanceof ListCompositeType) { + return new ListCompositeType(replaceUintTypeWithUintBigintType(type.elementType), type.limit) as unknown as T; + } + if (type instanceof VectorCompositeType) { + return new VectorCompositeType(replaceUintTypeWithUintBigintType(type.elementType), type.length) as unknown as T; + } + return type; +} +``` + +- [ ] **Step 2: Write src/lib/yaml.ts** + +Simple js-yaml wrapper. BigInt values need to be converted to strings before dumping since js-yaml's default schema doesn't handle BigInt. + +```typescript +import yaml from "js-yaml"; + +function bigintReplacer(_key: string, value: unknown): unknown { + return typeof value === "bigint" ? value.toString() : value; +} + +export function dumpYaml(input: unknown): string { + return yaml.dump(JSON.parse(JSON.stringify(input, bigintReplacer))); +} + +export function parseYaml(input: string): unknown { + return yaml.load(input); +} +``` + +- [ ] **Step 3: Write src/lib/formats.ts** + +Consolidates input_types.ts and output_types.ts. Preserves the parse/dump interfaces. + +```typescript +import { type Type, fromHexString, toHexString } from "@chainsafe/ssz"; +import { dumpYaml, parseYaml } from "./yaml"; + +// --- Input formats (for parsing user input into SSZ values) --- + +type InputFormat = { + parse: (raw: string, type: Type) => T; + dump: (value: unknown, type: Type) => string; +}; + +export const inputFormats: Record = { + yaml: { + parse: (raw, type) => type.fromJson(parseYaml(raw)), + dump: (value, type) => + dumpYaml(type.toJson(typeof value === "number" ? value.toString() : value)), + }, + json: { + parse: (raw, type) => type.fromJson(JSON.parse(raw)), + dump: (value, type) => JSON.stringify(type.toJson(value), null, 2), + }, + hex: { + parse: (raw, type) => type.deserialize(fromHexString(raw)), + dump: (value, type) => toHexString(type.serialize(value as never)), + }, +}; + +// --- Output formats (for displaying results) --- + +function toBase64(data: Uint8Array): string { + const binstr = Array.from(data, (ch) => String.fromCharCode(ch)).join(""); + return btoa(binstr); +} + +type SerializeOutputFormat = { + dump: (value: Uint8Array) => string; +}; + +export const serializeOutputFormats: Record = { + hex: { dump: (value) => toHexString(value) }, + base64: { dump: (value) => toBase64(value) }, +}; + +type DeserializeOutputFormat = { + dump: (value: unknown, type: Type) => string; +}; + +export const deserializeOutputFormats: Record = { + yaml: { + dump: (value, type) => + dumpYaml(type.toJson(typeof value === "number" ? value.toString() : value)), + }, + json: { + dump: (value, type) => JSON.stringify(type.toJson(value), null, 2), + }, +}; + +export const serializeInputFormatNames = ["yaml", "json", "hex"] as const; +export const deserializeInputFormatNames = ["hex"] as const; +export const serializeOutputFormatNames = ["hex", "base64"] as const; +export const deserializeOutputFormatNames = ["yaml", "json"] as const; +``` + +- [ ] **Step 4: Delete all old source files** + +```bash +rm -rf src/util src/types src/components +``` + +- [ ] **Step 5: Verify type checking passes** + +```bash +npx tsc +``` + +Expected: No errors (the lib modules should compile cleanly). + +- [ ] **Step 6: Commit** + +```bash +git add -A +git commit -m "feat: port core library modules (types, formats, yaml)" +``` + +--- + +## Task 3: Web Worker with Comlink + +**Files:** +- Create: `src/workers/ssz-worker.ts` +- Create: `src/hooks/use-worker.ts` + +- [ ] **Step 1: Write src/workers/ssz-worker.ts** + +Comlink-exposed worker with serialize, deserialize, and defaultValue methods. + +```typescript +import { type Type, fromHexString } from "@chainsafe/ssz"; +import * as Comlink from "comlink"; +import { inputFormats } from "../lib/formats"; +import { forks } from "../lib/types"; + +function getType(typeName: string, forkName: string): Type { + return forks[forkName][typeName]; +} + +const worker = { + serialize( + typeName: string, + forkName: string, + input: string, + inputFormat: string, + ) { + const type = getType(typeName, forkName); + const parsed = inputFormats[inputFormat].parse(input, type); + const serialized = type.serialize(parsed); + const hashTreeRoot = type.hashTreeRoot(parsed); + return Comlink.transfer( + { serialized, hashTreeRoot }, + [serialized.buffer, hashTreeRoot.buffer], + ); + }, + + deserialize( + typeName: string, + forkName: string, + hexData: string, + ): { deserialized: unknown } { + const type = getType(typeName, forkName); + const bytes = fromHexString(hexData); + const deserialized = type.deserialize(bytes); + return { deserialized }; + }, + + defaultValue( + typeName: string, + forkName: string, + ): { value: unknown } { + const type = getType(typeName, forkName); + const value = type.defaultValue(); + return { value }; + }, +}; + +export type SszWorkerApi = typeof worker; + +Comlink.expose(worker); +``` + +- [ ] **Step 2: Write src/hooks/use-worker.ts** + +Hook that creates the worker on mount, wraps it with Comlink, and terminates on unmount. + +```typescript +import * as Comlink from "comlink"; +import { useEffect, useRef } from "react"; +import type { SszWorkerApi } from "../workers/ssz-worker"; + +export function useWorker() { + const workerRef = useRef | null>(null); + const rawWorkerRef = useRef(null); + + useEffect(() => { + const raw = new Worker( + new URL("../workers/ssz-worker.ts", import.meta.url), + { type: "module" }, + ); + workerRef.current = Comlink.wrap(raw); + rawWorkerRef.current = raw; + + return () => { + raw.terminate(); + workerRef.current = null; + rawWorkerRef.current = null; + }; + }, []); + + return workerRef; +} +``` + +- [ ] **Step 3: Verify build works** + +```bash +npx vite build +``` + +Expected: Build succeeds. Worker is bundled separately by Vite. + +- [ ] **Step 4: Commit** + +```bash +git add -A +git commit -m "feat: add SSZ web worker with Comlink" +``` + +--- + +## Task 4: Hooks — useDebounce and useSsz + +**Files:** +- Create: `src/hooks/use-debounce.ts` +- Create: `src/hooks/use-ssz.ts` + +- [ ] **Step 1: Write src/hooks/use-debounce.ts** + +```typescript +import { useEffect, useState } from "react"; + +export function useDebounce(value: T, delayMs: number): T { + const [debounced, setDebounced] = useState(value); + + useEffect(() => { + const timer = setTimeout(() => setDebounced(value), delayMs); + return () => clearTimeout(timer); + }, [value, delayMs]); + + return debounced; +} +``` + +- [ ] **Step 2: Write src/hooks/use-ssz.ts** + +Central hook that takes all the state (mode, fork, type, input, format) and orchestrates worker calls. Returns the result state. + +```typescript +import type * as Comlink from "comlink"; +import { useCallback, useEffect, useState } from "react"; +import type { SszWorkerApi } from "../workers/ssz-worker"; +import { useDebounce } from "./use-debounce"; + +type SszResult = { + serialized: Uint8Array | null; + hashTreeRoot: Uint8Array | null; + deserialized: unknown | null; + error: string | null; + loading: boolean; +}; + +export function useSsz( + worker: Comlink.Remote | null, + mode: "serialize" | "deserialize", + forkName: string, + typeName: string, + input: string, + inputFormat: string, +): SszResult { + const [result, setResult] = useState({ + serialized: null, + hashTreeRoot: null, + deserialized: null, + error: null, + loading: false, + }); + + const debouncedInput = useDebounce(input, 300); + + useEffect(() => { + if (!worker || !debouncedInput.trim() || !typeName || !forkName) { + return; + } + + let cancelled = false; + setResult((prev) => ({ ...prev, loading: true, error: null })); + + const run = async () => { + try { + if (mode === "serialize") { + const { serialized, hashTreeRoot } = await worker.serialize( + typeName, + forkName, + debouncedInput, + inputFormat, + ); + if (!cancelled) { + setResult({ + serialized, + hashTreeRoot, + deserialized: null, + error: null, + loading: false, + }); + } + } else { + const { deserialized } = await worker.deserialize( + typeName, + forkName, + debouncedInput, + ); + if (!cancelled) { + setResult({ + serialized: null, + hashTreeRoot: null, + deserialized, + error: null, + loading: false, + }); + } + } + } catch (e) { + if (!cancelled) { + setResult({ + serialized: null, + hashTreeRoot: null, + deserialized: null, + error: e instanceof Error ? e.message : String(e), + loading: false, + }); + } + } + }; + + run(); + return () => { cancelled = true; }; + }, [worker, mode, forkName, typeName, debouncedInput, inputFormat]); + + return result; +} +``` + +- [ ] **Step 3: Verify type checking** + +```bash +npx tsc +``` + +Expected: No errors. + +- [ ] **Step 4: Commit** + +```bash +git add -A +git commit -m "feat: add useDebounce and useSsz hooks" +``` + +--- + +## Task 5: UI Components — Header and Footer + +**Files:** +- Create: `src/components/header.tsx` +- Create: `src/components/footer.tsx` + +- [ ] **Step 1: Write src/components/header.tsx** + +Header with title, spec version badge, fork selector, and type selector. Fork and type are controlled props. + +```tsx +import { type ForkName, forkNames, forks, typeNames } from "../lib/types"; + +const SPEC_VERSION = "1.6.0"; + +type HeaderProps = { + forkName: string; + typeName: string; + onForkChange: (fork: ForkName) => void; + onTypeChange: (type: string) => void; +}; + +export function Header({ forkName, typeName, onForkChange, onTypeChange }: HeaderProps) { + const types = typeNames(forks[forkName]); + + return ( +
+
+
+

+ SSZ Playground +

+ + spec v{SPEC_VERSION} + +
+ +
+ + + +
+
+
+ ); +} +``` + +- [ ] **Step 2: Write src/components/footer.tsx** + +```tsx +// Only import the specific fields we need to avoid bundling full package.json +import { dependencies } from "../../package.json"; + +export function Footer() { + return ( + + ); +} +``` + +- [ ] **Step 3: Commit** + +```bash +git add -A +git commit -m "feat: add header and footer components" +``` + +--- + +## Task 6: UI Components — FormatTabs and small UI pieces + +**Files:** +- Create: `src/components/format-tabs.tsx` +- Create: `src/components/ui/copy-button.tsx` +- Create: `src/components/ui/file-upload.tsx` + +- [ ] **Step 1: Write src/components/format-tabs.tsx** + +Reusable tab bar component. + +```tsx +type FormatTabsProps = { + options: readonly string[]; + selected: string; + onChange: (value: string) => void; +}; + +export function FormatTabs({ options, selected, onChange }: FormatTabsProps) { + return ( +
+ {options.map((opt) => ( + + ))} +
+ ); +} +``` + +- [ ] **Step 2: Write src/components/ui/copy-button.tsx** + +```tsx +import { toast } from "sonner"; + +type CopyButtonProps = { + text: string; + label?: string; +}; + +export function CopyButton({ text, label = "Copy" }: CopyButtonProps) { + const handleCopy = async () => { + await navigator.clipboard.writeText(text); + toast.success("Copied to clipboard"); + }; + + return ( + + ); +} +``` + +- [ ] **Step 3: Write src/components/ui/file-upload.tsx** + +Mode-aware file upload. In serialize mode, reads as text. In deserialize mode, reads as binary and converts to hex. + +```tsx +import { toHexString } from "@chainsafe/ssz"; +import { useRef } from "react"; + +type FileUploadProps = { + serializeMode: boolean; + onLoad: (content: string) => void; +}; + +export function FileUpload({ serializeMode, onLoad }: FileUploadProps) { + const inputRef = useRef(null); + + const handleChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + const reader = new FileReader(); + reader.onload = () => { + if (reader.result instanceof ArrayBuffer) { + onLoad(toHexString(new Uint8Array(reader.result))); + } else if (typeof reader.result === "string") { + onLoad(reader.result); + } + }; + + if (serializeMode) { + reader.readAsText(file); + } else { + reader.readAsArrayBuffer(file); + } + + // Reset input so the same file can be uploaded again + if (inputRef.current) inputRef.current.value = ""; + }; + + return ( + + ); +} +``` + +- [ ] **Step 4: Commit** + +```bash +git add -A +git commit -m "feat: add format tabs, copy button, file upload components" +``` + +--- + +## Task 7: Input Panel + +**Files:** +- Create: `src/components/input-panel.tsx` + +- [ ] **Step 1: Write src/components/input-panel.tsx** + +The input panel with format tabs, textarea, and action bar. + +```tsx +import { serializeInputFormatNames, deserializeInputFormatNames } from "../lib/formats"; +import { FormatTabs } from "./format-tabs"; +import { FileUpload } from "./ui/file-upload"; + +type InputPanelProps = { + serializeMode: boolean; + input: string; + inputFormat: string; + onInputChange: (value: string) => void; + onInputFormatChange: (format: string) => void; + onGenerateDefault: () => void; + loading: boolean; +}; + +export function InputPanel({ + serializeMode, + input, + inputFormat, + onInputChange, + onInputFormatChange, + onGenerateDefault, + loading, +}: InputPanelProps) { + const formatNames = serializeMode ? serializeInputFormatNames : deserializeInputFormatNames; + + return ( +
+
+

Input

+ +
+ +