@inkbar/core 0.8.0 → 0.10.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 CHANGED
@@ -57,11 +57,13 @@ The DOM layer (`@inkbar/core/dom`) provides the viewport, canvas renderer, text
57
57
 
58
58
  ### Without a framework
59
59
 
60
- `createViewer` opens a document and renders it into an element, with a comment thread popover for notes and highlights. The toolbars are yours: build them from the controls below, which use the same markup as the framework packages, so `@inkbar/theme` styles them.
60
+ `createViewer` opens a document and renders it into an element, with a comment thread popover for notes and highlights. The toolbars are yours: build them from the controls below, which use the same markup as the framework packages, so `@inkbar/theme` styles them. A viewer registers only `select-text` and `select` until you pass `tools`; `allTools()` returns all eight creation tools if you want every one.
61
61
 
62
62
  ```ts
63
63
  import {
64
64
  createViewer,
65
+ highlightTool,
66
+ noteTool,
65
67
  createToolbar,
66
68
  createToolButton,
67
69
  createPageInput,
@@ -72,13 +74,19 @@ import {
72
74
  } from '@inkbar/core/dom'
73
75
  import '@inkbar/theme/theme.css'
74
76
 
75
- const viewer = createViewer(el, { source: file, assets, zoom: 'fit-width' })
77
+ const viewer = createViewer(el, {
78
+ source: file,
79
+ assets,
80
+ zoom: 'fit-width',
81
+ tools: [highlightTool(), noteTool()],
82
+ })
76
83
 
77
84
  const tools = createToolbar()
78
85
  tools.append(
79
86
  createToolButton(viewer, 'select-text'),
80
87
  createToolButton(viewer, 'select'),
81
- createToolButton(viewer, 'highlight')
88
+ createToolButton(viewer, 'highlight'),
89
+ createToolButton(viewer, 'note')
82
90
  )
83
91
 
84
92
  const undo = document.createElement('button')
@@ -103,7 +111,7 @@ viewer.open(lockedFile, { password }) // this password applies to this load only
103
111
  viewer.destroy()
104
112
  ```
105
113
 
