vuehex 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE.txt ADDED
@@ -0,0 +1,23 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) Vincent Vollers
4
+
5
+ All rights reserved.
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ of this software and associated documentation files (the "Software"), to deal
9
+ in the Software without restriction, including without limitation the rights
10
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ copies of the Software, and to permit persons to whom the Software is
12
+ furnished to do so, subject to the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be included in all
15
+ copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,397 @@
1
+ # VueHex
2
+
3
+ VueHex is a fast, virtualized hex viewer component for Vue 3. It can be used both for cleanly displaying binary data and for efficiently viewing very large datasets.
4
+
5
+ * No dependencies
6
+ * Small package (~64KB minimized / ~15KB zipped)
7
+ * Render local and remote data
8
+ * Can handle extremely large data sizes
9
+ * Very flexible and themeable, can be as simple or as complicated as you wish
10
+
11
+ ![Screenshot Normal](public/screenshot001.png)
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ npm install vuehex
17
+ ```
18
+
19
+ Optionally register the plugin once to make the `<VueHex>` component available everywhere and load the bundled styles:
20
+
21
+ ```ts
22
+ import { createApp } from "vue";
23
+ import VueHex from "vuehex";
24
+ import "vuehex/styles";
25
+
26
+ import App from "./App.vue";
27
+
28
+ const app = createApp(App);
29
+ app.use(VueHex);
30
+ app.mount("#app");
31
+ ```
32
+
33
+ ## Quick start
34
+
35
+ It can be as simple as this
36
+
37
+ ```vue
38
+ <template>
39
+ <VueHex
40
+ v-model="windowData"
41
+ />
42
+ </template>
43
+
44
+ <script setup lang="ts">
45
+ import { ref } from "vue";
46
+
47
+ const backingFile = crypto.getRandomValues(new Uint8Array(2 ** 20));
48
+ const windowData = ref(backingFile.slice(0, 16 * 48));
49
+ </script>
50
+ ```
51
+
52
+ ## Props
53
+
54
+ | Prop | Default | Description |
55
+ |------|---------|-------------|
56
+ | `dataMode` / `data-mode` | `auto` | Data handling mode: `auto`, `buffer`, or `window`. |
57
+ | `expandToContent` / `expand-to-content` | `false` | Disables internal scrolling/virtualization and expands the component height to fit the full buffer (expects the full data in `v-model`). |
58
+ | `totalSize` | `modelValue.length` | Total bytes available. |
59
+ | `bytesPerRow` | `16` | Number of bytes to display per row. |
60
+ | `uppercase` | `false` | Whether to display hex values in uppercase. |
61
+ | `nonPrintableChar` | `'.'` | Character to display for non-printable bytes in ASCII column. |
62
+ | `isPrintable` / `renderAscii` | — | Functions to customize ASCII rendering. |
63
+ | `theme` | — | Color theme: `'dark'`, `'light'`, `'terminal'`, `'sunset'`, or `'auto'`. Omit to follow `prefers-color-scheme` between dark and light. |
64
+ | `cellClassForByte` | — | Function `(payload: { kind: 'hex' \| 'ascii'; index: number; byte: number }) => string \| string[] \| void` for custom highlighting. |
65
+ | `getSelectionData` | — | Function `(selectionStart: number, selectionEnd: number) => Uint8Array` used for clipboard copy (required when using virtual windows; auto-implemented in full-data mode). |
66
+ | `overscan` | `2` | Number of extra rows to render above and below the viewport. |
67
+ | `showChunkNavigator` + `chunkNavigatorPlacement` | — | Enable and configure the optional chunk navigator UI. |
68
+ | `statusbar` | `null` | Status bar placement: `'top'`, `'bottom'`, or `null` to hide. Shows byte info on hover and selection details. |
69
+ | `statusbarLayout` | — | Configuration object controlling which items appear in the status bar and their placement (`left`, `middle`, `right` sections). |
70
+ | `cursor` | `false` | Enable keyboard/click cursor navigation. Navigate with arrow keys when focused or click bytes to move cursor. |
71
+
72
+ ## Models
73
+
74
+ VueHex uses Vue 3's `defineModel` for two-way data binding. These support `v-model` syntax:
75
+
76
+ | Model | Type | Default | Description |
77
+ |-------|------|---------|-------------|
78
+ | `v-model` (modelValue) | `Uint8Array` | `new Uint8Array(0)` | The currently visible data buffer. In buffer mode, this is the entire dataset; in window mode, this is the current slice. |
79
+ | `v-model:windowOffset` | `number` | `0` | Absolute byte offset of the current window. Automatically updates when scrolling, and scrolls the viewer when changed externally. Use for two-way sync of scroll position. |
80
+ | `v-model:cursorLocation` | `number \| null` | `null` | Current cursor position (absolute byte index). Only active when `cursor` prop is `true`. Automatically updates when navigating with keyboard/mouse. |
81
+
82
+ ## Events
83
+
84
+ VueHex emits several events to enable interactive features:
85
+
86
+ | Event | Payload | Description |
87
+ |-------|---------|-------------|
88
+ | `updateVirtualData` | `{ offset: number, length: number }` | Emitted when the component needs more data in windowed mode. Load the requested byte range and update `v-model` with the new data. |
89
+ | `byte-click` | `{ index: number, byte: number, kind: 'hex' \| 'ascii' }` | Emitted when a user clicks on a specific byte cell. `index` is the absolute byte position, `byte` is the value (0-255), and `kind` indicates whether the hex or ASCII column was clicked. |
90
+ | `selection-change` | `{ start: number \| null, end: number \| null, length: number }` | Emitted when the selection range changes. `start` and `end` are absolute byte positions (inclusive), or `null` if nothing is selected. `length` is the number of selected bytes. |
91
+ | `row-hover-on` / `row-hover-off` | `{ offset: number }` | Emitted when hovering over/leaving a row. |
92
+ | `hex-hover-on` / `hex-hover-off` | `{ index: number, byte: number }` | Emitted when hovering over/leaving a hex cell. |
93
+ | `ascii-hover-on` / `ascii-hover-off` | `{ index: number, byte: number }` | Emitted when hovering over/leaving an ASCII cell. |
94
+
95
+ ## Styling options
96
+
97
+ 1. Import `vuehex/styles` for the default look.
98
+ 2. Pass `theme="dark" | "light" | "terminal" | "sunset" | "auto"` to toggle bundled palettes explicitly (or skip the prop entirely to stick with OS detection).
99
+ 3. Roll your own styles targeting the emitted class names (`.vuehex`, `.vuehex-byte`, `.vuehex-ascii-char`, etc.). The default sheet sets `.vuehex { height: 100%; }`, so remember to give the wrapper a concrete height.
100
+
101
+ ## How VueHex handles large datasets
102
+
103
+ ### Technique 1/3 : Virtual Scrolling
104
+
105
+ Instead of rendering **all** rows in the DOM, which works fine for small files but quickly becomes unusable for large datasets. A 1 MB file with 16 bytes per row produces over 65,000 DOM nodes. A 100 MB file? 6.5 million nodes. Browsers slow to a crawl when manipulating or even keeping these trees in memory.
106
+
107
+ VueHex solves this with optimized **virtual scrolling**:
108
+
109
+ 1. **Calculate the full height** – VueHex computes how tall the entire table *would* be if every row were rendered (e.g., 65,000 rows × 24px = 1,560,000px).
110
+ 2. **Create a container of that height** – The scroll container's inner wrapper is sized to the full calculated height, so the scrollbar represents the entire dataset.
111
+ 3. **Render only visible rows** – As you scroll, VueHex calculates which rows are currently in the viewport (plus a small overscan buffer above/below) and renders **only** those rows.
112
+ 4. **Translate the table** – The rendered slice is positioned at the correct scroll offset using CSS `transform: translateY()` so it appears in the right place as you scroll.
113
+
114
+ **Result:** Instead of 65,000 DOM nodes for a 1 MB file, VueHex renders ~50 rows (what fits on your screen + overscan). Scrolling through a 100 MB file feels instant because the browser only ever manages a tiny fraction of the data.
115
+
116
+ ### Technique 2/3 : Chunking
117
+
118
+ The above technique works very well, but unfortunately it has a limit. Browsers impose limits on element dimensions to prevent rendering engine crashes. Most modern browsers cap `max-height` at around **33,554,432 pixels** (Chrome/Edge) or **17,895,698 pixels** (Firefox). Once your calculated scroll container height exceeds this limit, virtualization breaks—the scrollbar becomes inaccurate, and you can't reach data beyond the cap.
119
+
120
+ For a hex viewer with 16 bytes per row and 24px row height:
121
+ - **Firefox limit:** ~746,000 rows = ~11.4 MB of data
122
+ - **Chrome limit:** ~1.4 million rows = ~21.4 MB of data
123
+
124
+ Files larger than these thresholds need **chunking**:
125
+
126
+ 1. **Divide the dataset into chunks** – VueHex splits the file into manageable pieces (e.g., 20,000 rows per chunk).
127
+ 2. **Render one chunk at a time** – The scroll container only represents the *current chunk*, keeping its height safely below browser limits.
128
+ 3. **Provide chunk navigation** – Users click through chunks via the optional chunk navigator UI, and VueHex resets the scroll position and loads the next slice of data.
129
+
130
+ **Why this matters:**
131
+ - Without chunking, a 100 MB file would require a scroll container **416 million pixels tall**—far beyond any browser's rendering capabilities.
132
+ - With chunking (e.g., 10,000 rows per chunk = 240,000px per chunk), the same file becomes 6,400 navigable chunks, each comfortably under browser limits.
133
+
134
+ **When chunking activates:**
135
+ - VueHex automatically enables chunking when the calculated container height would exceed 8,000,000 pixels (a safe default below browser caps).
136
+ - You can disable chunking by setting `expand-to-content` (which removes virtualization entirely)
137
+
138
+ ### Technique 3/3 : Virtual Data
139
+
140
+ Even with virtualization and chunking, loading a 40 GB file entirely into memory isn't practical. VueHex supports **windowed data mode**, where:
141
+
142
+ 1. Your application keeps the full file in a storage backend.
143
+ 2. You provide VueHex with only the **currently needed slice** via `v-model` (e.g., 100 KB around the visible rows).
144
+ 3. When VueHex needs different bytes (because the user scrolled or jumped to a new chunk), it emits `updateVirtualData` with `{ offset, length }`.
145
+ 4. Your application fetches the requested slice and updates `v-model` asynchronously.
146
+
147
+ **Result:** VueHex can display terabyte-scale files while keeping browser memory usage under a few megabytes.
148
+
149
+ ---
150
+
151
+ ## Data modes
152
+
153
+ ### Virtual data mode
154
+
155
+ When VueHex needs different bytes it emits `updateVirtualData` with `{ offset, length }`. The component keeps rendering whatever you provide through `v-model` until you feed it a new slice. That means you can back the viewer with disk I/O, HTTP range requests, IndexedDB, or any other storage you control.
156
+
157
+ You can use `v-model:window-offset` for two-way binding of the current offset. This automatically:
158
+ - Updates the parent when the user scrolls to new data
159
+ - Scrolls the viewer when you change the offset programmatically
160
+
161
+ ```vue
162
+ <template>
163
+ <VueHex
164
+ v-model="windowData"
165
+ v-model:window-offset="windowOffset"
166
+ :total-size="fileSize"
167
+ data-mode="window"
168
+ @updateVirtualData="loadWindow"
169
+ />
170
+ </template>
171
+
172
+ <script setup>
173
+ const windowData = ref(new Uint8Array());
174
+ const windowOffset = ref(0);
175
+
176
+ async function loadWindow({ offset, length }) {
177
+ const data = await fetchBytesFromSource(offset, length);
178
+ windowData.value = data;
179
+ // windowOffset will be automatically updated by the component
180
+ }
181
+ </script>
182
+ ```
183
+
184
+ Alternatively, manually sync the offset when handling `updateVirtualData`:
185
+
186
+ ```vue
187
+ <template>
188
+ <VueHex
189
+ v-model="windowData"
190
+ :window-offset="windowOffset"
191
+ :total-size="fileSize"
192
+ @updateVirtualData="loadWindow"
193
+ />
194
+ </template>
195
+
196
+ <script setup>
197
+ async function loadWindow({ offset, length }) {
198
+ windowOffset.value = offset; // Manual sync
199
+ windowData.value = await fetchBytesFromSource(offset, length);
200
+ }
201
+ </script>
202
+ ```
203
+
204
+ ### Full data mode
205
+
206
+ If you already have the entire `Uint8Array`, you can skip the virtual data handshake entirely. Set `data-mode="buffer"` (or omit it entirely) and point `v-model` at the whole buffer.
207
+
208
+ ```vue
209
+ <VueHex v-model="entireFile" />
210
+ ```
211
+
212
+ For large datasets you'll probably still want to use the virtual data mode.
213
+
214
+ ### Expand-to-content mode
215
+
216
+ If you don't want VueHex to create its own scroll container, enable `expand-to-content`. In this mode the component grows to fit the rendered rows, so scrolling happens in the parent/page instead.
217
+
218
+ - No internal scrolling
219
+ - No virtualization / window requests
220
+ - `v-model` must contain the full `Uint8Array`
221
+
222
+ ```vue
223
+ <VueHex v-model="entireFile" expand-to-content :bytes-per-row="16" />
224
+ ```
225
+
226
+ This is useful for docs pages or print-style layouts, but avoid it for very large buffers.
227
+
228
+ ### Asynchronous providers
229
+
230
+ Responding to `updateVirtualData` can be asynchronous—just update `windowOffset` and `windowData` when the bytes arrive:
231
+
232
+ ```ts
233
+ async function handleUpdateVirtualData(request: VueHexWindowRequest) {
234
+ const response = await fetch(
235
+ `/api/blob?offset=${request.offset}&length=${request.length}`,
236
+ );
237
+ const arrayBuffer = await response.arrayBuffer();
238
+ windowOffset.value = request.offset;
239
+ windowData.value = new Uint8Array(arrayBuffer);
240
+ }
241
+ ```
242
+
243
+ ## Storybook workspace
244
+
245
+ Run Storybook to explore prebuilt demos:
246
+
247
+ ```bash
248
+ npm run storybook
249
+ ```
250
+
251
+ The static build lives in `storybook-static/` when you run `npm run storybook:build`.
252
+
253
+ ## Viewport sizing
254
+
255
+ VueHex virtualizes DOM rows, so make sure the component has a bounded height. Set `height`, `max-height`, or place it inside a flex/grid cell with a defined size. Without a viewport the table expands indefinitely and virtualization is effectively disabled.
256
+
257
+ ## Imperative scrolling
258
+
259
+ Call `scrollToByte` through a template ref to jump to absolute offsets:
260
+
261
+ ```vue
262
+ <VueHex ref="viewer" v-model="windowData" ... />
263
+
264
+ const viewer = ref<InstanceType<typeof VueHex> | null>(null);
265
+ viewer.value?.scrollToByte(0x1f400);
266
+ ```
267
+
268
+ ## Hover events
269
+
270
+ VueHex emits enter/leave events for rows, hex cells, and ASCII cells so you can power tooltips or side panels:
271
+
272
+ - `row-hover-on` / `row-hover-off` – `{ offset }`
273
+ - `hex-hover-on` / `hex-hover-off` – `{ index, byte }`
274
+ - `ascii-hover-on` / `ascii-hover-off` – `{ index, byte }`
275
+
276
+ ```vue
277
+ <VueHex
278
+ v-model="windowData"
279
+ v-model:window-offset="windowOffset"
280
+ @hex-hover-on="handleHexEnter"
281
+ @hex-hover-off="handleHexLeave"
282
+ />
283
+
284
+ function handleHexEnter(payload: { index: number; byte: number }) {
285
+ tooltip.open({ byteOffset: payload.index, value: payload.byte });
286
+ }
287
+
288
+ function handleHexLeave() {
289
+ tooltip.close();
290
+ }
291
+ ```
292
+
293
+ ## ASCII rendering overrides
294
+
295
+ The ASCII pane renders characters in the standard printable range (`0x20`–`0x7E`) by default. Override the behaviour with `is-printable` and `render-ascii` props:
296
+
297
+ ```vue
298
+ <VueHex
299
+ v-model="windowData"
300
+ :is-printable="(byte) => byte >= 0x30 && byte <= 0x39"
301
+ :render-ascii="(byte) => `${String.fromCharCode(byte)}`"
302
+ />
303
+ ```
304
+
305
+ VueHex also exports `VUE_HEX_ASCII_PRESETS` (`standard`, `latin1`, `visibleWhitespace`) if you want a drop-in configuration.
306
+
307
+ ## Chunk Navigator Customization
308
+
309
+ When using the chunk navigator (`show-chunk-navigator` prop), you can customize how the navigator header and chunk items are rendered using slots:
310
+
311
+ ### chunk-navigator-header
312
+
313
+ Customize the chunk navigator header with access to the chunks list and active index:
314
+
315
+ ```vue
316
+ <VueHex
317
+ v-model="windowData"
318
+ show-chunk-navigator
319
+ chunk-navigator-placement="right"
320
+ >
321
+ <template #chunk-navigator-header="{ chunks, activeIndex }">
322
+ <div class="custom-header">
323
+ <h3>File Chunks</h3>
324
+ <p>{{ chunks.length }} total | Active: {{ activeIndex + 1 }}</p>
325
+ </div>
326
+ </template>
327
+ </VueHex>
328
+ ```
329
+
330
+ **Slot props:**
331
+ - `chunks`: Array of all chunk descriptors
332
+ - `activeIndex`: Index of the currently active chunk
333
+
334
+ ### chunk-navigator-item
335
+
336
+ Customize individual chunk items with access to chunk data and active state:
337
+
338
+ ```vue
339
+ <VueHex
340
+ v-model="windowData"
341
+ show-chunk-navigator
342
+ chunk-navigator-placement="right"
343
+ >
344
+ <template #chunk-navigator-item="{ chunk, active, select }">
345
+ <div :class="{ 'my-chunk': true, 'active': active }">
346
+ <strong>{{ chunk.label }}</strong>
347
+ <span>{{ chunk.range }}</span>
348
+ <button @click="select">Jump</button>
349
+ </div>
350
+ </template>
351
+ </VueHex>
352
+ ```
353
+
354
+ **Slot props:**
355
+ - `chunk`: Object with `{ index: number, label: string, range: string }`
356
+ - `active`: Boolean indicating if this is the currently active chunk
357
+ - `select`: Function to programmatically select this chunk
358
+
359
+ Both slots are optional. If not provided, VueHex uses the default chunk navigator appearance.
360
+
361
+ ## Status Bar Slots
362
+
363
+ When using the status bar (`statusbar="top"` or `statusbar="bottom"`), you can provide custom content via slots. Use the `statusbarLayout` prop with the `"slot"` component name to control where your custom content appears.
364
+
365
+ ### Available slots
366
+
367
+ - `#statusbar-left` - Content for the left section
368
+ - `#statusbar-middle` - Content for the middle section
369
+ - `#statusbar-right` - Content for the right section
370
+
371
+ ### Example
372
+
373
+ ```vue
374
+ <VueHex
375
+ v-model="data"
376
+ statusbar="bottom"
377
+ :statusbar-layout="{
378
+ left: ['offset', 'slot', 'hex'],
379
+ middle: ['ascii'],
380
+ right: ['selection', 'slot']
381
+ }"
382
+ >
383
+ <template #statusbar-left>
384
+ <span>Mode: RO</span>
385
+ </template>
386
+
387
+ <template #statusbar-right>
388
+ <span>Endian: LE</span>
389
+ </template>
390
+ </VueHex>
391
+ ```
392
+
393
+ The `"slot"` entry in `statusbarLayout` determines where your custom content renders relative to built-in status bar items. You can place it at any position within each section array.
394
+
395
+ ## License
396
+
397
+ MIT © Vincent Vollers