From a276fe20a968f66a556f8fed5702fe89f734d422 Mon Sep 17 00:00:00 2001 From: Jaime Idolpx Date: Thu, 11 Jun 2026 03:34:35 -0400 Subject: [PATCH] feat(HexEditor): implement responsive bytes per row for improved layout adaptability --- src/app/components/HexEditor.tsx | 38 +++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/src/app/components/HexEditor.tsx b/src/app/components/HexEditor.tsx index c082fef..1d16cfe 100644 --- a/src/app/components/HexEditor.tsx +++ b/src/app/components/HexEditor.tsx @@ -1,7 +1,6 @@ import { useCallback, useEffect, useReducer, useRef, useState } from 'react'; import { Eye, Pencil, Redo2, Save, Search, Undo2, X } from 'lucide-react'; -const BYTES_PER_ROW = 8; const ROW_HEIGHT = 20; // px — matches leading-5 at Tailwind's 16px base const CHUNK_ROWS = 256; // virtual-scroll overscan: render prev+curr+next windows @@ -75,12 +74,25 @@ export default function HexEditor({ data, readOnly = false, onSave }: HexEditorP const [searchType, setSearchType] = useState<'text' | 'hex'>('text'); const [matchIdx, setMatchIdx] = useState(-1); + // Responsive columns + const [bytesPerRow, setBytesPerRow] = useState(8); + // Virtual scroll const scrollRef = useRef(null); const searchRef = useRef(null); const [scrollTop, setScrollTop] = useState(0); - const totalRows = Math.ceil(current.length / BYTES_PER_ROW); + useEffect(() => { + const el = scrollRef.current; + if (!el) return; + const update = (w: number) => setBytesPerRow(w >= 600 ? 16 : 8); + const obs = new ResizeObserver(entries => update(entries[0].contentRect.width)); + obs.observe(el); + update(el.clientWidth); + return () => obs.disconnect(); + }, []); + + const totalRows = Math.ceil(current.length / bytesPerRow); const firstRenderRow = Math.max(0, Math.floor(scrollTop / ROW_HEIGHT) - CHUNK_ROWS); const lastRenderRow = Math.min(totalRows, firstRenderRow + CHUNK_ROWS * 3); const paddingTop = firstRenderRow * ROW_HEIGHT; @@ -108,7 +120,7 @@ export default function HexEditor({ data, readOnly = false, onSave }: HexEditorP // Scroll cursor row into view (programmatic, works with virtual scroll) useEffect(() => { if (cursor < 0) return; - const row = Math.floor(cursor / BYTES_PER_ROW); + const row = Math.floor(cursor / bytesPerRow); const rowTop = row * ROW_HEIGHT + 12; // 12 = top padding equivalent const el = scrollRef.current; if (!el) return; @@ -117,7 +129,7 @@ export default function HexEditor({ data, readOnly = false, onSave }: HexEditorP } else if (rowTop + ROW_HEIGHT > el.scrollTop + el.clientHeight) { el.scrollTop = rowTop + ROW_HEIGHT - el.clientHeight; } - }, [cursor]); + }, [cursor, bytesPerRow]); const pushByte = useCallback((offset: number, value: number) => { const next = current.slice(); @@ -166,10 +178,10 @@ export default function HexEditor({ data, readOnly = false, onSave }: HexEditorP const len = current.length; if (e.key === 'ArrowRight') { e.preventDefault(); setNibble(0); setCursor(c => Math.min(c < 0 ? 0 : c + 1, len - 1)); return; } if (e.key === 'ArrowLeft') { e.preventDefault(); setNibble(0); setCursor(c => c <= 0 ? 0 : c - 1); return; } - if (e.key === 'ArrowDown') { e.preventDefault(); setCursor(c => Math.min(c < 0 ? 0 : c + BYTES_PER_ROW, len - 1)); return; } - if (e.key === 'ArrowUp') { e.preventDefault(); setCursor(c => c < 0 ? 0 : Math.max(c - BYTES_PER_ROW, 0)); return; } - if (e.key === 'Home') { e.preventDefault(); setCursor(c => Math.floor(Math.max(c, 0) / BYTES_PER_ROW) * BYTES_PER_ROW); return; } - if (e.key === 'End') { e.preventDefault(); setCursor(c => Math.min(Math.ceil((Math.max(c, 0) + 1) / BYTES_PER_ROW) * BYTES_PER_ROW - 1, len - 1)); return; } + if (e.key === 'ArrowDown') { e.preventDefault(); setCursor(c => Math.min(c < 0 ? 0 : c + bytesPerRow, len - 1)); return; } + if (e.key === 'ArrowUp') { e.preventDefault(); setCursor(c => c < 0 ? 0 : Math.max(c - bytesPerRow, 0)); return; } + if (e.key === 'Home') { e.preventDefault(); setCursor(c => Math.floor(Math.max(c, 0) / bytesPerRow) * bytesPerRow); return; } + if (e.key === 'End') { e.preventDefault(); setCursor(c => Math.min(Math.ceil((Math.max(c, 0) + 1) / bytesPerRow) * bytesPerRow - 1, len - 1)); return; } if (!editMode || cursor < 0) return; @@ -306,18 +318,18 @@ export default function HexEditor({ data, readOnly = false, onSave }: HexEditorP
{Array.from({ length: lastRenderRow - firstRenderRow }, (_, i) => { const row = firstRenderRow + i; - const base = row * BYTES_PER_ROW; + const base = row * bytesPerRow; return (
{/* Address */} - + {base.toString(16).padStart(8, '0').toUpperCase()} {/* Hex pane */} -
- {Array.from({ length: BYTES_PER_ROW }, (_, col) => { +
+ {Array.from({ length: bytesPerRow }, (_, col) => { const idx = base + col; if (idx >= current.length) { return ; @@ -349,7 +361,7 @@ export default function HexEditor({ data, readOnly = false, onSave }: HexEditorP {/* ASCII pane */}
- {Array.from({ length: BYTES_PER_ROW }, (_, col) => { + {Array.from({ length: bytesPerRow }, (_, col) => { const idx = base + col; if (idx >= current.length) return ; const byte = current[idx];