Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
name: CI

on:
push:
branches:
- main
- master
pull_request:

permissions:
contents: read

concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true

jobs:
quality:
name: Lint, typecheck, build & unit tests
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
run_install: false

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: "pnpm"

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Lint
run: pnpm lint

- name: Typecheck
run: pnpm typecheck

- name: Build
run: pnpm build

- name: Unit tests
run: pnpm test

storybook-tests:
name: Storybook interaction tests
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
run_install: false

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: "pnpm"

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Install Playwright browser
run: pnpm exec playwright install --with-deps chromium

- name: Run Storybook tests
run: pnpm test:storybook
1 change: 1 addition & 0 deletions .storybook/preview.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Preview } from '@storybook/react-vite'
import '@fontsource/jetbrains-mono'

const preview: Preview = {
parameters: {
Expand Down
7 changes: 6 additions & 1 deletion eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
import { defineConfig, globalIgnores } from 'eslint/config'

export default defineConfig([globalIgnores(['dist']), {
// `example/` is a standalone demo app with its own eslint/tsconfig setup, so it
// is excluded here to avoid ambiguous tsconfig root detection.
export default defineConfig([globalIgnores(['dist', 'example']), {
files: ['**/*.{ts,tsx}'],
extends: [
js.configs.recommended,
Expand All @@ -18,5 +20,8 @@ export default defineConfig([globalIgnores(['dist']), {
],
languageOptions: {
globals: globals.browser,
parserOptions: {
tsconfigRootDir: import.meta.dirname,
},
},
}, ...storybook.configs["flat/recommended"]])
32 changes: 19 additions & 13 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@
"dev": "vite",
"build": "tsc -b && IS_LIB=true vite build",
"lint": "eslint .",
"typecheck": "tsc -b",
"test": "vitest run --project unit",
"test:watch": "vitest --project unit",
"test:storybook": "vitest run --project storybook",
"test:all": "vitest run",
"preview": "vite preview",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build"
Expand All @@ -28,38 +33,39 @@
"react": "^19.2.6"
},
"devDependencies": {
"react-dom": "^19.2.6",
"@babel/core": "^7.29.0",
"@chromatic-com/storybook": "^5.2.1",
"@eslint/js": "^10.0.1",
"@fontsource/jetbrains-mono": "^5.2.8",
"@microsoft/api-extractor": "^7.58.9",
"@rolldown/plugin-babel": "^0.2.3",
"@storybook/addon-a11y": "^10.4.6",
"@storybook/addon-docs": "^10.4.6",
"@storybook/addon-mcp": "^0.6.0",
"@storybook/addon-vitest": "^10.4.6",
"@storybook/react-vite": "^10.4.6",
"@types/babel__core": "^7.20.5",
"@types/node": "^24.12.3",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.1",
"@vitest/browser-playwright": "^4.1.9",
"@vitest/coverage-v8": "^4.1.9",
"babel-plugin-react-compiler": "^1.0.0",
"eslint": "^10.3.0",
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.2",
"eslint-plugin-storybook": "^10.4.6",
"globals": "^17.6.0",
"playwright": "^1.61.0",
"react-dom": "^19.2.6",
"storybook": "^10.4.6",
"typescript": "~6.0.2",
"typescript-eslint": "^8.59.2",
"vite": "^8.0.12",
"vite-plugin-css-injected-by-js": "^5.0.1",
"vite-plugin-dts": "^5.0.2",
"storybook": "^10.4.6",
"@storybook/react-vite": "^10.4.6",
"@chromatic-com/storybook": "^5.2.1",
"@storybook/addon-vitest": "^10.4.6",
"@storybook/addon-a11y": "^10.4.6",
"@storybook/addon-docs": "^10.4.6",
"@storybook/addon-mcp": "^0.6.0",
"eslint-plugin-storybook": "^10.4.6",
"vitest": "^4.1.9",
"playwright": "^1.61.0",
"@vitest/browser-playwright": "^4.1.9",
"@vitest/coverage-v8": "^4.1.9"
"vitest": "^4.1.9"
},
"repository": {
"type": "git",
Expand Down
8 changes: 8 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

90 changes: 90 additions & 0 deletions src/components/DebugPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { useId, useMemo } from 'react'
import { interpretBytes, type Selection } from '../lib/hex'

export interface DebugPanelProps {
selection: Selection | null
data: Uint8Array
isLittleEndian: boolean
onLittleEndianChange: (value: boolean) => void
}

interface NumericRowProps {
label: string
value: number | string | null
}

/** One labelled numeric interpretation, hidden when there aren't enough bytes. */
const NumericRow = ({ label, value }: NumericRowProps) =>
value === null ? null : (
<div className="rhv-debug-text">
{label.padEnd(8)}
{value}
</div>
)

export const DebugPanel = ({
selection,
data,
isLittleEndian,
onLittleEndianChange,
}: DebugPanelProps) => {
const littleEndianId = useId()

const interpretation = useMemo(() => {
if (!selection) return null
const bytes = data.slice(selection.start, selection.end + 1)
return interpretBytes(bytes, isLittleEndian)
}, [selection, data, isLittleEndian])

return (
<div className="rhv-debug-panel">
<header className="rhv-debug-header">Debug</header>
{selection ? (
<div className="rhv-debug-content">
<div className="rhv-debug-text">
Selection: 0x{selection.start.toString(16).toUpperCase()} –{' '}
0x{selection.end.toString(16).toUpperCase()}
</div>
<div className="rhv-debug-text">
Size: {selection.end - selection.start + 1} bytes (
{(selection.end - selection.start + 1) * 8} bits)
</div>
{interpretation && (
<>
<div className="rhv-debug-text">Binary: {interpretation.binary}</div>
<NumericRow label="Int8:" value={interpretation.int8} />
<NumericRow label="Uint8:" value={interpretation.uint8} />
<NumericRow label="Int16:" value={interpretation.int16} />
<NumericRow label="Uint16:" value={interpretation.uint16} />
<NumericRow label="Int32:" value={interpretation.int32} />
<NumericRow label="Uint32:" value={interpretation.uint32} />
<NumericRow
label="Float32:"
value={
interpretation.float32 === null
? null
: interpretation.float32.toPrecision(7)
}
/>
<NumericRow label="Float64:" value={interpretation.float64} />
<div className="rhv-endian-container">
<input
type="checkbox"
id={littleEndianId}
checked={isLittleEndian}
onChange={(e) => onLittleEndianChange(e.target.checked)}
className="rhv-checkbox"
/>
<label htmlFor={littleEndianId} className="rhv-debug-text">
Little Endian
</label>
</div>
</>
)}
</div>
) : (
<div className="rhv-debug-text-muted">No selection</div>
)}
</div>
)
}
26 changes: 26 additions & 0 deletions src/components/HexHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { BYTES_PER_ROW } from '../lib/hex'

const ASCII_LABEL = ['A', 'S', 'C', 'I', 'I']

/** Sticky column header: the byte-offset gutter, the 0x00–0x0F columns and the ASCII label. */
export const HexHeader = () => (
<div className="rhv-header">
<span className="rhv-offset-label"> </span>
{Array.from({ length: BYTES_PER_ROW }, (_, i) => {
const hex = i.toString(16).padStart(2, '0').toUpperCase()
return (
<span key={hex} className="rhv-header-byte">
{hex}
</span>
)
})}
{ASCII_LABEL.map((char, i) => (
<span
key={char + i}
className={i === 0 ? 'rhv-header-ascii-title' : 'rhv-header-ascii-char'}
>
{char}
</span>
))}
</div>
)
Loading
Loading