106
- It takes the [`mountViewer` options](#mounting-a-viewer) except `container` and `document`, plus `source` and `assets` (both required), `password`, `defaults`, `history`, `onReady` and `onError`. 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. The controls are `createToolButton`, `createUndo`, `createRedo`, `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. `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"`.
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"`.
107
115
 
108
116
  `createViewer` reads the theme's colours from the element you pass, so that element needs to be in the page when you call it.
109
117
 
@@ -112,12 +120,13 @@ It takes the [`mountViewer` options](#mounting-a-viewer) except `container` and
112
120
  `mountViewer` wires the viewport, page layers, tools and zoom and page keys onto a scroll container. You render the chrome around it.
113
121
 
114
122
  ```ts
115
- import { mountViewer } from '@inkbar/core/dom'
123
+ import { mountViewer, highlightTool } from '@inkbar/core/dom'
116
124
 
117
125
  const engine = mountViewer({
118
126
  container, // a focusable, scrollable element
119
127
  document: doc,
120
128
  zoom: 'fit-width',
129
+ tools: [highlightTool()],
121
130
  onPageError: (page, error) => console.warn(page, error),
122
131
  })
123
132
  engine.tools.set('highlight')
@@ -128,6 +137,7 @@ engine.destroy()
128
137
  Options:
129
138
 
130
139
  - `container` and `document`, both required.
140
+ - `tools`: the creation tools to register (`allTools()` or the ones you import from `@inkbar/core/dom`). `select-text` and `select` are always present.
131
141
  - `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`.
132
142
  - `gap`, `buffer` and `customAnnotations` are fixed for a build. Change them with `engine.rebuild`.
133
143
  - `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.
@@ -179,7 +189,7 @@ One gap remains:
179
189
 
180
190
  ## Tools and keyboard
181
191
 
182
- `createToolController` from `@inkbar/core/dom` routes pointer and key events on the viewport to the active tool: `select-text`, `select`, `highlight`, `redact-area`, `sketch`, `draw`, `shape`, `freetext`, `signature` or `note`. Highlights, text redactions, notes and signatures can be made from the keyboard. Area redactions, sketches and drawings need a pointer.
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, sketches and drawings need a pointer.
183
193
 
184
194
  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.
185
195
 
@@ -293,10 +303,25 @@ A command's `run` gets `doc`, the document open when it started, and a `signal`
293
303
 
294
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 `body`, and `actions`. Actions must be synchronous: a write after an `await` falls outside the action's undo step. 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` and `.ink-card-actions`, and the card's `className` adds your own class.
295
305
 
306
+ 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
+
296
308
  `suggestions.annotates('redact', { from, onRemoved })` links records to the annotations made from them. Its `add(record)` adds an annotation from `from(record)`, stores the link at `custom.<plugin>.<records name>` (here `custom.review.suggestions`) and fills the type's default colour when `from` gives none. `of(record)` returns the linked annotations. `onRemoved(record, annotation)` and `onChanged(record, annotation)` run inside the user's undo step, never on undo or redo. Call it in `setup`. The link travels with the annotation through a Gloss export and import.
297
309
 
298
310
  `quadsFromRect(rect)` and `rectFromQuads(quads)` from `@inkbar/core` convert between a box and the quads that text markup takes, and `doc.pageText(page)` resolves to a page's text joined the way the page reads: nothing between touching items, a space at line ends and at wide gaps.
299
311
 
312
+ ### Reading records from your app
313
+
314
+ `viewer.records(key)` gives your app the records a plugin keeps, under the key `<plugin>.<name>` (`review.suggestions`). It is the plugin's own handle less `annotates`, and it follows the open document: `list` is a readable that emits as records change, and `[]` while nothing is open. The same key returns the same handle. Writes are undo steps like the plugin's own, and `batch(fn)` makes several one step. An unknown key returns an empty handle that ignores writes, and logs a warning once.
315
+
316
+ ```ts
317
+ const suggestions = viewer.records<Suggestion>('review.suggestions')
318
+ suggestions.list.subscribe((items) => render(items))
319
+ suggestions.batch(() => {
320
+ suggestions.update('s1', { status: 'accepted' })
321
+ suggestions.remove('s2')
322
+ })
323
+ ```
324
+
300
325
  ### Going further
301
326
 
302
327
  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]`.
@@ -350,6 +375,100 @@ Ctrl/Cmd+F opens the bar when one is mounted and the key comes from inside the v
350
375
 
351
376
  Hits are drawn as a layer over the page that multiplies with it, so the text shows through. `@inkbar/theme` styles them with `--ink-search-hit` and, for the current hit, `--ink-search-current`.
352
377
 
378
+ ## Sidebars
379
+
380
+ `createSidebar` builds a sidebar of panels, each showing an element you give it. It needs no document or viewer: place it beside the viewer with your own flex or grid layout.
381
+
382
+ ```ts
383
+ import { createSidebar } from '@inkbar/core/dom'
384
+ import { Square } from '@inkbar/icons'
385
+
386
+ const { element, controller } = createSidebar({
387
+ layout: 'rail',
388
+ side: 'left',
389
+ panels: [
390
+ { id: 'pages', label: 'Pages', icon: Square },
391
+ { id: 'notes', label: 'Notes', badge: 3 },
392
+ ],
393
+ content: { pages: pagesElement, notes: notesElement },
394
+ onOpenChange: (open) => console.log(open),
395
+ })
396
+ shell.prepend(element)
397
+ ```
398
+
399
+ Each panel has an `id`, a `label`, an optional `icon` (an `IconNode` from `@inkbar/icons`) and an optional `badge`, a number or short string hidden when empty or 0. In the rail, the open panel shows its label as a title above its content. Pass `visible: false`, or call `controller.update({ visible })`, to hide the whole sidebar and keep its open panel, content and scroll for when it shows again: a results sidebar shown only while a search has hits, for instance.
400
+
401
+ `layout` is one of:
402
+
403
+ - `rail` (default): a vertical strip of icon buttons on the inner edge, beside the document, with the label as a tooltip. One panel is open at a time; activating the open panel's button closes it, leaving only the rail.
404
+ - `tabs`: labelled tabs across the top with one panel open. Pass `open: null` to collapse the sidebar, from a toolbar button of your own for instance.
405
+ - `stack`: every panel as a section with a header button, one under another, any number expanded.
406
+
407
+ `side` (`left` by default) decides which edge the rail and border sit on. `open` is the open panel id (`string | null`) for rail and tabs, and the expanded ids (`string[]`) for stack. Without it the sidebar starts with the first panel open and keeps its own state. With it the sidebar is controlled: a click reports the new value through `onOpenChange` and nothing changes until you call `controller.update({ open })`. When `update({ panels })` removes the open panel, rail and tabs open the first one left and stack drops it; the new value goes to `onOpenChange` either way.
408
+
409
+ Closed panels stay in the document with `hidden`, so their content keeps its state and scroll position. Rail and tabs follow the WAI-ARIA tabs pattern: one Tab stop, arrow keys (Up and Down on the rail, Left and Right for tabs), Home and End to move, Enter or Space to open. Stack headers follow the accordion pattern: each is a Tab stop with `aria-expanded`, and Up and Down move between them.
410
+
411
+ 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
+
413
+ `@inkbar/theme` sizes the sidebar with `--ink-sidebar-width` (280px) and the rail with `--ink-sidebar-rail-width` (48px). Override either per sidebar with CSS.
414
+
415
+ ## Thumbnails
416
+
417
+ `createThumbnail(viewer, { page })` draws one page of the open document as a small image, with its annotations and plugin marks on top. Put it anywhere: a sidebar panel, a dialog, a card.
418
+
419
+ ```ts
420
+ import { createThumbnail } from '@inkbar/core/dom'
421
+
422
+ panel.append(createThumbnail(viewer, { page: 3, width: 120 }))
423
+ ```
424
+
425
+ `page` is 0-based. Give `width` or `height` in CSS pixels and the other follows the page's shape and its own /Rotate (the viewer's rotation does not apply); `width` wins when you give both, and with neither the width is 120px. `annotations: false` and `marks: false` leave those layers out. `label` replaces the accessible name, "Page 4" for page 3.
426
+
427
+ A thumbnail is an image to assistive technology (`role="img"`), with no focus and no click behaviour of its own: wrap it in a button or link for that. It renders once it comes within 200px of the visible area and shows a placeholder of the right size before then, and while no document is open. Form fields show the values saved in the PDF. Annotations follow edits, undo and redo, and marks are drawn with the plugin's class and style but take no presses.
428
+
429
+ To paint into your own element, call `createThumbnailPainter(element, { document, pluginHost }, options)` with readables of the document and, for marks, the plugin host. It adds the `ink-thumbnail` class, its role, name, size and layers, and leaves the element's own classes and children alone. `update(options)` applies changed options and `destroy()` removes everything it added. `thumbnailSize(pageInfo, { width, height })` gives the size and zoom a thumbnail of that page gets.
430
+
431
+ ### A list of pages
432
+
433
+ `createThumbnails(viewer, options)` lists the pages as thumbnails, each a button that scrolls the viewer to its page. It fills its parent's height and scrolls, so put it in a sidebar panel or any box with a height.
434
+
435
+ ```ts
436
+ import { createThumbnails } from '@inkbar/core/dom'
437
+
438
+ panel.append(createThumbnails(viewer))
439
+ panel.append(createThumbnails(viewer, { pages: [0, 3, 7], width: 96 }))
440
+ ```
441
+
442
+ `pages` lists those pages in that order (0-based); without it every page is listed. The thumbnails fill the list's width less a 16px gutter each side and follow it as it resizes; `width` fixes them at that many pixels instead. `annotations` and `marks` work as they do for one thumbnail. The current page has `aria-current="page"` and scrolls into view in the list when it changes, unless the list was scrolled by hand in the last second. The list is one Tab stop: arrow keys move between pages, Home and End go to the first and last, Enter or Space opens one.
443
+
444
+ Only the items near the visible part of the list are in the document, so a 500-page file stays cheap. When the list scrolls away from the focused item, focus moves to the first visible one without scrolling back.
445
+
446
+ To render your own items, use `createThumbnailsController({ document, pluginHost, viewport }, options)`. Its `state` gives the `pages` listed, the `current` page (-1 with no viewport), the `window` of indexes into `pages` to mount the `offsets` of each item from the top of the list (the last is the total height) and the `width` to draw each thumbnail at. Render a spacer of the total height and each item in the window at its offset, as a button with `data-ink-thumbnail-item` set to its page and a `createThumbnailPainter` at that `width` inside, wrapped in an element with `itemAttrs(index)` spread on it: its `listitem` role, and `aria-setsize` and `aria-posinset` for its place among all the pages, as only the window is mounted. An item's height is its thumbnail's plus 38px for the 16px gutter above it, a 6px gap and its 16px number, as the theme draws it. Call `attach(scrollElement)` once the list is in the document: it follows scrolling, handles clicks and keys, and sets each item's `tabindex`. `activate(page)` scrolls the viewer to a page, and `update(options)` takes new options.
447
+
448
+ ## Record lists
449
+
450
+ `createRecordList(viewer, { records })` lists a plugin's records as cards, the same cards their marks open, with the same actions. It fills its parent's height and scrolls, so put it in a sidebar panel or any box with a height.
451
+
452
+ ```ts
453
+ import { createRecordList, createSearchResults } from '@inkbar/core/dom'
454
+
455
+ panel.append(createRecordList(viewer, { records: 'review.suggestions' }))
456
+ ```
457
+
458
+ Each record takes its card from the first `plugin.marks` call over those records whose `where` it passes, and the list shows only records that pass one. Records that no `marks` call covers are all listed, titled with their id, with no actions. Pass `where`, `card` or `sort` to choose for yourself; actions on a card you pass run in one undo step too. The default order is by page, then top to bottom and left to right by the mark's rect.
459
+
460
+ The list and the marks share one selection per records key, so every list over the same records agrees with the marks. Clicking a card, or Enter on it, selects the record, gives its mark `data-selected` and scrolls the viewer to it. Focus stays in the list and no popover opens. Activating a mark selects its card and scrolls every list over those records to it, inside the list, without moving the page; a list in a closed panel scrolls when it shows. Escape in the list clears the selection, and so does removing the record or undoing it away. A `where` of yours that is stricter than the marks' hides a selected record from your list but leaves it selected, for the marks and other lists. Choosing a record that no `marks` call covers scrolls to it without selecting it.
461
+
462
+ The list is one Tab stop: arrow keys move between cards, Home and End go to the first and last. When the focused card goes, after an action, an undo or any other change, focus moves to the next card, else the previous one, else the list. A polite live region says "3 items" when an action or your app adds or removes cards, again when the count repeats, but not on undo or redo. With nothing listed it shows "Nothing here yet."
463
+
464
+ `createSearchResults(viewer)` is the same list over the viewer's search: one card per hit, titled with its page ("Page 2"), with its place on that page ("Match 3") and the text around it. Choosing a card makes its hit current, as next and previous in the find bar do, and the find bar's current hit is the selected card. It has its own query field, labelled "Find in document", above the cards and the find bar's match count in a footer below them. Both share the viewer's search state, so they never disagree. Pass `{ field: false }` to leave the field out when a find bar is on the page. A hit's id is `<page>:<n>`, its place among that page's hits, so it stays the same while the scan adds hits from other pages. Escape in the results does not clear the current hit. If you render your own field, `handleResultsFieldKey(event, search)` gives it the find bar's Enter and Shift+Enter. `isBlankQuery(query)` says whether a query counts as no query at all, as the search treats it: the list shows its empty text only then.
465
+
466
+ To render your own cards, build a source with `recordsSource({ pluginHost, viewport }, key, options)` or `searchSource(search)` and pass it to `createRecordListController(source)`. Its `rows` give each item, its card and whether it is selected, and `count` and `announcement` give the count and the live-region text. Render each row as an element with `data-ink-record` set to the item's id, call `attach(element)` on the element holding them, and call `act(id, label)` from each action button. `attach` handles clicks and keys, sets each row's `tabindex` and scrolls the selected row into view. `choose(id)` selects and reveals an item from code. A source of your own needs `items` (each with an `id`, a 0-based `page` and an optional `rect`), `card`, `selected`, `select`, `reveal`, `run` and `replaying`.
467
+
468
+ `@inkbar/theme` styles the list with `.ink-records`, `.ink-record-list`, `.ink-record` and `.ink-record-list-empty`, and the selected card with `aria-current="true"`.
469
+
470
+ `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
+
353
472
  ## Forms
354
473
 
355
474
  Inkbar fills the AcroForm fields in a PDF and runs the form's scripts. `doc.form` reads and sets the values:
@@ -372,7 +491,7 @@ A signature here is a mark on the page, not a cryptographic signature of the fil
372
491
 
373
492
  Your app owns the templates. Pass a `SignatureStore` (`list`, `save`, `remove`, each returning a promise) as `signatures`. `createMemorySignatureStore()` is the default, and forgets everything on reload. `createLocalSignatureStore(key)` keeps them in `localStorage` and falls back to memory where storage is blocked. 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.
374
493
 
375
- `createSignatureMenu(viewer)` lists the saved signatures, removes them, and opens `createSignatureDialog`, which draws, types or uploads a new one and selects it.
494
+ `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.
376
495
 
377
496
  ## Serving PDF.js assets
378
497