@orz/ui-hex 0.0.0-stage → 0.0.0
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 +29 -0
- package/README.md +536 -2
- package/dist/column-fit.d.ts +16 -0
- package/dist/column-fit.d.ts.map +1 -0
- package/dist/column-fit.js +22 -0
- package/dist/column-fit.js.map +1 -0
- package/dist/decode.d.ts +11 -0
- package/dist/decode.d.ts.map +1 -0
- package/dist/decode.js +63 -0
- package/dist/decode.js.map +1 -0
- package/dist/document.d.ts +75 -0
- package/dist/document.d.ts.map +1 -0
- package/dist/document.js +523 -0
- package/dist/document.js.map +1 -0
- package/dist/editing.d.ts +66 -0
- package/dist/editing.d.ts.map +1 -0
- package/dist/editing.js +202 -0
- package/dist/editing.js.map +1 -0
- package/dist/encoding-picker.d.ts +6 -0
- package/dist/encoding-picker.d.ts.map +1 -0
- package/dist/encoding-picker.js +84 -0
- package/dist/encoding-picker.js.map +1 -0
- package/dist/encodings.d.ts +19 -0
- package/dist/encodings.d.ts.map +1 -0
- package/dist/encodings.js +106 -0
- package/dist/encodings.js.map +1 -0
- package/dist/format.d.ts +29 -0
- package/dist/format.d.ts.map +1 -0
- package/dist/format.js +73 -0
- package/dist/format.js.map +1 -0
- package/dist/hex-editor.d.ts +27 -0
- package/dist/hex-editor.d.ts.map +1 -0
- package/dist/hex-editor.js +270 -0
- package/dist/hex-editor.js.map +1 -0
- package/dist/hex-grid.d.ts +102 -0
- package/dist/hex-grid.d.ts.map +1 -0
- package/dist/hex-grid.js +860 -0
- package/dist/hex-grid.js.map +1 -0
- package/dist/hex-inspector.d.ts +12 -0
- package/dist/hex-inspector.d.ts.map +1 -0
- package/dist/hex-inspector.js +68 -0
- package/dist/hex-inspector.js.map +1 -0
- package/dist/hex-paste.d.ts +26 -0
- package/dist/hex-paste.d.ts.map +1 -0
- package/dist/hex-paste.js +42 -0
- package/dist/hex-paste.js.map +1 -0
- package/dist/hex-view.d.ts +4 -0
- package/dist/hex-view.d.ts.map +1 -0
- package/dist/hex-view.js +6 -0
- package/dist/hex-view.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/navigation.d.ts +25 -0
- package/dist/navigation.d.ts.map +1 -0
- package/dist/navigation.js +100 -0
- package/dist/navigation.js.map +1 -0
- package/dist/page-cache.d.ts +45 -0
- package/dist/page-cache.d.ts.map +1 -0
- package/dist/page-cache.js +334 -0
- package/dist/page-cache.js.map +1 -0
- package/dist/source.d.ts +22 -0
- package/dist/source.d.ts.map +1 -0
- package/dist/source.js +31 -0
- package/dist/source.js.map +1 -0
- package/dist/text-decoding-multibyte.d.ts +17 -0
- package/dist/text-decoding-multibyte.d.ts.map +1 -0
- package/dist/text-decoding-multibyte.js +448 -0
- package/dist/text-decoding-multibyte.js.map +1 -0
- package/dist/text-decoding.d.ts +45 -0
- package/dist/text-decoding.d.ts.map +1 -0
- package/dist/text-decoding.js +106 -0
- package/dist/text-decoding.js.map +1 -0
- package/dist/types.d.ts +15 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +10 -0
- package/dist/types.js.map +1 -0
- package/dist/use-hex-document-state.d.ts +10 -0
- package/dist/use-hex-document-state.d.ts.map +1 -0
- package/dist/use-hex-document-state.js +22 -0
- package/dist/use-hex-document-state.js.map +1 -0
- package/package.json +53 -3
- package/styles.css +456 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright 2026 the orzui team
|
|
4
|
+
Copyright 2026 the todou team
|
|
5
|
+
|
|
6
|
+
Redistribution and use in source and binary forms, with or without
|
|
7
|
+
modification, are permitted provided that the following conditions are met:
|
|
8
|
+
|
|
9
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
10
|
+
list of conditions and the following disclaimer.
|
|
11
|
+
|
|
12
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
13
|
+
this list of conditions and the following disclaimer in the documentation
|
|
14
|
+
and/or other materials provided with the distribution.
|
|
15
|
+
|
|
16
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
17
|
+
contributors may be used to endorse or promote products derived from
|
|
18
|
+
this software without specific prior written permission.
|
|
19
|
+
|
|
20
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
21
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
22
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
23
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
24
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
25
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
26
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
27
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
28
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
29
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,537 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @orz/ui-hex
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A virtualised hex view over any byte source, in memory or paged; an editor over the same sources
|
|
4
|
+
that keeps its edits in memory and exports the result; and a data inspector that interprets the
|
|
5
|
+
bytes at the selection. Colours come from `@orz/ui`'s theme tokens, so every theme gets a matching
|
|
6
|
+
selection.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
pnpm add @orz/ui-hex @orz/ui react react-dom radix-ui lucide-react sonner
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
`@orz/ui`, `radix-ui` and React are peer dependencies; `lucide-react` and `sonner` are there because
|
|
15
|
+
`@orz/ui` declares them as its own peers. The encoding picker in the view's and the editor's header
|
|
16
|
+
is `@orz/ui`'s `FilterMenu`, and the inspector's endianness switch its `Switch`; both have to share
|
|
17
|
+
the app's radix copy.
|
|
18
|
+
|
|
19
|
+
## CSS
|
|
20
|
+
|
|
21
|
+
```css
|
|
22
|
+
@import "tailwindcss";
|
|
23
|
+
@import "@orz/ui/styles.css";
|
|
24
|
+
@import "@orz/ui-hex/styles.css";
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Import it after `@orz/ui/styles.css`. The stylesheet is plain CSS, scoped under `.orz-hex` and
|
|
28
|
+
`.orz-hex-inspector`, and every colour in it is derived from the theme tokens through these
|
|
29
|
+
variables:
|
|
30
|
+
|
|
31
|
+
| Variable | Default |
|
|
32
|
+
|---|---|
|
|
33
|
+
| `--hex-font` | `var(--font-mono)`, then the system monospace fonts |
|
|
34
|
+
| `--hex-row-height` | `1.375rem` |
|
|
35
|
+
| `--hex-fill` | `color-mix(in oklch, var(--primary) 32%, var(--card))` |
|
|
36
|
+
| `--hex-edge` | `var(--primary)` |
|
|
37
|
+
| `--hex-twin-fill` | `color-mix(in oklch, var(--foreground) 6%, var(--card))` |
|
|
38
|
+
| `--hex-twin-edge` | `var(--muted-foreground)` |
|
|
39
|
+
| `--hex-hover` | `var(--accent)` |
|
|
40
|
+
| `--hex-label` | `var(--muted-foreground)` |
|
|
41
|
+
| `--hex-faded` | `color-mix(in oklch, var(--muted-foreground) 70%, transparent)` |
|
|
42
|
+
| `--hex-modified` | `var(--destructive)` |
|
|
43
|
+
|
|
44
|
+
The defaults are declared on `.orz-hex` and `.orz-hex-inspector` themselves, so they resolve
|
|
45
|
+
against whichever theme is active above them. To override one, target those elements:
|
|
46
|
+
|
|
47
|
+
```css
|
|
48
|
+
.my-dump :is(.orz-hex, .orz-hex-inspector) {
|
|
49
|
+
--hex-row-height: 1.5rem;
|
|
50
|
+
--hex-hover: var(--secondary);
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The strong pair (`--hex-fill`, `--hex-edge`) marks the selection in the pane it was made in while
|
|
55
|
+
the view has focus; the twin pair marks it in the other pane, and in both once focus leaves.
|
|
56
|
+
`--hex-modified` colours the glyphs of bytes that came from editing, in `HexEditor` only; selection
|
|
57
|
+
and hover draw over it as usual.
|
|
58
|
+
|
|
59
|
+
### Font
|
|
60
|
+
|
|
61
|
+
The stylesheet loads no font. Any monospace font works. Control codes are drawn as two small
|
|
62
|
+
stacked letters (`NU`, `LF`, `CR`, …) rather than the U+2400 control pictures, so the grid stays
|
|
63
|
+
aligned in fonts that lack them, which is every webfont subset on fontsource. To use JetBrains Mono:
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
import "@fontsource-variable/jetbrains-mono";
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
```css
|
|
70
|
+
/* after the @orz/ui-hex/styles.css import */
|
|
71
|
+
.orz-hex,
|
|
72
|
+
.orz-hex-inspector {
|
|
73
|
+
--hex-font: "JetBrains Mono Variable", monospace;
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Byte sources
|
|
78
|
+
|
|
79
|
+
Every component reads through a `ByteSource`, the editor through a document that is one itself:
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
interface ByteSource {
|
|
83
|
+
/** Total length in bytes; read again after every change notification. */
|
|
84
|
+
readonly size: number;
|
|
85
|
+
/** Exactly min(length, size - offset) bytes, sync or as a promise. */
|
|
86
|
+
read(offset: number, length: number, signal?: AbortSignal): Uint8Array | Promise<Uint8Array>;
|
|
87
|
+
/** The bytes when they are at hand with no I/O, else undefined; never starts a read. */
|
|
88
|
+
peek?(offset: number, length: number): Uint8Array | undefined;
|
|
89
|
+
/** Content or size changed: the view drops its cache and re-reads what is visible. */
|
|
90
|
+
subscribe?(onChange: () => void): () => void;
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
The size must be known up front, because it sets the scroll extent. A source that holds bytes in
|
|
95
|
+
memory can answer `peek`, and a view then redraws from it at once after a change instead of showing
|
|
96
|
+
placeholders while it re-reads. `createHexDocument` learns whether a source is sync from its first
|
|
97
|
+
real read, and never reads zero bytes.
|
|
98
|
+
|
|
99
|
+
In memory, reads are subarrays and nothing is copied or cached; `peek` answers the same way:
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
import { bytesSource } from "@orz/ui-hex";
|
|
103
|
+
|
|
104
|
+
const source = bytesSource(new Uint8Array(await file.arrayBuffer()));
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
A `File` or any other `Blob` is read on demand, in 64 KiB pages:
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
import { blobSource } from "@orz/ui-hex";
|
|
111
|
+
|
|
112
|
+
const source = blobSource(file);
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Anything else is a source of your own. Over HTTP range requests:
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
import type { ByteSource } from "@orz/ui-hex";
|
|
119
|
+
|
|
120
|
+
async function httpSource(url: string): Promise<ByteSource> {
|
|
121
|
+
const head = await fetch(url, { method: "HEAD" });
|
|
122
|
+
const size = Number(head.headers.get("content-length"));
|
|
123
|
+
return {
|
|
124
|
+
size,
|
|
125
|
+
async read(offset, length, signal) {
|
|
126
|
+
if (length <= 0 || offset >= size) return new Uint8Array(0);
|
|
127
|
+
const end = Math.min(size, offset + length) - 1;
|
|
128
|
+
const response = await fetch(url, {
|
|
129
|
+
headers: { Range: `bytes=${offset}-${end}` },
|
|
130
|
+
signal,
|
|
131
|
+
});
|
|
132
|
+
if (response.status !== 206) throw new Error(`HTTP ${response.status}`);
|
|
133
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const source = await httpSource("https://app.example/dumps/core.bin");
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
The view asks for whole pages, shares one request between the rows that need a page, keeps up to
|
|
142
|
+
256 pages (16 MiB), and aborts the `signal` of a page that has scrolled more than a page out of
|
|
143
|
+
view. A failed page renders `??`, is not cached, and is asked for again when it next comes into
|
|
144
|
+
view; `onReadError(error, offset)` reports each failure. A `HexDocument` over an async source keeps
|
|
145
|
+
a page cache of the same size of its own, which survives edits, so an editor over a paged source
|
|
146
|
+
can hold up to twice that.
|
|
147
|
+
|
|
148
|
+
## `HexView` and `HexInspector`
|
|
149
|
+
|
|
150
|
+
```tsx
|
|
151
|
+
import { bytesSource, HexInspector, type HexSelection, HexView } from "@orz/ui-hex";
|
|
152
|
+
|
|
153
|
+
function Dump({ bytes }: { bytes: Uint8Array }) {
|
|
154
|
+
const source = useMemo(() => bytesSource(bytes), [bytes]);
|
|
155
|
+
const [selection, setSelection] = useState<HexSelection | null>(null);
|
|
156
|
+
return (
|
|
157
|
+
<div style={{ display: "flex", gap: "1.5rem", height: 480 }}>
|
|
158
|
+
<HexView source={source} selection={selection} onSelectionChange={setSelection} />
|
|
159
|
+
<HexInspector source={source} selection={selection} />
|
|
160
|
+
</div>
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Keep the source's identity stable: a new source object drops the view's cache.
|
|
166
|
+
|
|
167
|
+
A selection is `{ anchor, focus, pane }`, inclusive at both ends, so `anchor === focus` is one
|
|
168
|
+
selected byte; `null` is nothing. `pane` is `"hex"` or `"text"`, the pane the selection was made
|
|
169
|
+
in.
|
|
170
|
+
|
|
171
|
+
### `HexView` props
|
|
172
|
+
|
|
173
|
+
| Prop | Default | |
|
|
174
|
+
|---|---|---|
|
|
175
|
+
| `source` | | The bytes |
|
|
176
|
+
| `selection`, `onSelectionChange` | | Controlled selection |
|
|
177
|
+
| `defaultSelection` | `null` | Uncontrolled selection |
|
|
178
|
+
| `columns` | `16` | Bytes per row, or `"auto"` to fit the width (below) |
|
|
179
|
+
| `maxColumns` | `16` | The most columns `"auto"` picks |
|
|
180
|
+
| `groupSize` | `8` | Bytes between group gaps; `0` for none |
|
|
181
|
+
| `baseAddress` | `0` | Added to displayed addresses, never to offsets |
|
|
182
|
+
| `rows` | | A fixed viewport height in rows; omitted, the view fills its parent's height |
|
|
183
|
+
| `resizable` | `false` | A resize handle in the bottom corner: `true` for both ways, `"horizontal"` or `"vertical"` |
|
|
184
|
+
| `showAddresses`, `showColumnLabels`, `showText`, `showStatusBar` | `true` | |
|
|
185
|
+
| `uppercase` | `true` | Hex digit case |
|
|
186
|
+
| `fadeZeroBytes` | `true` | Fades `00` |
|
|
187
|
+
| `controlCodes` | `"abbreviations"` | Or `"dots"` |
|
|
188
|
+
| `encoding`, `onEncodingChange` | | Controlled text-pane encoding (below) |
|
|
189
|
+
| `defaultEncoding` | `"ascii"` | Uncontrolled text-pane encoding |
|
|
190
|
+
| `showEncodingPicker` | `true` | The encoding picker in the text pane's header |
|
|
191
|
+
| `onCopy` | | Reports each copy (below) |
|
|
192
|
+
| `onReadError` | | `(error, offset)` per failed page |
|
|
193
|
+
| `ref` | | `{ scrollToOffset(offset, align?), focus() }`; `align` is `"nearest"`, `"start"` or `"center"` |
|
|
194
|
+
| `className`, `aria-label` | `"Hex view"` | |
|
|
195
|
+
|
|
196
|
+
Without `rows`, give the parent a definite height. The view scrolls by whole rows; without `rows`,
|
|
197
|
+
the strip under the last whole row stays empty. Above roughly 13 MiB at 16 columns the content
|
|
198
|
+
is taller than browsers let an element be; the view then maps the scrollbar onto rows and moves a
|
|
199
|
+
few rows per wheel notch, so dragging the scrollbar becomes coarse but the wheel and keyboard stay
|
|
200
|
+
precise.
|
|
201
|
+
|
|
202
|
+
The view is as wide as its columns, and never wider than its container. With a fixed `columns`, a
|
|
203
|
+
narrower container scrolls it sideways. With `columns="auto"` it takes the width `maxColumns` need
|
|
204
|
+
and shows the widest of `maxColumns` and the powers of two below it, down to 4, that fits the width
|
|
205
|
+
it gets, so a narrow container or a narrow drag of the handle gets 8 or 4 columns instead of a
|
|
206
|
+
scrollbar; below 4 it scrolls. A resize keeps the byte at the start of the top row in the top row.
|
|
207
|
+
To cap the width in pixels rather than columns, put the view in a container with a `max-width`: the
|
|
208
|
+
stylesheet is unlayered, so its `max-width: 100%` on `.orz-hex` wins over a Tailwind class passed
|
|
209
|
+
through `className`.
|
|
210
|
+
|
|
211
|
+
`resizable` lets the user drag the view's size. A dragged height adds rows beyond `rows`, and a
|
|
212
|
+
dragged width overrides the content width, so a fixed `columns` scrolls or leaves space beside the
|
|
213
|
+
text pane while `"auto"` refits.
|
|
214
|
+
|
|
215
|
+
### `HexInspector` props
|
|
216
|
+
|
|
217
|
+
| Prop | Default | |
|
|
218
|
+
|---|---|---|
|
|
219
|
+
| `source`, `selection` | | Usually the view's |
|
|
220
|
+
| `littleEndian`, `onLittleEndianChange` | | Controlled endianness |
|
|
221
|
+
| `defaultLittleEndian` | `true` | Uncontrolled endianness |
|
|
222
|
+
| `className` | | |
|
|
223
|
+
|
|
224
|
+
It reads up to 8 bytes at the selection start and shows them as binary, Int8 to Int64, Uint8 to
|
|
225
|
+
Uint64, Float16, Float32 and Float64. A row wider than the selection is dimmed but still shows the
|
|
226
|
+
value read from the bytes after it; `—` marks a value that would run past the end of the source.
|
|
227
|
+
|
|
228
|
+
## Keyboard and copy
|
|
229
|
+
|
|
230
|
+
| Key | Moves the caret |
|
|
231
|
+
|---|---|
|
|
232
|
+
| ← / → | one byte |
|
|
233
|
+
| ↑ / ↓ | one row |
|
|
234
|
+
| PageUp / PageDown | one viewport |
|
|
235
|
+
| Home / End | start / end of the row |
|
|
236
|
+
| Ctrl/⌘ + Home / End | start / end of the source |
|
|
237
|
+
| Ctrl/⌘ + A | selects everything |
|
|
238
|
+
| Escape | clears the selection |
|
|
239
|
+
|
|
240
|
+
Shift extends the selection from its anchor. Tab leaves the view; clicking in the other pane
|
|
241
|
+
switches panes, and a drag may cross from one pane into the other.
|
|
242
|
+
|
|
243
|
+
Copy (Ctrl/⌘ + C, or the browser menu) writes the selection as space-separated hex pairs from the
|
|
244
|
+
hex pane, or as the text pane shows it from the text pane, up to 1 MiB. When
|
|
245
|
+
the bytes are not all loaded, the view reads them and writes through `navigator.clipboard`.
|
|
246
|
+
`onCopy` receives `{ ok: true, bytes }`, or `{ ok: false, reason }` with `"too-large"`,
|
|
247
|
+
`"read-failed"` or `"clipboard-unavailable"`; the view shows no toast itself. From the text pane,
|
|
248
|
+
a copy also writes the exact bytes as hex digits under `application/x-orz-hex` (`HEX_BYTES_TYPE`),
|
|
249
|
+
because its text shows an undecodable byte as a dot, which pastes back as `2E`; `navigator.clipboard`
|
|
250
|
+
writes text only, so a copy through it carries no exact bytes. `copyText`,
|
|
251
|
+
`hexByte`, `formatSize` and the other formatters are exported for copy actions of your own;
|
|
252
|
+
`copyText` takes the view's `encoding` (ASCII by default), and `copyRange` gives the byte range to
|
|
253
|
+
read for it.
|
|
254
|
+
|
|
255
|
+
## Encodings
|
|
256
|
+
|
|
257
|
+
The text pane decodes its bytes in one of these encodings, ASCII by default:
|
|
258
|
+
|
|
259
|
+
| Group | Encodings |
|
|
260
|
+
|---|---|
|
|
261
|
+
| Western | ASCII, ISO-8859-1, Windows-1252 |
|
|
262
|
+
| Unicode | UTF-8, UTF-16LE, UTF-16BE |
|
|
263
|
+
| East Asian | Shift_JIS, EUC-JP, GB18030 (which covers GBK), Big5, EUC-KR |
|
|
264
|
+
| Other | Windows-1250, 1251, 1253–1258; ISO-8859-2 to -8, -10, -13 to -16; KOI8-R, KOI8-U; IBM866; Macintosh |
|
|
265
|
+
|
|
266
|
+
The ids are the [Encoding Standard](https://encoding.spec.whatwg.org/) names (`"utf-8"`,
|
|
267
|
+
`"shift_jis"`, `"windows-1251"`, …), typed as `HexEncoding`. Which of them a runtime supports depends
|
|
268
|
+
on its `TextDecoder`: `HEX_ENCODINGS` lists the supported ones, each with its `id`, picker `label` and
|
|
269
|
+
`group`, in picker order. Node, for one, lacks ISO-8859-16. An id the runtime lacks renders as ASCII
|
|
270
|
+
and logs one warning.
|
|
271
|
+
|
|
272
|
+
The picker sits in the text pane's header and filters as you type: a query matches an encoding's
|
|
273
|
+
label, id or a common alias (`cp1252`, `sjis`, `latin2`, …), ignoring case and separators, so `utf8`
|
|
274
|
+
finds UTF-8. A row found by an alias shows that alias beside its label.
|
|
275
|
+
|
|
276
|
+
Hide it with `showEncodingPicker={false}`, and the header shows the encoding's name instead; with no
|
|
277
|
+
header (`showColumnLabels={false}`) or no text pane, the prop alone sets the encoding. A controlled
|
|
278
|
+
`encoding` without `onEncodingChange` makes the picker change nothing. To pass a detected charset
|
|
279
|
+
through:
|
|
280
|
+
|
|
281
|
+
```tsx
|
|
282
|
+
import { HEX_ENCODINGS, type HexEncoding, HexView } from "@orz/ui-hex";
|
|
283
|
+
|
|
284
|
+
function encodingFor(charset: string | undefined): HexEncoding {
|
|
285
|
+
const id = charset?.toLowerCase();
|
|
286
|
+
return HEX_ENCODINGS.find((info) => info.id === id)?.id ?? "utf-8";
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
<HexView source={source} defaultEncoding={encodingFor(detectedCharset)} />;
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
A multi-byte character is drawn once, centred over its bytes; one that crosses the end of a row is
|
|
293
|
+
drawn on its starting row, squeezed into the cells left there. Bytes that do not decode, control
|
|
294
|
+
codes and characters with no glyph of their own (format characters, combining marks, private use)
|
|
295
|
+
show as faded dots, as in ASCII.
|
|
296
|
+
|
|
297
|
+
- **Selection** stays per byte in both panes, so a selection may end inside a character.
|
|
298
|
+
- **Hover** lights up every byte of the character under the pointer, in both panes.
|
|
299
|
+
- **Copy** from the text pane writes what is shown: a character whose first byte is selected is
|
|
300
|
+
copied whole, one whose first byte lies before the selection is left out, and every dot is `.`.
|
|
301
|
+
|
|
302
|
+
Wide characters fall back to the platform's CJK fonts when `--hex-font` lacks them; how well they
|
|
303
|
+
look depends on the fonts the reader has installed.
|
|
304
|
+
|
|
305
|
+
## Editing
|
|
306
|
+
|
|
307
|
+
`HexEditor` edits a `HexDocument`: a piece table over a `ByteSource` that never writes to the
|
|
308
|
+
source. Edits stay in the document until the app saves them.
|
|
309
|
+
|
|
310
|
+
```tsx
|
|
311
|
+
import {
|
|
312
|
+
blobSource,
|
|
313
|
+
createHexDocument,
|
|
314
|
+
type HexDocument,
|
|
315
|
+
HexEditor,
|
|
316
|
+
useHexDocumentState,
|
|
317
|
+
} from "@orz/ui-hex";
|
|
318
|
+
import { useMemo } from "react";
|
|
319
|
+
|
|
320
|
+
function Editor({ file }: { file: File }) {
|
|
321
|
+
const doc = useMemo(() => createHexDocument(blobSource(file)), [file]);
|
|
322
|
+
const { dirty, canUndo, canRedo } = useHexDocumentState(doc);
|
|
323
|
+
return (
|
|
324
|
+
<>
|
|
325
|
+
<button type="button" disabled={!canUndo} onClick={() => doc.undo()}>Undo</button>
|
|
326
|
+
<button type="button" disabled={!canRedo} onClick={() => doc.redo()}>Redo</button>
|
|
327
|
+
<button type="button" disabled={!dirty} onClick={() => download(doc, file.name)}>Save</button>
|
|
328
|
+
<HexEditor document={doc} rows={24} />
|
|
329
|
+
</>
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
async function download(doc: HexDocument, name: string) {
|
|
334
|
+
const url = URL.createObjectURL(await doc.toBlob("application/octet-stream"));
|
|
335
|
+
const link = document.createElement("a");
|
|
336
|
+
link.href = url;
|
|
337
|
+
link.download = name;
|
|
338
|
+
link.click();
|
|
339
|
+
URL.revokeObjectURL(url);
|
|
340
|
+
doc.markSaved();
|
|
341
|
+
}
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
Keep the document's identity stable, as with a source: a new document starts with no edits and no
|
|
345
|
+
history. A document is itself a `ByteSource`, so a `HexInspector` given the same document and
|
|
346
|
+
selection follows the edits, and a `HexView` can show it read-only, without the modified colour.
|
|
347
|
+
|
|
348
|
+
### `HexDocument`
|
|
349
|
+
|
|
350
|
+
`createHexDocument(source, { resizable })` takes `resizable: false` to lock the size: then there is
|
|
351
|
+
no insert mode, Delete, Backspace and cut do nothing, and typing and pasting stop at the last byte.
|
|
352
|
+
|
|
353
|
+
| Member | |
|
|
354
|
+
|---|---|
|
|
355
|
+
| `size`, `read`, `peek`, `subscribe` | The edited bytes, as a `ByteSource` |
|
|
356
|
+
| `source`, `resizable` | What it was created with |
|
|
357
|
+
| `replace(offset, length, bytes)` | Replaces `length` bytes at `offset` |
|
|
358
|
+
| `insert(offset, bytes)`, `delete(offset, length)` | `replace` with length 0, or with no bytes |
|
|
359
|
+
| `overwrite(offset, bytes)` | Writes from `offset`; past the end it extends the document, or stops at the end when the size is locked |
|
|
360
|
+
| `undo()`, `redo()`, `canUndo`, `canRedo` | |
|
|
361
|
+
| `dirty`, `markSaved()` | |
|
|
362
|
+
| `modifiedRanges(offset, length)` | The bytes in the range that came from editing, as `[start, end)` pairs |
|
|
363
|
+
| `pieces()`, `toBlob(type?)`, `stream()` | Export (below) |
|
|
364
|
+
|
|
365
|
+
- **Bounds:** offsets and lengths are clamped to the document. A call left with nothing to remove
|
|
366
|
+
and nothing to write records no undo step; one that writes the bytes already there is still an
|
|
367
|
+
edit, with a step, the dirty state and the modified colour. With the size locked, a `replace`,
|
|
368
|
+
`insert` or `delete` that would change the size throws a `RangeError`; `HexEditor` never makes one.
|
|
369
|
+
- **Undo:** each call is one step, and so is each editor keystroke, except that a run of typing is
|
|
370
|
+
one step until the caret moves, the mode or pane changes, or the user pastes, deletes or saves.
|
|
371
|
+
Depth is unlimited. A step stores no source bytes, so undoing an overwrite far into a paged source
|
|
372
|
+
reads nothing. An edit after an undo drops the redo steps.
|
|
373
|
+
- **Memory:** every byte written is kept for the document's lifetime, undone and dropped edits
|
|
374
|
+
included, so it grows with the total amount edited. A new document over the saved bytes starts
|
|
375
|
+
empty.
|
|
376
|
+
- **Selection:** a step made in the editor records the selection before and after it, and the
|
|
377
|
+
editor restores it on undo and redo, including when the app calls `doc.undo()` from a toolbar.
|
|
378
|
+
After the app's own calls the editor only clamps its selection to the new size.
|
|
379
|
+
- **Dirty:** `markSaved()` records the current undo position; the document is dirty while its
|
|
380
|
+
position differs, so undoing back to the save point makes it clean again. A save point lost to a
|
|
381
|
+
dropped redo branch keeps the document dirty until the next `markSaved()`.
|
|
382
|
+
- **Modified colour:** it marks bytes that were written, so a deletion leaves no mark, and a
|
|
383
|
+
document whose only edits are deletions is dirty with nothing coloured. `markSaved()` keeps it; a
|
|
384
|
+
byte loses it only when its edit is undone. For a clean slate after saving, create a new document
|
|
385
|
+
over the saved bytes.
|
|
386
|
+
- **Source changes:** the source must not change while a document is open over it. A change it
|
|
387
|
+
notifies is passed on, so views re-read, but the document's pieces still point at the old
|
|
388
|
+
offsets. Create a new document over the changed source instead.
|
|
389
|
+
|
|
390
|
+
Export takes a snapshot of the document when called, so edits made while it runs do not reach it:
|
|
391
|
+
|
|
392
|
+
- **`toBlob(type?)`** slices the source's `blob` when it has one, as a `blobSource` does, and takes
|
|
393
|
+
subarrays of its `bytes`, as a `bytesSource`'s, reading nothing; so a multi-gigabyte `File` with a
|
|
394
|
+
few edits exports at once. A source of your own gets the same path by exposing a `blob` or `bytes`
|
|
395
|
+
property holding exactly its content. Any other source is read in 1 MiB chunks, all held in
|
|
396
|
+
memory until the `Blob` is built, and a failed read rejects; for a large one, use `stream()`.
|
|
397
|
+
- **`stream()`** is a pull-based `ReadableStream`, for a `FileSystemWritableFileStream` or an
|
|
398
|
+
upload. Each pull yields one run of edited bytes or the next 1 MiB of the source, so a slow
|
|
399
|
+
consumer never holds more than a chunk. Cancelling it aborts the read in flight.
|
|
400
|
+
- **`pieces()`** lists the document in order, as `{ kind: "source", offset, length }` ranges of the
|
|
401
|
+
source and `{ kind: "bytes", bytes }` runs of edited bytes. To write back only what changed, check
|
|
402
|
+
that every `source` piece's `offset` equals the document offset where it starts, which always
|
|
403
|
+
holds with the size locked; an insert and a delete elsewhere keep the size but move the bytes
|
|
404
|
+
between them, and then the whole document has to be written. The `bytes` are views onto the
|
|
405
|
+
document's own buffer, so do not write to them.
|
|
406
|
+
|
|
407
|
+
`useHexDocumentState(doc)` returns `{ size, dirty, canUndo, canRedo }` for a toolbar or a title, as
|
|
408
|
+
a new object only after the document changes.
|
|
409
|
+
|
|
410
|
+
### `HexEditor` props
|
|
411
|
+
|
|
412
|
+
Every `HexView` prop except `source`, and:
|
|
413
|
+
|
|
414
|
+
| Prop | Default | |
|
|
415
|
+
|---|---|---|
|
|
416
|
+
| `document` | | From `createHexDocument`; anything else throws a `TypeError` |
|
|
417
|
+
| `mode`, `onModeChange` | | Controlled mode, `"overwrite"` or `"insert"` |
|
|
418
|
+
| `defaultMode` | `"overwrite"` | Uncontrolled mode |
|
|
419
|
+
| `readOnly` | `false` | Refuses every edit from the keyboard, paste, cut and the mode button; navigation and copy still work, and edits the app makes to the document still show |
|
|
420
|
+
| `onPaste` | | Reports each paste (below) |
|
|
421
|
+
| `aria-label` | `"Hex editor"` | |
|
|
422
|
+
|
|
423
|
+
The `ref` is the same handle as `HexView`'s. On a document with a locked size the mode is always
|
|
424
|
+
overwrite.
|
|
425
|
+
|
|
426
|
+
The status bar shows an `OVR`/`INS` button before the size. It toggles the mode, as the Insert key
|
|
427
|
+
does, and is disabled when the size is locked or the editor is `readOnly`. Mac laptops have no
|
|
428
|
+
Insert key, so an app that hides the status bar puts its own toggle on `mode`.
|
|
429
|
+
|
|
430
|
+
While the editor has focus, the caret in the hex pane underlines the digit the next keystroke
|
|
431
|
+
writes. In insert mode it is a bar before the byte, where the next byte goes, except after a new
|
|
432
|
+
byte's first digit, when it underlines the second. In the text pane it underlines the whole cell,
|
|
433
|
+
or is a bar in insert mode.
|
|
434
|
+
|
|
435
|
+
A resizable document gets one empty cell after its last byte, `readOnly` or not. The caret reaches
|
|
436
|
+
it like any other cell, by click or by key, and typing there appends; copy, cut, Delete and
|
|
437
|
+
Ctrl/⌘ + A leave it out. So in a resizable document a selection's `focus` can equal `size`: clamp
|
|
438
|
+
to `size - 1` before counting bytes. An empty document shows only that cell, and an empty one with
|
|
439
|
+
a locked size shows nothing.
|
|
440
|
+
|
|
441
|
+
A hex-pane selection in the editor also carries `nibble`, `0` or `1`, the digit the next keystroke
|
|
442
|
+
writes. A controlled selection has to pass it back unchanged, or the caret jumps back to the high
|
|
443
|
+
digit in the middle of a byte.
|
|
444
|
+
|
|
445
|
+
### Editing keys
|
|
446
|
+
|
|
447
|
+
Navigation, selection and copy work as in [Keyboard and copy](#keyboard-and-copy). On top of them:
|
|
448
|
+
|
|
449
|
+
| Key | Overwrite mode | Insert mode |
|
|
450
|
+
|---|---|---|
|
|
451
|
+
| Hex digit, hex pane | Sets the digit under the caret, keeping the other; the caret moves to the next digit | The first digit inserts a byte before the caret, its low digit 0; the second sets that digit |
|
|
452
|
+
| Character, text pane | Writes its bytes from the caret | Inserts its bytes before the caret |
|
|
453
|
+
| Delete | Deletes the selection, or the byte under the caret | Same |
|
|
454
|
+
| Backspace | Deletes a selection of several bytes; at a byte's second hex digit, moves back to its first; else deletes the byte before the caret | Same |
|
|
455
|
+
| Insert | Toggles the mode | Same |
|
|
456
|
+
| Ctrl/⌘ + Z | Undo | Same |
|
|
457
|
+
| Ctrl/⌘ + Shift + Z, Ctrl/⌘ + Y | Redo | Same |
|
|
458
|
+
| Ctrl/⌘ + X | Cut (below) | Same |
|
|
459
|
+
| Ctrl/⌘ + V | Paste (below) | Same |
|
|
460
|
+
|
|
461
|
+
- **The mode governs typing and paste only.** Delete, Backspace and cut remove bytes in both modes,
|
|
462
|
+
so overwrite mode still has a way to remove a byte from the keyboard.
|
|
463
|
+
- **Delete and Backspace work on bytes**, in the text pane too: one Backspace after a multi-byte
|
|
464
|
+
character removes only its last byte, while undo takes back whole keystrokes.
|
|
465
|
+
- **Typing over a selection of several bytes** replaces it in insert mode, as one undo step; in
|
|
466
|
+
overwrite mode it writes from the selection's start. With the size locked, a character typed too
|
|
467
|
+
near the end is cut off there, so only part of a multi-byte character may be written.
|
|
468
|
+
- **A byte still loading or failed to read** counts as 0 when a typed digit keeps its other digit.
|
|
469
|
+
Nothing waits for the read.
|
|
470
|
+
- **Cut** copies the selection as a copy does and deletes it only once the copy succeeds, so a
|
|
471
|
+
selection over 1 MiB, or a copy that fails, deletes nothing and reports through `onCopy`. The
|
|
472
|
+
copy carries the exact bytes, so a cut and paste puts back what was cut, dots included. When
|
|
473
|
+
only text can go, through `navigator.clipboard` because the selection is not loaded, the cut
|
|
474
|
+
deletes only if that text pastes back into the same pane as the same bytes; otherwise it copies,
|
|
475
|
+
keeps the bytes and announces "Copied, not cut", while `onCopy` still reports `{ ok: true }`.
|
|
476
|
+
With the size locked or `readOnly`, cut neither copies nor reports.
|
|
477
|
+
- **Keys that do nothing:** typing, Delete, Backspace and cut while nothing is selected (undo, redo
|
|
478
|
+
and Insert still work); a character that is not a hex digit, in the hex pane; any editing key
|
|
479
|
+
when `readOnly`.
|
|
480
|
+
- **Modifiers:** keys held with Ctrl, ⌘ or Alt never type, though AltGr does. On macOS, ⌥ types
|
|
481
|
+
the characters it composes (ø, ¢…); ⌘⌥ and ⌃⌥ chords are left to shortcuts.
|
|
482
|
+
- **Input methods:** typing comes from `keydown`, so IME composition, for CJK text say, is not
|
|
483
|
+
supported. Paste such text instead.
|
|
484
|
+
|
|
485
|
+
### Typed and pasted text
|
|
486
|
+
|
|
487
|
+
The text pane's `encoding` (see [Encodings](#encodings)) also chooses what typed and pasted text is
|
|
488
|
+
written as. The default is ASCII, so until the encoding changes, a non-ASCII character typed into
|
|
489
|
+
the text pane does nothing and a paste containing one is refused as `"unencodable"`. The same holds
|
|
490
|
+
for any character the encoding cannot represent, and for a lone surrogate in every encoding. A paste
|
|
491
|
+
is all or nothing: one unencodable character refuses the whole text.
|
|
492
|
+
|
|
493
|
+
The East Asian encoders build their tables in idle time whenever an editor becomes editable (on
|
|
494
|
+
mount without `readOnly`, or when `readOnly` turns off) or its encoding changes, so the first
|
|
495
|
+
keystroke does not wait for them.
|
|
496
|
+
|
|
497
|
+
### Paste
|
|
498
|
+
|
|
499
|
+
Paste runs on the native `paste` event while the editor has focus, so Ctrl/⌘ + V, Shift + Insert
|
|
500
|
+
and a menu bar's Paste reach it; a right-click menu usually offers no Paste over the grid. In
|
|
501
|
+
either pane it writes the exact bytes from `application/x-orz-hex` when a copy put them there (see
|
|
502
|
+
[Keyboard and copy](#keyboard-and-copy)); when that type is missing or does not parse, it reads the
|
|
503
|
+
clipboard's plain text as below. `clipboardBytes` is the same logic, and `hexDigits` writes the
|
|
504
|
+
type's format, for copy and paste commands of your own. With nothing selected a paste does nothing
|
|
505
|
+
and does not call `onPaste`, and a paste into the encoding picker stays native.
|
|
506
|
+
|
|
507
|
+
- **Hex pane:** the text is read as a hex dump. Whitespace, commas, semicolons, brackets, braces,
|
|
508
|
+
parentheses and every `0x` / `\x` are dropped, so `89 50 4E 47`, `0x89, 0x50`, `{ 0x89, 0x50 }`,
|
|
509
|
+
`\x89\x50` and `89504e47` all paste. The remaining digits are joined and read two at a time, so
|
|
510
|
+
each byte needs both its digits: `0x5, 0x1` pastes the one byte `51`. `parseHexText` is the same
|
|
511
|
+
parser, for a paste command of your own.
|
|
512
|
+
- **Text pane:** the text is encoded, as above.
|
|
513
|
+
- **Insert mode:** the bytes replace a selection of several bytes, or go in before the caret.
|
|
514
|
+
- **Overwrite mode:** the bytes overwrite from the selection's start. Past the end they extend the
|
|
515
|
+
document; with the size locked they are cut off at the end.
|
|
516
|
+
|
|
517
|
+
The caret lands after the last pasted byte, or on the last byte when the size is locked and the
|
|
518
|
+
paste reaches the end, and the paste is one undo step. `onPaste` receives
|
|
519
|
+
`{ ok: true, bytes, truncated }`, where `bytes` counts what was written and `truncated` says the end
|
|
520
|
+
was cut off, or `{ ok: false, reason }`; the editor shows no toast itself.
|
|
521
|
+
|
|
522
|
+
| Reason | |
|
|
523
|
+
|---|---|
|
|
524
|
+
| `"not-hex"` | The hex pane's text has something besides hex digits and separators, or an odd number of digits |
|
|
525
|
+
| `"unencodable"` | The text pane's encoding cannot represent a character in it |
|
|
526
|
+
| `"empty"` | The clipboard has no plain text, or only separators |
|
|
527
|
+
| `"read-only"` | The editor is `readOnly` |
|
|
528
|
+
|
|
529
|
+
An app that wants separate paste-insert and paste-overwrite commands calls the document's `insert`
|
|
530
|
+
or `overwrite` itself, because the `paste` event carries no modifier keys.
|
|
531
|
+
|
|
532
|
+
## Entry points
|
|
533
|
+
|
|
534
|
+
| Import | Contents |
|
|
535
|
+
|---|---|
|
|
536
|
+
| `@orz/ui-hex` | `HexView`, `HexEditor`, `HexInspector`, `createHexDocument`, `useHexDocumentState`, `HEX_ENCODINGS`, `bytesSource`, `blobSource`, `parseHexText`, `clipboardBytes`, `hexDigits`, `HEX_BYTES_TYPE`, `copyText`, `copyRange`, `hexByte`, `formatAddress`, `formatOffset`, `formatSize`, `addressDigits`, `MAX_COPY_BYTES`, types |
|
|
537
|
+
| `@orz/ui-hex/styles.css` | the view, editor and inspector styles |
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Measured widths, in px, that a row's width is built from. */
|
|
2
|
+
export interface ColumnMetrics {
|
|
3
|
+
/** Everything but the columns: the address and the gap before the text pane. */
|
|
4
|
+
fixed: number;
|
|
5
|
+
/** One hex cell and its text-pane twin. */
|
|
6
|
+
column: number;
|
|
7
|
+
/** What a group-end cell adds over a plain one. */
|
|
8
|
+
groupGap: number;
|
|
9
|
+
groupSize: number;
|
|
10
|
+
}
|
|
11
|
+
/** The counts `columns="auto"` picks from, widest first: `max`, then the powers of two below it, down to 4. */
|
|
12
|
+
export declare function columnChoices(max: number): number[];
|
|
13
|
+
export declare function rowWidth(columns: number, m: ColumnMetrics): number;
|
|
14
|
+
/** The widest choice whose row fits `available` px; the narrowest when none does. */
|
|
15
|
+
export declare function fitColumns(available: number, max: number, m: ColumnMetrics): number;
|
|
16
|
+
//# sourceMappingURL=column-fit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"column-fit.d.ts","sourceRoot":"","sources":["../src/column-fit.ts"],"names":[],"mappings":"AAAA,gEAAgE;AAChE,MAAM,WAAW,aAAa;IAC5B,gFAAgF;IAChF,KAAK,EAAE,MAAM,CAAC;IACd,2CAA2C;IAC3C,MAAM,EAAE,MAAM,CAAC;IACf,mDAAmD;IACnD,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,+GAA+G;AAC/G,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAQnD;AAED,wBAAgB,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,CAAC,EAAE,aAAa,UAIzD;AAED,qFAAqF;AACrF,wBAAgB,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,CAAC,EAAE,aAAa,UAO1E"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** The counts `columns="auto"` picks from, widest first: `max`, then the powers of two below it, down to 4. */
|
|
2
|
+
export function columnChoices(max) {
|
|
3
|
+
const out = [max];
|
|
4
|
+
let n = 2 ** Math.ceil(Math.log2(max) - 1);
|
|
5
|
+
while (n >= 4 && n < max) {
|
|
6
|
+
out.push(n);
|
|
7
|
+
n /= 2;
|
|
8
|
+
}
|
|
9
|
+
return out;
|
|
10
|
+
}
|
|
11
|
+
export function rowWidth(columns, m) {
|
|
12
|
+
const groupEnds = m.groupSize > 0 ? Math.floor((columns - 1) / m.groupSize) : 0;
|
|
13
|
+
return m.fixed + columns * m.column + groupEnds * m.groupGap;
|
|
14
|
+
}
|
|
15
|
+
/** The widest choice whose row fits `available` px; the narrowest when none does. */
|
|
16
|
+
export function fitColumns(available, max, m) {
|
|
17
|
+
const choices = columnChoices(max);
|
|
18
|
+
// Half a pixel of slack: the widths come from rounded layout boxes.
|
|
19
|
+
return (choices.find((n) => rowWidth(n, m) <= available + 0.5) ??
|
|
20
|
+
choices.at(-1));
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=column-fit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"column-fit.js","sourceRoot":"","sources":["../src/column-fit.ts"],"names":[],"mappings":"AAWA,+GAA+G;AAC/G,MAAM,UAAU,aAAa,CAAC,GAAW;IACvC,MAAM,GAAG,GAAG,CAAC,GAAG,CAAC,CAAC;IAClB,IAAI,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;IAC3C,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,GAAG,EAAE,CAAC;QACzB,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACZ,CAAC,IAAI,CAAC,CAAC;IACT,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,OAAe,EAAE,CAAgB;IACxD,MAAM,SAAS,GACb,CAAC,CAAC,SAAS,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,OAAO,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAChE,OAAO,CAAC,CAAC,KAAK,GAAG,OAAO,GAAG,CAAC,CAAC,MAAM,GAAG,SAAS,GAAG,CAAC,CAAC,QAAQ,CAAC;AAC/D,CAAC;AAED,qFAAqF;AACrF,MAAM,UAAU,UAAU,CAAC,SAAiB,EAAE,GAAW,EAAE,CAAgB;IACzE,MAAM,OAAO,GAAG,aAAa,CAAC,GAAG,CAAC,CAAC;IACnC,oEAAoE;IACpE,OAAO,CACL,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,SAAS,GAAG,GAAG,CAAC;QACrD,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,CAAY,CAC3B,CAAC;AACJ,CAAC"}
|
package/dist/decode.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface DecodedValue {
|
|
2
|
+
label: string;
|
|
3
|
+
/** Bytes the value spans. */
|
|
4
|
+
width: number;
|
|
5
|
+
/** `null` when the value would run past the end of the source. */
|
|
6
|
+
value: string | null;
|
|
7
|
+
}
|
|
8
|
+
export declare function float16(bits: number): number;
|
|
9
|
+
/** One entry per inspector row, read from the first `available` bytes of `bytes`. */
|
|
10
|
+
export declare function decodeAll(bytes: Uint8Array, available: number, littleEndian: boolean): DecodedValue[];
|
|
11
|
+
//# sourceMappingURL=decode.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"decode.d.ts","sourceRoot":"","sources":["../src/decode.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,6BAA6B;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,kEAAkE;IAClE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAGD,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAO5C;AA+CD,qFAAqF;AACrF,wBAAgB,SAAS,CACvB,KAAK,EAAE,UAAU,EACjB,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,OAAO,GACpB,YAAY,EAAE,CAQhB"}
|