@inkbar/core 0.10.0 → 0.12.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/README.md +155 -35
- package/dist/dom.d.mts +435 -16
- package/dist/dom.d.mts.map +1 -1
- package/dist/dom.mjs +2216 -144
- package/dist/dom.mjs.map +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +3 -3
- package/dist/index.mjs.map +1 -1
- package/dist/{sign-DZVDJhqT.mjs → sign-BSP7H3b7.mjs} +1799 -1479
- package/dist/sign-BSP7H3b7.mjs.map +1 -0
- package/dist/{sign-BTbtPHbM.d.mts → sign-UdMGeqRr.d.mts} +161 -13
- package/dist/sign-UdMGeqRr.d.mts.map +1 -0
- package/package.json +3 -3
- package/dist/sign-BTbtPHbM.d.mts.map +0 -1
- package/dist/sign-DZVDJhqT.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
|
|
21
21
|
---
|
|
22
22
|
|
|
23
|
-
`@inkbar/core` opens PDFs, renders pages with PDF.js and keeps annotations as typed data. The Vue, React and Svelte packages build on it. You can also use it on its own: `createViewer` puts a complete viewer, with comment
|
|
23
|
+
`@inkbar/core` opens PDFs, renders pages with PDF.js and keeps annotations as typed data. The Vue, React and Svelte packages build on it. You can also use it on its own: `createViewer` puts a complete viewer, with a comment thread popover and controls to build your toolbars from, into any element in a plain TypeScript app.
|
|
24
24
|
|
|
25
25
|
Every coordinate is stored in PDF user space (points, origin bottom-left, before `/Rotate`), so annotations stay put whatever the zoom, rotation or screen.
|
|
26
26
|
|
|
@@ -47,13 +47,15 @@ doc.annotations.add({ type: 'highlight', page: 0, quads })
|
|
|
47
47
|
const json = doc.exportAnnotations('json')
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
-
`file` can be a `File`, `Blob`, `Uint8Array`, a URL string, `{ url, key }` when the URL is signed and changes between loads, or `{ data, key }` for bytes you want to identify. `quads` is a list of quads in PDF user space.
|
|
50
|
+
`file` can be a `File`, `Blob`, `Uint8Array`, `ArrayBuffer`, a URL string, `{ url, key }` when the URL is signed and changes between loads, or `{ data, key }` for bytes you want to identify. `quads` is a list of quads in PDF user space.
|
|
51
|
+
|
|
52
|
+
A document that fails to open still resolves: `doc.status` turns to `{ state: 'error', error }` and `doc.ready` rejects with the `InkError`. Its `code` says why, such as `PasswordRequired` (open again with `password`), `InvalidPdf` or `SourceFetchFailed`. `isInkError(e)` tells an `InkError` from other errors.
|
|
51
53
|
|
|
52
54
|
`exportAnnotations('json')` returns a [Gloss](https://gloss-format.org) document, an open JSON format for PDF annotations, and `importAnnotations` reads one. Gloss has no loupe, mask, pen pressure or free-text border colour, so Inkbar writes them under `custom.inkbar`. It writes a loupe as a circle and a mask as a circle or square, so other Gloss readers can still draw them. Inkbar leaves your own keys under `custom` as they are. In the store and the API, loupes, masks, `pressure` and `borderColor` keep their plain names.
|
|
53
55
|
|
|
54
56
|
Writing annotations back into the PDF needs `@inkbar/engine`, which is not released yet. Until then `save()` rejects with `EngineUnavailable`. Persist `exportAnnotations('json')` instead and re-import it on the next open. `doc.key` stays the same between opens only when you open with a key: a URL string, `{ url, key }` or `{ data, key }`. A `File`, `Blob` or raw bytes get a new random key each time.
|
|
55
57
|
|
|
56
|
-
The DOM layer (`@inkbar/core/dom`) provides the viewport, canvas renderer, text layer, overlay and tools for hosts without a framework package. Vue hosts should use [`@inkbar/vue`](https://github.com/inkbar/inkbar/tree/main/packages/vue), React hosts [`@inkbar/react`](https://github.com/inkbar/inkbar/tree/main/packages/react), Svelte hosts [`@inkbar/svelte`](https://github.com/inkbar/inkbar/tree/main/packages/svelte).
|
|
58
|
+
The DOM layer (`@inkbar/core/dom`) provides the viewport, canvas renderer, text layer, overlay and tools for hosts without a framework package. Vue hosts should use [`@inkbar/vue`](https://github.com/inkbar/inkbar/tree/main/packages/vue), React hosts [`@inkbar/react`](https://github.com/inkbar/inkbar/tree/main/packages/react), Svelte hosts [`@inkbar/svelte`](https://github.com/inkbar/inkbar/tree/main/packages/svelte). Below `mountViewer` sit `createViewport`, `createPageRenderer`, `createTextLayer`, `createOverlay` and `createPageMount`, for a host that wires its own.
|
|
57
59
|
|
|
58
60
|
### Without a framework
|
|
59
61
|
|
|
@@ -111,7 +113,7 @@ viewer.open(lockedFile, { password }) // this password applies to this load only
|
|
|
111
113
|
viewer.destroy()
|
|
112
114
|
```
|
|
113
115
|
|
|
114
|
-
It takes the [`mountViewer` options](#mounting-a-viewer) except `container` and `document`, plus `source` and `assets` (both required), `password`, `defaults`, `history`, `annotations`, `fields`, `onReady` and `onError`. Pass `annotations: { mode: 'controlled', initial, onChange }` to own the session annotation list: `onChange` hears each proposed list, and you push it back with `viewer.document.get()?.annotations.setControlledValue(next)`. `initial` applies to every document the viewer opens, so a host that keeps its own list pushes it with `setControlledValue` in `onReady` for each new document. Undo and redo are inert in controlled mode. Set `threads` to `false` to leave out the thread popover. `viewer.status`, `viewer.document`, `viewer.engine`, `viewer.viewport` and `viewer.tools` are readables; the last two change when the engine rebuilds. `viewer.activated` is a readable too: the id of the annotation the user last clicked or activated with Enter, or `null`. `viewer.setActivated(id)` activates one from your code without calling `onActivate`; it ignores an id the document doesn't have, and `setActivated(null)` clears it. A password you pass to `open` replaces the `createViewer` one for that load. `viewer.download({ filename })` and `viewer.print()` download and print the open PDF, and Ctrl or Cmd+S and +P inside the root do the same; they do nothing while no document is open. The download is named by `doc.fileName`: the opened `File`'s name, else the URL's last segment, else `document.pdf`. Printing loads the file into a hidden frame and opens the browser's print dialog for it; on iOS the PDF opens in a new tab instead. Both use `doc.getData()`, which today is the file as it was opened, without this session's annotations. For a host of your own, `downloadDocument(doc, opts)`, `printDocument(doc)` and `attachFileKeys(el, () => doc)` do the same for any document. The controls are `createToolButton`, `createUndo`, `createRedo`, `createDownloadButton`, `createPrintButton`, `createZoomIn`, `createZoomOut`, `createZoomSelect`, `createPageInput`, `createPageCount`, `createPrevPage`, `createNextPage` and `createCommandButton`. Each one disables itself while no document is ready and lives as long as its viewer. Buttons show an icon from [`@inkbar/icons`](https://github.com/inkbar/inkbar/tree/main/packages/icons). Replace their children with your own icon and the accessible name stays; `createIcon` draws any `@inkbar/icons` icon for your own buttons. For a tool the viewer doesn't have, `createToolButton` and the highlight, shape and signature menus log `InkError('UnknownTool')`. The button renders disabled and the menus render nothing. `createToolbar` gives one Tab stop to all its buttons, with arrow keys between them, and `createToolbarSeparator` divides groups of controls in it. `createCommandButton(viewer, id)` places a plugin command: a `div.ink-command` holding a button named by the command's label (disabled with the command; while it runs, it keeps focus with `aria-busy` and `aria-disabled`, and a press does nothing) and a `span.ink-command-status[role="status"]` with its status text; for an id no plugin registered it logs an `InkError('UnknownCommand')` to the console, naming the id and the ones that are registered, and returns the same markup disabled and hidden (`aria-hidden`, no label). Pass `plugins: [...]` to `createViewer` to register plugins; their tools join the viewer's, and `createToolButton` takes a plugin tool's own label and icon. `viewer.commands.get(id)` returns a command. An input, select or textarea you add keeps its arrow keys and its own `tabindex`: it is a Tab stop unless you give it `tabindex="-1"`.
|
|
116
|
+
It takes the [`mountViewer` options](#mounting-a-viewer) except `container` and `document`, plus `source` and `assets` (both required), `password`, `defaults`, `history`, `annotations`, `fields`, `onReady` and `onError`. Pass `annotations: { mode: 'controlled', initial, onChange }` to own the session annotation list: `onChange` hears each proposed list, and you push it back with `viewer.document.get()?.annotations.setControlledValue(next)`. `initial` applies to every document the viewer opens, so a host that keeps its own list pushes it with `setControlledValue` in `onReady` for each new document. Undo and redo are inert in controlled mode. Set `threads` to `false` to leave out the thread popover. `viewer.status`, `viewer.document`, `viewer.engine`, `viewer.viewport` and `viewer.tools` are readables; the last two change when the engine rebuilds. `viewer.activated` is a readable too: the id of the annotation the user last clicked or activated with Enter, or `null`. `viewer.setActivated(id)` activates one from your code without calling `onActivate`; it ignores an id the document doesn't have, and `setActivated(null)` clears it. A password you pass to `open` replaces the `createViewer` one for that load. `viewer.download({ filename })` and `viewer.print()` download and print the open PDF, and Ctrl or Cmd+S and +P inside the root do the same; they do nothing while no document is open. The download is named by `doc.fileName`: the opened `File`'s name, else the URL's last segment, else `document.pdf`. Printing loads the file into a hidden frame and opens the browser's print dialog for it; on iOS the PDF opens in a new tab instead. Both use `doc.getData()`, which today is the file as it was opened, without this session's annotations. For a host of your own, `downloadDocument(doc, opts)`, `printDocument(doc)` and `attachFileKeys(el, () => doc)` do the same for any document. The controls are `createToolButton`, `createUndo`, `createRedo`, `createDownloadButton`, `createPrintButton`, `createZoomIn`, `createZoomOut`, `createZoomSelect`, `createPageInput`, `createPageCount`, `createPrevPage`, `createNextPage` and `createCommandButton`. `createHighlightMenu`, `createShapeMenu` and `createSignatureMenu` are split buttons for those tools, and `createColorMenu`, `createLineStyleMenu` and `createTextStyleMenu` edit the selection's style, or the active tool's when nothing is selected. Each one disables itself while no document is ready and lives as long as its viewer. Buttons show an icon from [`@inkbar/icons`](https://github.com/inkbar/inkbar/tree/main/packages/icons). Replace their children with your own icon and the accessible name stays; `createIcon` draws any `@inkbar/icons` icon for your own buttons. For a tool the viewer doesn't have, `createToolButton` and the highlight, shape and signature menus log `InkError('UnknownTool')`. The button renders disabled and the menus render nothing. `createToolbar` gives one Tab stop to all its buttons, with arrow keys between them, and `createToolbarSeparator` divides groups of controls in it. `createCommandButton(viewer, id)` places a plugin command: a `div.ink-command` holding a button named by the command's label (disabled with the command; while it runs, it keeps focus with `aria-busy` and `aria-disabled`, and a press does nothing) and a `span.ink-command-status[role="status"]` with its status text; for an id no plugin registered it logs an `InkError('UnknownCommand')` to the console, naming the id and the ones that are registered, and returns the same markup disabled and hidden (`aria-hidden`, no label). Pass `plugins: [...]` to `createViewer` to register plugins; their tools join the viewer's, and `createToolButton` takes a plugin tool's own label and icon. `viewer.commands.get(id)` returns a command. An input, select or textarea you add keeps its arrow keys and its own `tabindex`: it is a Tab stop unless you give it `tabindex="-1"`.
|
|
115
117
|
|
|
116
118
|
`createViewer` reads the theme's colours from the element you pass, so that element needs to be in the page when you call it.
|
|
117
119
|
|
|
@@ -139,9 +141,11 @@ Options:
|
|
|
139
141
|
- `container` and `document`, both required.
|
|
140
142
|
- `tools`: the creation tools to register (`allTools()` or the ones you import from `@inkbar/core/dom`). `select-text` and `select` are always present.
|
|
141
143
|
- `zoom` (a number, `'fit-width'` or `'fit-page'`; default `'fit-width'`), `page`, `mode`, `rotation` and `tool` set the starting view. Change them later through `engine.viewport` and `engine.tools`.
|
|
142
|
-
- `
|
|
144
|
+
- `fonts`: the font families text boxes offer (default: the PDF standard families).
|
|
145
|
+
- `gap`, `buffer`, `pinchZoom` and `customAnnotations` are fixed for a build. Change them with `engine.rebuild`.
|
|
146
|
+
- `pinchZoom` (default `true`): a two-finger pinch over the container, a trackpad pinch or ctrl+wheel zooms the document. One mouse wheel notch with ctrl held zooms by 10%. Two fingers also scroll the document. Set it to `false` and the browser handles pinches itself, zooming the whole page.
|
|
143
147
|
- `toolOptions` holds the tools' options, such as the shape kind. Pass one from `createToolOptions()` and keep it to carry the options over to the next document; `createViewer` keeps one for its whole life. The tool style (colours, line style, text style) belongs to the document: each new document starts from the theme's defaults.
|
|
144
|
-
- `
|
|
148
|
+
- `storage` is what the viewer keeps between visits, such as saved signatures; `signatures` is a store of your own for them (see [Signatures](#signatures)); `signatureFonts` lists the handwriting fonts a typed signature offers.
|
|
145
149
|
- `decorateAnnotation(annotation)` returns your own `data`, `classes` and `description` for an annotation's element, or `undefined`. `{ data: { 'my-flag': 'on' } }` writes `data-my-flag="on"`, and `description` becomes the element's `aria-description`. Inkbar re-applies them whenever the annotations change. Call `engine.redecorate()` when they depend on anything else. Core's own attributes and `ink-` classes always win.
|
|
146
150
|
- `onActivate(id)` fires when the user activates an annotation.
|
|
147
151
|
- `onBuild(viewport, tools)` fires after every build, the first one included, before `mountViewer` returns. Subscribe to the new viewport and tool controller here.
|
|
@@ -189,28 +193,30 @@ One gap remains:
|
|
|
189
193
|
|
|
190
194
|
## Tools and keyboard
|
|
191
195
|
|
|
192
|
-
`createToolController` from `@inkbar/core/dom` routes pointer and key events on the viewport to the active tool. Without `tools` it has `select-text` and `select`, plus whatever tools you pass; `allTools()` gives the other eight: `highlight`, `redact-area`, `sketch`, `draw`, `shape`, `freetext`, `signature` and `note`. Highlights, text redactions, notes and signatures can be made from the keyboard. Area redactions,
|
|
196
|
+
`createToolController` from `@inkbar/core/dom` routes pointer and key events on the viewport to the active tool. Without `tools` it has `select-text` and `select`, plus whatever tools you pass; `allTools()` gives the other eight: `highlight`, `redact-area`, `sketch`, `draw`, `shape`, `freetext`, `signature` and `note`. Highlights, text redactions, notes, shapes, text boxes and signatures can be made from the keyboard. Area redactions, freehand drawing, loupes and masks need a pointer.
|
|
193
197
|
|
|
194
198
|
Most creation tools are one-shot: `shape`, `freetext`, `note`, `redact-area` and `signature` each make one thing, select it, then hand back to `select-text`. After a one-shot tool has made something, a press on empty page or `Escape` clears the selection and switches to `select-text` without making anything more. The press that closes a new text box, or a new note's popover, counts as that press too. Until then, what it made keeps its edit handles: drag it to move it, drag a handle to resize it, and the tool stays on. A press on another annotation switches to `select-text` and selects it, and a drag moves it. Removing what the tool made (`Delete`, undo, or a controlled host refusing the add) leaves the tool on for another go, and the selection moving to something else switches to `select-text`. Picking a tool again starts it afresh. While the tool waits for that press, the viewport carries `data-ink-made`. `draw`, `sketch` and `highlight` are sticky: they stay on until you pick another tool.
|
|
195
199
|
|
|
196
200
|
A host tool says which it is with `kind: 'one-shot' | 'sticky'` (no `kind` is sticky). A one-shot tool adds and selects what it makes, then calls `ctx.made(ids)`; `ctx.done()` clears the selection and switches to `select-text` itself. Under a sticky tool, `ctx.made` does nothing. The controller's `active` readable reports every switch, including one a one-shot tool makes.
|
|
197
201
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
|
201
|
-
|
|
|
202
|
-
| Viewport, `
|
|
203
|
-
| Viewport, `
|
|
204
|
-
| Viewport
|
|
205
|
-
| Viewport
|
|
206
|
-
|
|
|
207
|
-
|
|
|
208
|
-
|
|
|
209
|
-
|
|
|
210
|
-
| Annotation | `
|
|
211
|
-
| Annotation | `
|
|
212
|
-
| Annotation | `
|
|
213
|
-
| Annotation | `
|
|
202
|
+
On a touch screen, `draw`, `sketch`, `shape`, `redact-area` and `freetext` take one-finger drags: one finger draws and two fingers scroll and zoom. A finger tap with `draw` or `sketch` draws nothing, while a pen tap or a mouse click makes a dot. A pen that lands while a palm rests on the screen takes over and draws. A one-shot tool gives one-finger scrolling back once it has made something. A host tool opts in with `claimsTouch: true`; while it is active, one finger no longer scrolls the document. For this the controller sets `touch-action` on the pages element (`[data-ink-pages]`) and removes it when the tool changes, so a value of your own there is lost.
|
|
203
|
+
|
|
204
|
+
| Where focus is | Key | Effect |
|
|
205
|
+
| ---------------------------- | --------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
|
|
206
|
+
| Viewport, `highlight` tool | `Enter` | Turns the text selection into a highlight |
|
|
207
|
+
| Viewport, `redact-area` tool | `Enter` | Turns the text selection into text redactions |
|
|
208
|
+
| Viewport, `note` tool | `Enter` | Places a note at the centre of the current page and focuses it |
|
|
209
|
+
| Viewport, `signature` tool | `Enter` | Places the chosen signature at the centre of the current page |
|
|
210
|
+
| Viewport | `+` or `=`, `-` | Zoom in, zoom out |
|
|
211
|
+
| Viewport | `Home`, `End`, `PageUp`, `PageDown` | First, last, previous, next page |
|
|
212
|
+
| Viewer root, toolbars too | `Ctrl` or `Cmd` + `Z` | Undo |
|
|
213
|
+
| Viewer root, toolbars too | `Ctrl` or `Cmd` + `Shift` + `Z`, `Ctrl` + `Y` | Redo |
|
|
214
|
+
| Annotation | `Enter` or `Space` | Selects and activates it (`Enter` edits a text box) |
|
|
215
|
+
| Annotation | `ArrowUp`, `ArrowDown` | Previous or next annotation on the page, in reading order |
|
|
216
|
+
| Annotation | `Shift` + arrow | Moves it 1 pt (`Shift` + `Alt` + arrow: 10 pt) |
|
|
217
|
+
| Annotation | `Ctrl` or `Cmd` + `Shift` + arrow | Resizes an area redaction, text box, shape, loupe, mask or signature image by 1 pt. Right and Down grow it, Left and Up shrink it |
|
|
218
|
+
| Annotation | `Delete` or `Backspace` | Removes it |
|
|
219
|
+
| Annotation | `Escape` | Clears the selection |
|
|
214
220
|
|
|
215
221
|
The tool controller and the overlay handle the tool and annotation keys. The zoom and page keys (`+`, `=`, `-`, `Home`, `End`, `PageUp`, `PageDown`) come from `attachViewportKeys`, which the tool controller does not install. Attach it to the same container:
|
|
216
222
|
|
|
@@ -232,9 +238,9 @@ import { attachHistoryKeys } from '@inkbar/core/dom'
|
|
|
232
238
|
const detach = attachHistoryKeys(root, () => doc.history)
|
|
233
239
|
```
|
|
234
240
|
|
|
235
|
-
An Undo or Redo button that undoes or redoes the last step disables itself, and the browser would move focus to `<body>`, out of reach of the keys. `createUndo`, `createRedo` and the binding buttons move
|
|
241
|
+
An Undo or Redo button that undoes or redoes the last step disables itself, and the browser would move focus to `<body>`, out of reach of the keys. Back and Forward do the same at either end of the history. `createUndo`, `createRedo`, `createBack`, `createForward` and the binding buttons move focus to the scroll element instead. For your own buttons, mark them `data-ink-undo`, `data-ink-redo`, `data-ink-back` or `data-ink-forward` and call `keepHistoryFocus(root, () => scrollElement)`.
|
|
236
242
|
|
|
237
|
-
On a focused annotation, `Enter` or `Space` selects it and calls `onActivate`; what activation opens is up to the host.
|
|
243
|
+
On a focused annotation, `Enter` or `Space` selects it and calls `onActivate`; what activation opens is up to the host. On an editable text box, `Enter` opens the editor instead.
|
|
238
244
|
|
|
239
245
|
Locked annotations, such as those embedded in the file, ignore move, resize and delete. Without a text selection, `Enter` in the `highlight` and `redact-area` tools does nothing.
|
|
240
246
|
|
|
@@ -244,9 +250,9 @@ Locked annotations, such as those embedded in the file, ignore move, resize and
|
|
|
244
250
|
|
|
245
251
|
The `shape` tool's `loupe` kind adds a loupe: a circle that shows the page under its centre at `magnification` times the size (2 by default), with a rim in the shape style's border colour and width. A drag sizes it, always as a circle, and a click places one 72 pt across. It needs a pointer: `Enter` places no loupe. It magnifies the page itself, not the annotations under it, and redraws a moment after it moves, resizes or the zoom changes. Pass the overlay a `document` (page mounts and the viewer do) or a loupe shows only its rim.
|
|
246
252
|
|
|
247
|
-
The `shape` tool's `mask` and `ovalMask` kinds add a mask: the page is dimmed everywhere outside a rectangle or an ellipse (`shape: 'rect' | 'ellipse'`), by `dim` (0 to 1, 0.5 by default) in black. A drag sizes it, with `Shift` for a square or circle, and a click places one 72 pt across. Like the loupe it needs a pointer: `Enter` places no mask. A page has one mask: adding a second replaces the first in the same undo step, and in a controlled store in the same `onChange`. The dimming is an SVG (`svg[data-ink-mask-layer]`) between the page and the annotation overlay that never takes the pointer; the mask annotation's own element is the shape, and only a thin band along its edge takes presses. Inside the shape the page works as usual: select text, highlight it, add shapes or notes. The style controls have nothing to change on a mask.
|
|
253
|
+
The `shape` tool's `mask` and `ovalMask` kinds add a mask: the page is dimmed everywhere outside a rectangle or an ellipse (`shape: 'rect' | 'ellipse'`), by `dim` (0 to 1, 0.5 by default) in the mask's `color` (black by default). A drag sizes it, with `Shift` for a square or circle, and a click places one 72 pt across. Like the loupe it needs a pointer: `Enter` places no mask. A page has one mask: adding a second replaces the first in the same undo step, and in a controlled store in the same `onChange`. The dimming is an SVG (`svg[data-ink-mask-layer]`) between the page and the annotation overlay that never takes the pointer; the mask annotation's own element is the shape, and only a thin band along its edge takes presses. Inside the shape the page works as usual: select text, highlight it, add shapes or notes. The style controls have nothing to change on a mask.
|
|
248
254
|
|
|
249
|
-
With either select tool (`select-text` or `select`), or on what a one-shot tool just made, a selected area redaction, text box, shape, loupe, mask, polyline, polygon or drawing shows four corner handles (`.ink-handle[data-handle="nw|ne|sw|se"]`), and a selected line shows one on each end (`data-handle="start|end"`). Dragging a corner resizes the annotation, scaling its points with it; `Shift` keeps its aspect ratio. Dragging a line end moves that end only. The store records a single update on release. Drag the annotation itself to move it, or any member of a multi-selection to move them all in one undo step. Text markup in the selection stays on its text unless it is the one you drag.
|
|
255
|
+
With either select tool (`select-text` or `select`), or on what a one-shot tool just made, a selected area redaction, text box, shape, loupe, mask, signature image, polyline, polygon or drawing shows four corner handles (`.ink-handle[data-handle="nw|ne|sw|se"]`), and a selected line shows one on each end (`data-handle="start|end"`). Dragging a corner resizes the annotation, scaling its points with it; `Shift` keeps its aspect ratio. Dragging a line end moves that end only. The store records a single update on release. Drag the annotation itself to move it, or any member of a multi-selection to move them all in one undo step. Text markup in the selection stays on its text unless it is the one you drag.
|
|
250
256
|
|
|
251
257
|
A press on text markup (a highlight, underline, strikeout or squiggly) does nothing at first. A click selects it; a drag selects the text under it, as a drag on the bare text would. Presses on every other annotation select it straight away.
|
|
252
258
|
|
|
@@ -301,7 +307,7 @@ Three rules cover undo and stale results:
|
|
|
301
307
|
|
|
302
308
|
A command's `run` gets `doc`, the document open when it started, and a `signal` that aborts when that document closes or another opens. Returning `undefined` clears the status. A failed run is logged and sets the status to `errorStatus` (default "Something went wrong."); the status clears when the document changes.
|
|
303
309
|
|
|
304
|
-
Marks redraw when their records change, undo and redo included, and keep their elements, so focus and an open card survive a zoom. Chain `.scrollToFirst()` onto `plugin.marks(…)` to scroll the viewer to new marks: when records start to get a mark, it scrolls to the first in reading order, unless that one is in view already. It moves no focus, and the records a document opens with, undo and redo never scroll. Each carries `data-record` and `data-plugin`; `style` (custom properties included), `className` and `attrs` add your own look. A card has a `title`, optional `label`, `meta`, `swatch` and `
|
|
310
|
+
Marks redraw when their records change, undo and redo included, and keep their elements, so focus and an open card survive a zoom. Chain `.scrollToFirst()` onto `plugin.marks(…)` to scroll the viewer to new marks: when records start to get a mark, it scrolls to the first in reading order, unless that one is in view already. It moves no focus, and the records a document opens with, undo and redo never scroll. Each carries `data-record` and `data-plugin`; `style` (custom properties included), `className` and `attrs` add your own look. A card has a `title`, optional `label`, `meta`, `swatch`, `body` and `excerpt` (a line of context with the match marked), and `actions`. Actions must be synchronous: a write after an `await` falls outside the action's undo step. `onActivate(record, mark)` runs when a mark is clicked or activated with `Enter` or `Space`, after its card opens. The first action is the primary one and takes focus. After an action that removes the mark, focus moves to the next of this `marks()` call's marks, else the previous one, else the viewport. A mark with a card has `aria-haspopup="dialog"`, and `aria-expanded` is true while its card is open. Escape or a press outside closes the card and returns focus to the mark. `@inkbar/theme` styles `.ink-card`, `.ink-card-title`, `.ink-card-meta`, `.ink-card-swatch`, `.ink-card-body`, `.ink-card-excerpt` and `.ink-card-actions`, and the card's `className` adds your own class.
|
|
305
311
|
|
|
306
312
|
Your marks' card also gives you a list. `createRecordList(viewer, { records: 'review.suggestions' })` (`InkRecordList` in the framework packages) lists the records that have a mark, each as the card the mark opens, with the same actions in the same undo step. See [Record lists](#record-lists).
|
|
307
313
|
|
|
@@ -317,7 +323,7 @@ Your marks' card also gives you a list. `createRecordList(viewer, { records: 're
|
|
|
317
323
|
const suggestions = viewer.records<Suggestion>('review.suggestions')
|
|
318
324
|
suggestions.list.subscribe((items) => render(items))
|
|
319
325
|
suggestions.batch(() => {
|
|
320
|
-
suggestions.update('s1', { status: '
|
|
326
|
+
suggestions.update('s1', { status: 'dismissed' })
|
|
321
327
|
suggestions.remove('s2')
|
|
322
328
|
})
|
|
323
329
|
```
|
|
@@ -326,7 +332,7 @@ suggestions.batch(() => {
|
|
|
326
332
|
|
|
327
333
|
The `plugin` that `setup` receives is also the plugin's context, so everything below works in the new form. The object form, `definePlugin({ id, setup(ctx) { … } })`, still works and returns the plugin itself: pass `plugins: [plugin]`.
|
|
328
334
|
|
|
329
|
-
A command added with `ctx.commands.add` is disabled until a document is ready, unless it passes its own `enabled` readable, and it is busy, and disabled, until the promise `run` returns settles. A failed run is logged, never thrown. `ctx.store(name)` gives each document its own record store, keyed `<plugin>.<name>`, and its changes undo with annotation edits; `ctx.onAnnotationEdit(fn)` runs inside each user edit's undo step. `ctx.history.batch(fn)` makes everything `fn` changes, annotations and records alike, one undo step. Load records with `initial(doc)` and save them from `onChange`. `ctx.scrollTo(page, rect)` scrolls the viewer to a rect in user space and moves no focus.
|
|
335
|
+
A command added with `ctx.commands.add` is disabled until a document is ready, unless it passes its own `enabled` readable, and it is busy, and disabled, until the promise `run` returns settles. A failed run is logged, never thrown. `ctx.store(name)` gives each document its own record store, keyed `<plugin>.<name>`, and its changes undo with annotation edits; `ctx.onAnnotationEdit(fn)` runs inside each user edit's undo step. `ctx.history.batch(fn)` makes everything `fn` changes, annotations and records alike, one undo step. Load records with `initial(doc)` and save them from `onChange`. `ctx.storage` is the other place to keep things: an `InkStorage` scoped to `plugin.<id>`, shared across documents and visits, for settings such as a panel width. It reads and writes the viewer's `storage`, so it is memory unless your app passes a store. Use `ctx.store()` for records that undo with edits. `ctx.scrollTo(page, rect)` scrolls the viewer to a rect in user space and moves no focus.
|
|
330
336
|
|
|
331
337
|
#### Plugin authors
|
|
332
338
|
|
|
@@ -337,6 +343,21 @@ A command added with `ctx.commands.add` is disabled until a document is ready, u
|
|
|
337
343
|
- Decorations from `ctx.decorate` re-apply when the annotations change. Call `ctx.redecorate()` when one reads state of your own that changed. Marks from `plugin.marks` take `refresh()` for the same job.
|
|
338
344
|
- The framework packages run `setup` once the viewer has mounted in the browser, never while rendering on the server. Until then the plugin's commands are disabled, and anything `setup` creates, such as a store, doesn't exist yet.
|
|
339
345
|
|
|
346
|
+
### Presses
|
|
347
|
+
|
|
348
|
+
`ctx.onPress(fn)` and `ctx.onLongPress(fn)` tell a plugin about a press on a page that nothing in Inkbar acted on. Each returns a function that stops listening, and listeners are removed when the plugin is torn down.
|
|
349
|
+
|
|
350
|
+
```ts
|
|
351
|
+
ctx.onPress(({ page, point, pointerType }) => console.log(page, point, pointerType))
|
|
352
|
+
ctx.onLongPress(({ page, point }) => openMenu(page, point))
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
The payload is `{ page, point, pointerType }`. `page` is a zero-based index, `point` is in user space on that page (points, origin bottom-left, before `/Rotate`), and `pointerType` is `'mouse'`, `'touch'` or `'pen'`.
|
|
356
|
+
|
|
357
|
+
A press is a pointer going down and up on a page without moving more than a few pixels. A mouse click is a press. A touch or pen held still for about half a second is a long press instead: it fires while the finger is still down, and the release reports nothing more. A mouse never long-presses. A touch that becomes part of a pinch before a long press fires is neither, and so is one that starts a scroll.
|
|
358
|
+
|
|
359
|
+
Both fire only under the text and rectangle select tools; any other tool owns every press. They stay silent when something else took the press: an annotation, a resize handle, a link, a form field, a plugin mark or a text editor. A press that commits an open text editor or closes a popover reports nothing either. Inkbar leaves the browser's own long-press behaviour alone, so text selection and context menus still work.
|
|
360
|
+
|
|
340
361
|
## Search
|
|
341
362
|
|
|
342
363
|
`doc.search(query, options?)` finds text across the document. The query is a string or a `RegExp`. It returns `SearchResults`: iterate it with `for await` to get hits as pages are scanned, or call `all()` for the lot.
|
|
@@ -360,7 +381,7 @@ Each `SearchHit` has:
|
|
|
360
381
|
- `exact`: `true` when every quad comes from where the page draws its glyphs. `false` when some of the text could not be matched to them (right-to-left or vertical text, or an unusual file) and its quad is cut at estimated widths, which can fall short. Pad those before using them to cover text, as a redaction does.
|
|
361
382
|
- `before`, `after`: up to 40 characters of page text either side.
|
|
362
383
|
|
|
363
|
-
`viewer.search` is the state and actions behind the viewer's find bar: `state` (a readable of `open`, `query`, `matchCase`, `hits`, `current` and `scanning`), `open(query?)`, `setQuery`, `setMatchCase`, `next`, `previous` and `close`. `open()` runs the search and draws the highlights even with no find bar on the page. Plugins get the same object as `ctx.search`.
|
|
384
|
+
`viewer.search` is the state and actions behind the viewer's find bar: `state` (a readable of `open`, `query`, `matchCase`, `hits`, `current` and `scanning`), `open(query?)`, `setQuery`, `setMatchCase`, `next`, `previous`, `select(index)` and `close`. `open()` runs the search and draws the highlights even with no find bar on the page. Plugins get the same object as `ctx.search`.
|
|
364
385
|
|
|
365
386
|
```ts
|
|
366
387
|
import { createFindBar, createSearchButton } from '@inkbar/core/dom'
|
|
@@ -410,7 +431,11 @@ Closed panels stay in the document with `hidden`, so their content keeps its sta
|
|
|
410
431
|
|
|
411
432
|
To render your own markup, use `createSidebarController(options)`, which holds the same state and behaviour. Spread `rootAttrs()` on the root, `tablistAttrs()` on the element holding the buttons, `triggerAttrs(id)` on each button and `panelAttrs(id)` on each panel, then call `bindTriggers(element)` once on an element containing the buttons. It handles clicks and keys and keeps the Tab stop right as buttons are re-rendered. `sidebarBadge(badge)` gives the text a badge shows, or `null` when it is hidden. Pass `idPrefix` when the markup is rendered on the server, so the element ids match on the client. A binding whose panels register one by one passes `pending: true` until they are all in, then `update({ pending: false })`: meanwhile a controlled `open` id not registered yet is kept instead of falling back, and the root reports the open state it will have. An uncontrolled sidebar created with no panels opens the first one to arrive.
|
|
412
433
|
|
|
413
|
-
|
|
434
|
+
Every sidebar has a resize handle on its inner edge. Drag it, or focus it and use the arrow keys (16px a step, mirrored on a right-hand sidebar so the arrow points the way the edge moves), Home and End. Enter or a double-click resets the width. The width starts at `defaultWidth` (320) and stays between `minWidth` (240) and `maxWidth` (480), and never past half of the sidebar's container. Pass `width` to control it and `onWidthChange` to hear about changes. With custom markup, spread `rootStyle()` on the root, `handleAttrs()` on the handle and `panelsAttrs()` on the panels element, then call `bindHandle(handle)`.
|
|
435
|
+
|
|
436
|
+
To keep the width between visits, pass `storage` (see the storage section) and a `name`. The width is saved under `sidebar.<name>.width`. Both are read once, when the sidebar is created.
|
|
437
|
+
|
|
438
|
+
`@inkbar/theme` sizes the sidebar with `--ink-sidebar-width` and the rail with `--ink-sidebar-rail-width` (48px). The sidebar sets `--ink-sidebar-width` inline, so a CSS override of it no longer applies. Pass `defaultWidth` instead. Override the rail per sidebar with CSS.
|
|
414
439
|
|
|
415
440
|
## Thumbnails
|
|
416
441
|
|
|
@@ -469,6 +494,47 @@ To render your own cards, build a source with `recordsSource({ pluginHost, viewp
|
|
|
469
494
|
|
|
470
495
|
`createThumbnail`, `createThumbnails`, `createRecordList` and `createSearchResults` follow the viewer and stop when it is destroyed, not when you remove their element. For a short-lived use, a dialog or a popover for instance, call `createThumbnailPainter(element, deps, options)` or `createRecordListController(source)` and its `destroy()` when you close it.
|
|
471
496
|
|
|
497
|
+
## Outline and links
|
|
498
|
+
|
|
499
|
+
`createOutline(viewer)` shows the document's outline (its bookmarks) as a tree. It fills its parent's height and scrolls, so put it in a sidebar panel or any box with a height.
|
|
500
|
+
|
|
501
|
+
```ts
|
|
502
|
+
import { createBack, createForward, createOutline } from '@inkbar/core/dom'
|
|
503
|
+
|
|
504
|
+
panel.append(createOutline(viewer, { emptyText: 'No sections' }))
|
|
505
|
+
toolbar.append(createBack(viewer), createForward(viewer))
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
Clicking an item, or Enter on it, goes to its destination without changing the zoom. Its chevron expands and collapses it. Items start expanded or collapsed as the file says, bold and italic titles keep their style, and the file's colours are ignored. Items Inkbar cannot follow, such as one pointing into another file, are greyed out with `aria-disabled`. The item the reader has reached is marked with `aria-current="location"` and scrolls into view unless focus is in the tree. It is the last item whose destination is at or above a line one third down the view, or at the bottom of the view once it is scrolled to the end, so the last sections can be reached. An item you jump to stays marked until you scroll away from it. Inside a collapsed parent, the parent is marked instead. With no outline the panel shows "This document has no outline", or your `emptyText`.
|
|
509
|
+
|
|
510
|
+
The tree follows the WAI-ARIA tree pattern and is one Tab stop. Up and Down move between items. Right expands an item, then moves to its first child. Left collapses an item, then moves to its parent. Home and End go to the first and last items, `*` expands the item's siblings, and typing jumps to the next title starting with what you typed.
|
|
511
|
+
|
|
512
|
+
Links in the PDF work without any setup. A click or tap follows one, and a drag that starts on a link still selects text. Drawing tools work over links, and a click with a modifier key held does not follow one. Each link is also a focusable element with a name, so Tab reaches links and form fields in reading order. Inkbar never opens a URL itself. An outline item or link with a URL calls `onAction` with `{ kind: 'open-url', url, newWindow }`, and only http, https, mailto and tel URLs get that far:
|
|
513
|
+
|
|
514
|
+
```ts
|
|
515
|
+
createViewer(el, {
|
|
516
|
+
source,
|
|
517
|
+
assets,
|
|
518
|
+
onAction: (a) => {
|
|
519
|
+
if (a.kind === 'open-url') window.open(a.url, '_blank', 'noopener')
|
|
520
|
+
},
|
|
521
|
+
})
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
Following a link or an outline item records where you were. `viewer.back()` and `viewer.forward()` move through those positions, `canGoBack` and `canGoForward` say whether they can, and `createBack(viewer)` and `createForward(viewer)` are toolbar buttons for them. Scrolling and the page buttons record nothing. The history keeps 50 positions and clears when another document opens. There are no keyboard shortcuts for it, so Alt+Left stays the browser's.
|
|
525
|
+
|
|
526
|
+
`doc.outline()` gives the outline as data: `OutlineItem`s with an `id` (its path, such as `"1.0"`), `title`, `target`, `bold`, `italic`, `open` and `children`. It is read on the first call and cached. To render your own tree, use `createOutlineController({ document, viewport, navigation })` with readables of the document, the viewport and the navigation. A `mountViewer` engine's `navigation` is a plain object, so wrap it, and the viewport too (a readable of your own should publish the new viewport after `engine.rebuild`):
|
|
527
|
+
|
|
528
|
+
```ts
|
|
529
|
+
const controller = createOutlineController({
|
|
530
|
+
document: { get: () => doc, subscribe: () => () => {} },
|
|
531
|
+
viewport: { get: () => engine.viewport, subscribe: () => () => {} },
|
|
532
|
+
navigation: { get: () => engine.navigation, subscribe: () => () => {} },
|
|
533
|
+
})
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
Spread `treeAttrs()` on the tree (a `ul`) and `itemAttrs(id)` on each item (an `li`). Each item holds a row, with `rowAttrs(id)`, holding the chevron and the title, which gets `titleAttrs(id)` and names the item, then its children in a `role="group"` list shown only while `expanded` has its id. The controller's JSDoc has the full markup. Pass `onKeydown` to the tree's keydown, call `activate(id)` on a row click and `toggle(id)` from the chevron, and call `bindScroll(scrollElement)` to keep the current item in view. `@inkbar/theme` styles `.ink-outline`, `.ink-outline-item`, `.ink-outline-row` (indented by `--ink-outline-level`), `.ink-outline-toggle` (a 44px column, a full touch target), `.ink-outline-title` and `.ink-outline-empty`, with the `--ink-outline-indent`, `--ink-outline-item-height` and `--ink-outline-current-bg` tokens.
|
|
537
|
+
|
|
472
538
|
## Forms
|
|
473
539
|
|
|
474
540
|
Inkbar fills the AcroForm fields in a PDF and runs the form's scripts. `doc.form` reads and sets the values:
|
|
@@ -476,23 +542,77 @@ Inkbar fills the AcroForm fields in a PDF and runs the form's scripts. `doc.form
|
|
|
476
542
|
```ts
|
|
477
543
|
doc.form.values.get() // { name: 'Ada', agree: 'Yes', ... }
|
|
478
544
|
doc.form.set({ name: 'Grace' }) // runs the scripts; one undo step
|
|
479
|
-
doc.form.reset() // every field back to the PDF's default
|
|
545
|
+
doc.form.reset() // every field back to the PDF's default; pass names to reset some
|
|
480
546
|
```
|
|
481
547
|
|
|
482
|
-
Pass `fields: { mode: 'controlled', initial, onChange }` to `openDocument` or `createViewer` to hold the values in your own state. The form never submits, opens a link, sends mail or prints. It hands each request to you: through `
|
|
548
|
+
Pass `fields: { mode: 'controlled', initial, onChange }` to `openDocument` or `createViewer` to hold the values in your own state. The form never submits, opens a link, sends mail or prints. It hands each request to you: through `onAction` on `createViewer` or `mountViewer` (`onFormAction` still works until 0.13.0), or through `doc.form.on('action', fn)` on a document you opened with `openDocument`.
|
|
483
549
|
|
|
484
550
|
Each fillable field has a light tint. `formHighlight: false` turns it off when the viewer opens. To change it later, call `viewer.setFormHighlight(on)` on a `createViewer` viewer, or `engine.setFormHighlight(on)` on a `mountViewer` engine. Neither renders a page again, and a focused field keeps its focus and typed text.
|
|
485
551
|
|
|
552
|
+
An empty signature field opens a menu of your saved signatures in `createViewer` (`createSignFieldMenu`). With `mountViewer`, handle `onSignField(req)` and call `signField` with the one picked. A script's `app.alert` shows in a message box (`createFormAlert`); pass `onFormAlert(message)` to show it yourself. A failing form script reaches `onError` as `FormScriptError`. `viewer.form` is a readable of the open document's form. `exportAnnotations('json')` also writes the form's values under `fields`, and `importAnnotations` restores them without an undo step.
|
|
553
|
+
|
|
486
554
|
The [forms guide](https://inkbar.dev/docs/guides/forms/) covers controlled fields, form actions, scripts, signature fields and saving values as Gloss.
|
|
487
555
|
|
|
488
556
|
## Signatures
|
|
489
557
|
|
|
490
558
|
A signature here is a mark on the page, not a cryptographic signature of the file. Draw one, type your name in a handwriting font, or upload a scan, and it is saved as a template. The `signature` tool places the chosen template: a click puts it about 150 pt wide, centred on the press, and `Enter` puts it at the centre of the current page. It is turned against the page's `/Rotate`, so it reads upright on the page; a view rotation turns it with the page. A drawn signature becomes `ink` and the others a `stamp` (an image), both with `subject: 'Signature'`. Both keep their aspect ratio when resized.
|
|
491
559
|
|
|
492
|
-
Your app owns the templates. Pass a `SignatureStore` (`list`, `save`, `remove`, each returning a promise) as `signatures
|
|
560
|
+
Your app owns the templates. They are kept in the viewer's `storage` under the key `signatures`. Pass `storage: createLocalStorage()` to keep them in `localStorage`, which falls back to memory where storage is blocked; the default is `createMemoryStorage()`, which forgets everything on reload. Any `InkStorage` works, including an async one that talks to your server. `createStorageSignatureStore(storage, key)` is the same store as a `SignatureStore`. A `SignatureStore` (`list`, `save`, `remove`, each returning a promise) passed as `signatures` still works and wins over `storage`, and `createLocalSignatureStore(key)` still works too. Both are deprecated in favour of `storage`, and 0.13.0 removes them. When storage is full, `save` rejects and the dialog says the signature was not saved. An uploaded image is scaled to at most 1000 px on its long side before it is stored. Typed signatures use the fonts in `signatureFonts`. Load each one with `@font-face` before offering it.
|
|
493
561
|
|
|
494
562
|
`createSignatureMenu(viewer)` lists the saved signatures, removes them, and opens `createSignatureDialog`, which draws, types or uploads a new one and, when the signature tool is registered, selects it.
|
|
495
563
|
|
|
564
|
+
## Storage
|
|
565
|
+
|
|
566
|
+
Inkbar keeps a few small things between visits, such as sidebar widths. Where they go is up to your app. Inkbar never touches `localStorage`, cookies or the network unless the store you pass does.
|
|
567
|
+
|
|
568
|
+
An `InkStorage` has three methods. Each may return a value or a promise, and values are JSON:
|
|
569
|
+
|
|
570
|
+
```ts
|
|
571
|
+
interface InkStorage {
|
|
572
|
+
get(key: string): unknown | Promise<unknown>
|
|
573
|
+
set(key: string, value: unknown): void | Promise<void>
|
|
574
|
+
remove(key: string): void | Promise<void>
|
|
575
|
+
}
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
Core ships two stores:
|
|
579
|
+
|
|
580
|
+
- `createMemoryStorage(initial?)` is the default. It forgets everything on reload and copies values in and out.
|
|
581
|
+
- `createLocalStorage(prefix = 'inkbar')` writes JSON to `localStorage` under `<prefix>:<key>`. It reads on every call, so another tab's write shows. It falls back to memory where storage is blocked. When storage is full, `set` rejects with an `Error` whose `cause` is the original, and the old value stays.
|
|
582
|
+
|
|
583
|
+
`scopeStorage(storage, prefix)` wraps a store so every key gains `<prefix>.`. Plugins get one scoped to `plugin.<id>`.
|
|
584
|
+
|
|
585
|
+
A store that talks to your server:
|
|
586
|
+
|
|
587
|
+
```ts
|
|
588
|
+
import type { InkStorage } from '@inkbar/core'
|
|
589
|
+
|
|
590
|
+
const rest: InkStorage = {
|
|
591
|
+
get: async (key) => {
|
|
592
|
+
const res = await fetch(`/api/prefs/${encodeURIComponent(key)}`)
|
|
593
|
+
return res.ok ? res.json() : undefined
|
|
594
|
+
},
|
|
595
|
+
set: async (key, value) => {
|
|
596
|
+
const res = await fetch(`/api/prefs/${encodeURIComponent(key)}`, {
|
|
597
|
+
method: 'PUT',
|
|
598
|
+
headers: { 'content-type': 'application/json' },
|
|
599
|
+
body: JSON.stringify(value),
|
|
600
|
+
})
|
|
601
|
+
// Reject so the signature dialog can say the signature was not saved.
|
|
602
|
+
if (!res.ok) throw new Error(`Saving ${key} failed: ${res.status}`)
|
|
603
|
+
},
|
|
604
|
+
remove: async (key) => {
|
|
605
|
+
await fetch(`/api/prefs/${encodeURIComponent(key)}`, { method: 'DELETE' })
|
|
606
|
+
},
|
|
607
|
+
}
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
The keys Inkbar uses:
|
|
611
|
+
|
|
612
|
+
- `signatures`: an array of signature templates.
|
|
613
|
+
- `sidebar.<name>.width`: a sidebar's width, a number in CSS pixels.
|
|
614
|
+
- `plugin.<id>.<key>`: whatever a plugin stores.
|
|
615
|
+
|
|
496
616
|
## Serving PDF.js assets
|
|
497
617
|
|
|
498
618
|
PDF.js needs a worker script plus directories of CMaps, standard fonts, wasm decoders and ICC profiles. Inkbar loads them from URLs you pass as the `assets` option. The option has no default, because the right URLs depend on how you deploy your app.
|