@inkbar/svelte 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 CHANGED
@@ -30,7 +30,7 @@ Annotations are plain typed objects. Bind them with `bind:annotations` to keep t
30
30
  pnpm add @inkbar/svelte @inkbar/core @inkbar/theme
31
31
  ```
32
32
 
33
- Svelte 5.46.4 or later is a peer dependency. Add the Vite plugin from `@inkbar/core/vite` so PDF.js can load its worker and fonts; the [`@inkbar/core` README](https://github.com/inkbar/inkbar/tree/main/packages/core#serving-pdfjs-assets) covers it. The package ships `.svelte` sources, so your bundler compiles them for your target, including the server.
33
+ `@inkbar/core` (installed above, at the same version) and Svelte 5.46.4 or later are peer dependencies. Add the Vite plugin from `@inkbar/core/vite` so PDF.js can load its worker and fonts; the [`@inkbar/core` README](https://github.com/inkbar/inkbar/tree/main/packages/core#serving-pdfjs-assets) covers it. The package ships `.svelte` sources, so your bundler compiles them for your target, including the server.
34
34
 
35
35
  ## Usage
36
36
 
@@ -64,7 +64,7 @@ Svelte 5.46.4 or later is a peer dependency. Add the Vite plugin from `@inkbar/c
64
64
  const tools = [highlightTool(), redactAreaTool(), noteTool()]
65
65
  </script>
66
66
 
67
- <InkRoot src={{ url: signedUrl, key: documentId }} {assets} {tools} bind:annotations>
67
+ <InkRoot src={{ url: signedUrl, key: documentId }} {assets} {tools} bind:annotations history={true}>
68
68
  <InkToolbar>
69
69
  <InkToolButton tool="select-text" />
70
70
  <InkToolButton tool="select" />
@@ -89,9 +89,9 @@ Svelte 5.46.4 or later is a peer dependency. Add the Vite plugin from `@inkbar/c
89
89
  </InkRoot>
90
90
  ```
91
91
 
92
- `select-text` and `select` are always there. Every other tool is opt-in: pass the ones you offer through `tools`, from the tool factories in `@inkbar/core/dom`, or `allTools()` for all of them. A button for a tool you did not pass renders disabled and hidden, a tool menu renders nothing, and each logs an `InkError('UnknownTool')` to the console once. If the active tool leaves `tools`, the viewer switches to `select-text` and reports it through `onToolChange`.
92
+ `select-text` and `select` are always there. Every other tool is opt-in: pass the ones you offer through `tools`, from the tool factories in `@inkbar/core/dom`, or `allTools()` for all of them. A button for a tool you did not pass renders empty, disabled and `aria-hidden`, the highlight, shape and signature menus render nothing, and each logs an `InkError('UnknownTool')` to the console once. If the active tool leaves `tools`, the viewer switches to `select-text` and reports it through `onToolChange`.
93
93
 
94
- Each toolbar control is a component, so keep the ones you need and put them where you like. Buttons show icons from `@inkbar/icons`. Pass your own as `children` to replace one, and use `InkIcon` to put the same icons on your own buttons. For a button of your own, such as `<InkToolbarButton onclick={download}>Download</InkToolbarButton>`, use `InkToolbarButton`: it joins the toolbar's arrow-key focus and looks like the other buttons. `bind:annotations` makes your list the source of truth. Leave it off and Inkbar keeps the state itself, with undo and redo.
94
+ Each toolbar control is a component, so keep the ones you need and put them where you like. Buttons show icons from `@inkbar/icons`. Pass your own as `children` to replace one, and use `InkIcon` to put the same icons on your own buttons. For a button of your own, such as `<InkToolbarButton onclick={download}>Download</InkToolbarButton>`, use `InkToolbarButton`: it joins the toolbar's arrow-key focus and looks like the other buttons. `bind:annotations` makes your list the source of truth, and `history={true}` keeps undo and redo working with it. Leave the binding off and Inkbar keeps the state itself, with undo and redo on by default.
95
95
 
96
96
  ## Controlled mode
97
97
 
@@ -149,9 +149,17 @@ Inside `InkRoot`, `useHistory()` returns the same object. Its `batch(fn)` runs `
149
149
 
150
150
  `key` identifies the document. Reuse the same key when only the signed URL changes and the document is not reloaded.
151
151
 
152
+ `InkRoot` takes a few more options. `password` opens an encrypted PDF. `storage` sets what the viewer keeps between visits (see [Storage](#storage)). `defaults` sets annotation colours; without it the theme's CSS variables apply. `fonts` sets the families the text style menu offers. `decorateAnnotation` adds attributes, classes and a screen-reader description to each annotation. `bind:page` and `bind:zoom` (or `onPageChange` and `onZoomChange`) follow and drive the viewport, and `bind:tool` (or `onToolChange`) does the same for the active tool. `onError` hears failed opens, form script errors and refused signings. `onActivate` hears the id of each activated annotation. `onFieldsChange` hears form value changes.
153
+
154
+ ## Viewport
155
+
156
+ `InkViewport` takes `mode`, `rotation`, `gap` (CSS pixels between pages, default 16), `buffer` (pages kept mounted either side of the visible ones, default 2) and `pinchZoom` (default `true`; `false` leaves pinches to the browser, which zooms the whole page). A change to `gap`, `buffer` or `pinchZoom` rebuilds the engine.
157
+
158
+ On a touch screen, two fingers pinch to zoom the document and scroll it. With `draw`, `sketch`, `shape`, `redact-area` and `freetext`, one finger draws; a finger tap draws nothing, and a pen that lands while a palm rests on the screen takes over. A shape, area redaction or text box gives one-finger scrolling back once it has made something.
159
+
152
160
  ## Forms
153
161
 
154
- `InkViewport` fills the PDF's form fields and runs the form's scripts. `useForm()` returns the fields, their values, and `set` and `reset`. Use `bind:fields` to hold the values in your own state. The form never submits, opens a link, sends mail or prints: `onformaction` hands each request to you. `formHighlight={false}` turns off the field tint, and a change applies to the open document in place.
162
+ `InkViewport` fills the PDF's form fields and runs the form's scripts. `useForm()` returns the fields, their values, and `set` and `reset`. Use `bind:fields` to hold the values in your own state. The form never submits, opens a link, sends mail or prints: `onaction` hands each request to you; so does a link to a URL. `onformaction` still works until 0.13.0. `formHighlight={false}` turns off the field tint, and a change applies to the open document in place.
155
163
 
156
164
  The [forms guide](https://inkbar.dev/docs/guides/forms/) covers controlled fields, form actions, scripts, signature fields and saving values as Gloss.
157
165
 
@@ -163,9 +171,26 @@ A root note with no text and no replies is removed when its popover closes, or w
163
171
 
164
172
  The highlight tool leaves `contents` free for the comment. `doc.text(page)` returns the page's text items; match the markup's `quads` against them to get the marked text. `InkHighlightMenu` is a split button for the tool: it picks the markup kind and its colour.
165
173
 
174
+ ## Style menus
175
+
176
+ `InkColorMenu` (`property="border"`, `"fill"` or `"text"`), `InkLineStyleMenu` (weight, dashes, arrowheads) and `InkTextStyleMenu` (font, size, colour, bold, italic, underline, alignment) edit the selection, or the active tool's style when nothing is selected. A menu disables when nothing supports its properties. `InkShapeMenu` is a split button for the `shape` tool and renders nothing unless `shapeTool` is in `tools`. `fonts` on `InkRoot` sets the families the text style menu offers.
177
+
166
178
  ## Signatures
167
179
 
168
- `InkSignatureMenu` lists saved signatures, picks one for the `signature` tool, removes them, and opens `InkSignatureDialog` to draw, type or upload a new one. The menu needs `signatureTool()` in `tools`. The dialog and signature form fields work without it; the dialog then only saves the signature. These are marks on the page, not cryptographic signatures. Pass `signatures` to `InkRoot` to keep them somewhere: `createLocalSignatureStore(key)` from `@inkbar/core` uses `localStorage`, and without it they last until reload. `signatureFonts` lists the handwriting fonts the Type tab offers; load them with `@font-face`. A placed signature keeps its aspect ratio when resized.
180
+ `InkSignatureMenu` lists saved signatures, picks one for the `signature` tool, removes them, and opens `InkSignatureDialog` to draw, type or upload a new one. The menu needs `signatureTool()` in `tools`. The dialog and signature form fields work without it; the dialog then only saves the signature. These are marks on the page, not cryptographic signatures. `InkRoot` keeps them in its `storage` under the key `signatures`, so without a store they last until reload. A `signatures` store of your own still works and wins over `storage` until 0.13.0, which removes it. `signatureFonts` lists the handwriting fonts the Type tab offers; load them with `@font-face`. A placed signature keeps its aspect ratio when resized.
181
+
182
+ ## Storage
183
+
184
+ `InkRoot` keeps a few small things between visits: saved signatures, sidebar widths and plugin settings. Pass `storage` to choose where. `createLocalStorage()` from `@inkbar/core` keeps them in `localStorage`, and any `InkStorage` works, including an async one that talks to your server. The default is `createMemoryStorage()`, which forgets everything on reload. Inkbar never touches `localStorage`, cookies or the network unless the store you pass does. The root reads `storage` once, when it is created. See the [`@inkbar/core` README](https://github.com/inkbar/inkbar/tree/main/packages/core#storage) for the keys.
185
+
186
+ ```svelte
187
+ <script lang="ts">
188
+ import { createLocalStorage } from '@inkbar/core'
189
+ const storage = createLocalStorage()
190
+ </script>
191
+
192
+ <InkRoot {src} {assets} {storage}>
193
+ ```
169
194
 
170
195
  ## Plugins
171
196
 
@@ -187,7 +212,7 @@ Plugin stores undo with annotations. In controlled mode with `history`, each und
187
212
 
188
213
  ## Search
189
214
 
190
- `InkFindBar` is the find bar and `InkSearchButton` opens it. Place the bar anywhere inside `InkRoot`, once `InkViewport` is there; Ctrl/Cmd+F opens it while it is mounted. `useSearch()` returns the search `state` and its actions (`open`, `setQuery`, `setMatchCase`, `next`, `previous`, `close`) for your own control. See the [`@inkbar/core` README](https://github.com/inkbar/inkbar/tree/main/packages/core#search) for the API.
215
+ `InkFindBar` is the find bar and `InkSearchButton` opens it. Place the bar anywhere inside `InkRoot`, once `InkViewport` is there; Ctrl/Cmd+F opens it while it is mounted. `useSearch()` returns the search `state` and its actions (`open(query?)`, `setQuery`, `setMatchCase`, `next`, `previous`, `select`, `close`) for your own control. See the [`@inkbar/core` README](https://github.com/inkbar/inkbar/tree/main/packages/core#search) for the API.
191
216
 
192
217
  The bar handles Escape itself and stops it there, so an `onkeydown` you pass to `InkFindBar` never sees Escape.
193
218
 
@@ -196,6 +221,10 @@ The bar handles Escape itself and stops it there, so an `onkeydown` you pass to
196
221
  `InkSidebar` holds `InkPanel`s and places nothing itself: put it beside `InkViewport` with your own flex or grid layout. 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. Its children are the panel's content, your own component or one of Inkbar's.
197
222
 
198
223
  ```svelte
224
+ <script lang="ts">
225
+ import { Square, MessageSquare } from '@inkbar/icons'
226
+ </script>
227
+
199
228
  <InkRoot {src} {assets}>
200
229
  <div class="app">
201
230
  <InkSidebar side="left" layout="rail" bind:open>
@@ -213,7 +242,13 @@ The bar handles Escape itself and stops it there, so an `onkeydown` you pass to
213
242
 
214
243
  `layout` is `rail` (default: icon buttons beside the document, and activating the open panel's button closes it), `tabs` (labelled tabs, one panel open) or `stack` (a header per panel, any number expanded). `side` (`left` by default) decides which edge the rail and border sit on. `bind:open` is the open panel's id, or `null`, for rail and tabs, and the expanded ids for stack. Left out, the first panel starts open. When the open panel goes (an `{#if}`), rail and tabs open the first one left and stack drops it, and the bound `open` receives the new value. In the rail, the open panel shows its label as a title above its content. `visible` (default `true`) shows or hides the whole sidebar; it keeps the open panel, its content and its scroll for when it shows again. Your code decides when: `visible={hits > 0}` keeps a results sidebar out of the way until a search finds something.
215
244
 
216
- Panels take their place in the order they appear in the markup, including one added later with `{#if}`. A closed panel stays mounted and hidden, so its content keeps its state and scroll position, across layout changes too. The keys and ARIA are core's: see the [`@inkbar/core` README](https://github.com/inkbar/inkbar/tree/main/packages/core#sidebars). The sidebar needs no document and works outside `InkRoot`. On the server it renders its frame with the open panel showing, and the stack headers; rail and tab buttons follow once it has mounted.
245
+ Panels take their place in the order they appear in the markup, including one added later with `{#if}`. A closed panel stays mounted and hidden, so its content keeps its state and scroll position, across layout changes too. The keys and ARIA are core's: see the [`@inkbar/core` README](https://github.com/inkbar/inkbar/tree/main/packages/core#sidebars). The sidebar needs no document and works outside `InkRoot`. On the server it renders its frame at its width with the open panel showing, the stack headers and the resize handle; rail and tab buttons follow once it has mounted.
246
+
247
+ Each sidebar has a resize handle on its inner edge. Drag it, or focus it and use the arrow keys (16px a step), 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. The sidebar sets `--ink-sidebar-width` inline, so a CSS override of it no longer applies. Pass `defaultWidth` instead. Inside `InkRoot` the sidebar saves its width in the root's `storage` under `sidebar.<name>.width`, where `name` defaults to the side; give two sidebars on one side different names. Pass `storage` to the sidebar to use another store, or to save its width outside `InkRoot`. The sidebar reads `storage` and `name` once, when it is created. To control the width, bind `width` to a number: the handle then only updates the binding, and the sidebar neither reads nor writes storage. Bind it to a value that starts `undefined` and you only receive the width; the sidebar keeps saving it.
248
+
249
+ ```svelte
250
+ <InkSidebar name="tools" side="right" defaultWidth={360} bind:width={toolsWidth}>
251
+ ```
217
252
 
218
253
  ## Thumbnails
219
254
 
@@ -231,6 +266,20 @@ Panels take their place in the order they appear in the markup, including one ad
231
266
 
232
267
  An item's height is worked out from its page alone, so position what you add absolutely. The `item` snippet renders inside the item's `<button>`: keep it non-interactive, with no links, buttons or inputs. Both need `InkRoot` and render empty on the server.
233
268
 
269
+ ## Outline and Back
270
+
271
+ `InkOutline` shows the document's outline as a tree. Clicking an item, or Enter on it, follows it: a destination scrolls there (without changing the zoom), a web address goes to the host as an `open-url` action. The chevron expands and collapses an item without following it. The item the reader has reached is marked `aria-current="location"` and kept in view. It fills its parent's height and scrolls, so put it in a panel:
272
+
273
+ ```svelte
274
+ <InkPanel id="outline" label="Outline" icon={List}>
275
+ <InkOutline emptyText="No outline" />
276
+ </InkPanel>
277
+ ```
278
+
279
+ `emptyText` is shown when the document has no outline. The `item` snippet, given the item and `{ current, expanded }`, replaces an item's title. The keys and ARIA are core's, as in the thumbnails. Needs `InkRoot`; the tree renders empty on the server.
280
+
281
+ `InkBack` and `InkForward` return to where the view was before a jump (a link, an outline item) and undo that. Each is disabled until there is somewhere to go, and takes a `child` snippet. Put them in an `InkToolbar`.
282
+
234
283
  ## Record lists
235
284
 
236
285
  `InkRecordList` lists a plugin's records as cards, the same cards their marks open, with the same actions. Name the records `<plugin>.<name>`:
@@ -243,7 +292,7 @@ An item's height is worked out from its page alone, so position what you add abs
243
292
  </InkPanel>
244
293
  ```
245
294
 
246
- The list and the marks share one selection: choosing a card selects its mark and scrolls the viewer there, and activating a mark selects its card. Each action runs in one undo step. `card`, `where` and `sort` replace the marks' card, filter and reading order. A new function rebuilds the list, so define them once in the script, never inline in the markup. `bind:count` receives the number listed whenever it changes, ready for a panel badge. The `empty` snippet replaces "Nothing here yet.". To list something of your own, pass a `source` (a `RecordListSource`, as core's `recordsSource` and `searchSource` build) in place of `records`: it gets the same cards, keys and selection. With both, `source` wins.
295
+ The list and the marks share one selection: choosing a card selects its mark and scrolls the viewer there, and activating a mark selects its card. Each action runs in one undo step. `card`, `where` and `sort` replace the marks' card, filter and reading order. A new function rebuilds the list, so define them once in the script, never inline in the markup. They do not apply to a `source`, which carries its own. `bind:count` receives the number listed whenever it changes, ready for a panel badge. The `empty` snippet replaces "Nothing here yet.". To list something of your own, pass a `source` (a `RecordListSource`, as core's `recordsSource` and `searchSource` build) in place of `records`: it gets the same cards, keys and selection. With both, `source` wins.
247
296
 
248
297
  `InkSearchResults` is the same list over the document 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 the find bar's next and previous do. It has its own field, labelled "Find in document", above the cards and the find bar's match count in a footer below them; `field={false}` leaves the field out when a find bar is on the page. Field and find bar share the search state, so they never disagree.
249
298
 
@@ -251,11 +300,11 @@ To build your own, `useRecords<T>(name)` returns the live `records` and the `han
251
300
 
252
301
  ## Custom rendering
253
302
 
254
- Slots are snippets. `InkViewport` takes `loading`, `empty`, `error`, `annotation` and `pageError`. `InkThreadPopover` takes `children` as a snippet that receives the popover's props. `InkToolButton` takes `children` as a snippet of `{ active, label }`, and `InkCommand` one of `{ label, busy }`. `InkThumbnails` takes `item`, and `InkRecordList` takes `empty`. The props each receives are typed as `AnnotationSlotProps` and `ThreadPopoverSlotProps`, the same types the Vue and React packages export.
303
+ Slots are snippets. `InkViewport` takes `loading`, `empty`, `error`, `annotation` and `pageError`. `InkThreadPopover` takes `children` as a snippet that receives the popover's props. `InkToolButton` takes `children` as a snippet of `{ active, label }`, and `InkCommand` one of `{ label, busy }`. `InkThumbnails` and `InkOutline` take `item`, and `InkRecordList` takes `empty`. The props each receives are typed as `AnnotationSlotProps` and `ThreadPopoverSlotProps`, the same types the Vue and React packages export.
255
304
 
256
305
  `InkDownloadButton` and `InkPrintButton` download and print the open PDF; `filename` names the download (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. Until saving into the PDF ships, both give the file as it was opened, without this session's annotations.
257
306
 
258
- `InkRoot`, `InkToolbar`, `InkToolButton`, `InkCommand`, `InkUndo`, `InkRedo`, `InkDownloadButton`, `InkPrintButton`, `InkPrevPage`, `InkNextPage`, `InkZoomIn` and `InkZoomOut` take a `child` snippet to render your own element. Spread the `props` it gives you on that element. The snippet type is exported as `ChildSnippet`.
307
+ `InkRoot`, `InkToolbar`, `InkToolButton`, `InkToolbarButton`, `InkCommand`, `InkUndo`, `InkRedo`, `InkDownloadButton`, `InkPrintButton`, `InkSearchButton`, `InkPrevPage`, `InkNextPage`, `InkBack`, `InkForward`, `InkZoomIn` and `InkZoomOut` take a `child` snippet to render your own element. Spread the `props` it gives you on that element. The snippet type is exported as `ChildSnippet`.
259
308
 
260
309
  ```svelte
261
310
  <InkToolbar>
@@ -277,11 +326,12 @@ The scroll area takes focus. From there:
277
326
 
278
327
  On a focused annotation:
279
328
 
280
- - `Enter` or `Space` selects and activates it. A note or text markup opens its thread; other types are only selected.
329
+ - `Enter` or `Space` selects and activates it. A note or text markup opens its thread; other types are only selected. `Enter` on an editable text box opens its editor instead.
281
330
  - `ArrowUp` and `ArrowDown` move to the previous or next annotation on the page.
282
331
  - `Shift` + arrow moves it by 1 pt, or by 10 pt with `Alt` as well.
283
332
  - `Ctrl` or `Cmd` + `Shift` + arrow resizes an area redaction, text box or shape.
284
333
  - `Delete` or `Backspace` removes it.
334
+ - `Escape` clears the selection.
285
335
 
286
336
  The full table, with the zoom and page keys, is in the [`@inkbar/core` README](https://github.com/inkbar/inkbar/tree/main/packages/core#tools-and-keyboard).
287
337
 
@@ -291,7 +341,7 @@ Components carry state on data attributes, for example `[data-annotation][data-s
291
341
 
292
342
  ## Hooks
293
343
 
294
- `useDocument`, `useAnnotations`, `useTool`, `useSelection`, `useZoom`, `usePage`, `useHistory`, `useEmbedded`, `useCommand`, `useRecords` and `useRecordSelection` read the state of the nearest `InkRoot`. Call them during component initialisation, as you would `getContext`. Each returns an object whose reactive members are getters: read `useZoom().zoom` in your template or an effect and it tracks; destructure it (`const { zoom } = useZoom()`) and you copy the value once. Keep the object and read through it. Tool options, such as the shape kind, last as long as `InkRoot`. The tool style that `useToolStyle` reads and sets (colours, line style, text style) belongs to the document: opening another one starts again from the theme's defaults.
344
+ `useDocument`, `useAnnotations`, `useTool`, `useSelection`, `useZoom`, `usePage`, `useHistory`, `useEmbedded`, `useCommand`, `useForm`, `useSearch`, `useToolStyle`, `useToolOptions`, `useRecords` and `useRecordSelection` read the state of the nearest `InkRoot`. Call them during component initialisation, as you would `getContext`. Each returns an object whose reactive members are getters: read `useZoom().zoom` in your template or an effect and it tracks; destructure it (`const { zoom } = useZoom()`) and you copy the value once. Keep the object and read through it. `useToolOptions('shape')` reads and sets a tool's options, such as the shape kind, which last as long as `InkRoot`. The tool style that `useToolStyle` reads and sets (colours, line style, text style) belongs to the document: opening another one starts again from the theme's defaults.
295
345
 
296
346
  ## Licence
297
347
 
@@ -0,0 +1,40 @@
1
+ <script lang="ts">
2
+ import { ArrowLeft } from '@inkbar/icons'
3
+ import { Toolbar } from 'bits-ui'
4
+
5
+ import { useInkContext } from '../context.js'
6
+ import { useReadable } from '../use-readable.svelte.js'
7
+
8
+ import { asBitsProps } from './child-snippet.js'
9
+ import InkIcon from './InkIcon.svelte'
10
+
11
+ import type { InkPageButtonProps } from './page-button.js'
12
+
13
+ let {
14
+ class: className,
15
+ children,
16
+ child,
17
+ disabled,
18
+ onclick,
19
+ ...rest
20
+ }: InkPageButtonProps = $props()
21
+
22
+ const ctx = useInkContext('InkBack')
23
+ const can = useReadable(() => ctx.navigation?.canGoBack, false)
24
+ </script>
25
+
26
+ <Toolbar.Button
27
+ class={['ink-back', className]}
28
+ data-ink-back=""
29
+ aria-label="Back"
30
+ title="Back"
31
+ disabled={!can.current || !!disabled}
32
+ {...child ? { child } : {}}
33
+ {...asBitsProps(rest)}
34
+ onclick={(e) => {
35
+ onclick?.(e)
36
+ if (!e.defaultPrevented) ctx.navigation?.back()
37
+ }}
38
+ >
39
+ {#if children}{@render children()}{:else}<InkIcon icon={ArrowLeft} />{/if}
40
+ </Toolbar.Button>
@@ -0,0 +1,5 @@
1
+ import type { InkPageButtonProps } from './page-button.js';
2
+ declare const InkBack: import("svelte").Component<InkPageButtonProps, {}, "">;
3
+ type InkBack = ReturnType<typeof InkBack>;
4
+ export default InkBack;
5
+ //# sourceMappingURL=InkBack.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"InkBack.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/InkBack.svelte.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAoC3D,QAAA,MAAM,OAAO,wDAAwC,CAAC;AACtD,KAAK,OAAO,GAAG,UAAU,CAAC,OAAO,OAAO,CAAC,CAAC;AAC1C,eAAe,OAAO,CAAC"}
@@ -0,0 +1,40 @@
1
+ <script lang="ts">
2
+ import { ArrowRight } from '@inkbar/icons'
3
+ import { Toolbar } from 'bits-ui'
4
+
5
+ import { useInkContext } from '../context.js'
6
+ import { useReadable } from '../use-readable.svelte.js'
7
+
8
+ import { asBitsProps } from './child-snippet.js'
9
+ import InkIcon from './InkIcon.svelte'
10
+
11
+ import type { InkPageButtonProps } from './page-button.js'
12
+
13
+ let {
14
+ class: className,
15
+ children,
16
+ child,
17
+ disabled,
18
+ onclick,
19
+ ...rest
20
+ }: InkPageButtonProps = $props()
21
+
22
+ const ctx = useInkContext('InkForward')
23
+ const can = useReadable(() => ctx.navigation?.canGoForward, false)
24
+ </script>
25
+
26
+ <Toolbar.Button
27
+ class={['ink-forward', className]}
28
+ data-ink-forward=""
29
+ aria-label="Forward"
30
+ title="Forward"
31
+ disabled={!can.current || !!disabled}
32
+ {...child ? { child } : {}}
33
+ {...asBitsProps(rest)}
34
+ onclick={(e) => {
35
+ onclick?.(e)
36
+ if (!e.defaultPrevented) ctx.navigation?.forward()
37
+ }}
38
+ >
39
+ {#if children}{@render children()}{:else}<InkIcon icon={ArrowRight} />{/if}
40
+ </Toolbar.Button>
@@ -0,0 +1,5 @@
1
+ import type { InkPageButtonProps } from './page-button.js';
2
+ declare const InkForward: import("svelte").Component<InkPageButtonProps, {}, "">;
3
+ type InkForward = ReturnType<typeof InkForward>;
4
+ export default InkForward;
5
+ //# sourceMappingURL=InkForward.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"InkForward.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/InkForward.svelte.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAoC3D,QAAA,MAAM,UAAU,wDAAwC,CAAC;AACzD,KAAK,UAAU,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;AAChD,eAAe,UAAU,CAAC"}
@@ -0,0 +1,170 @@
1
+ <script module lang="ts">
2
+ import type { OutlineItem } from '@inkbar/core'
3
+ import type { Snippet } from 'svelte'
4
+ import type { HTMLAttributes } from 'svelte/elements'
5
+
6
+ export interface InkOutlineProps extends Omit<
7
+ HTMLAttributes<HTMLDivElement>,
8
+ 'children'
9
+ > {
10
+ /** Shown when the document has no outline. Default: "This document has no outline". */
11
+ emptyText?: string | undefined
12
+ /**
13
+ * Replaces an item's title, given the item and whether it is the current one and whether it
14
+ * is expanded. Keep what it renders non-interactive.
15
+ */
16
+ item?: Snippet<[OutlineItem, { current: boolean; expanded: boolean }]> | undefined
17
+ }
18
+ </script>
19
+
20
+ <script lang="ts">
21
+ /*
22
+ * The open document's outline as a tree: `createOutline`'s markup on core's outline controller,
23
+ * which holds what is expanded, the Tab stop, the keys, the current item and Back's history.
24
+ * Clicking an item, or Enter on it, follows it; its chevron expands and collapses it. It fills
25
+ * its parent's height and scrolls, and keeps the item the reader has reached in view. The
26
+ * `item` snippet replaces an item's title. Inside `InkRoot`; the tree is empty on the server.
27
+ */
28
+ import {
29
+ createOutlineController,
30
+ outlineTreeAttrs,
31
+ type OutlineController,
32
+ } from '@inkbar/core/dom'
33
+ import { ChevronRight } from '@inkbar/icons'
34
+ import { untrack } from 'svelte'
35
+
36
+ import { useInkContext } from '../context.js'
37
+ import { toReadable } from '../to-readable.svelte.js'
38
+
39
+ import InkIcon from './InkIcon.svelte'
40
+
41
+ let {
42
+ emptyText = 'This document has no outline',
43
+ item: renderItem,
44
+ class: className,
45
+ ...rest
46
+ }: InkOutlineProps = $props()
47
+
48
+ const ctx = useInkContext('InkOutline')
49
+ const deps = {
50
+ document: toReadable(() => ctx.document),
51
+ viewport: toReadable(() => ctx.viewport),
52
+ navigation: toReadable(() => ctx.navigation),
53
+ }
54
+
55
+ let root = $state<HTMLDivElement | null>(null)
56
+ let controller = $state.raw<OutlineController | null>(null)
57
+ // Until the controller is built after mount, and on the server: nothing listed.
58
+ let items = $state.raw<readonly OutlineItem[]>([])
59
+ let loading = $state.raw(false)
60
+ let expanded = $state.raw<ReadonlySet<string>>(new Set())
61
+ let current = $state.raw<string | null>(null)
62
+ let focused = $state.raw<string | null>(null)
63
+
64
+ // Built once the element exists, as the other controller-backed components do: it subscribes
65
+ // to the document, so nothing is created while rendering on the server.
66
+ $effect(() => {
67
+ const el = root
68
+ if (!el) return
69
+ return untrack(() => {
70
+ const c = createOutlineController(deps)
71
+ controller = c
72
+ items = c.items.get()
73
+ loading = c.loading.get()
74
+ expanded = c.expanded.get()
75
+ current = c.current.get()
76
+ focused = c.focused.get()
77
+ const stops = [
78
+ c.items.subscribe((v) => (items = v)),
79
+ c.loading.subscribe((v) => (loading = v)),
80
+ c.expanded.subscribe((v) => (expanded = v)),
81
+ c.current.subscribe((v) => (current = v)),
82
+ c.focused.subscribe((v) => (focused = v)),
83
+ c.bindScroll(el),
84
+ ]
85
+ return () => {
86
+ for (const stop of stops) stop()
87
+ c.destroy()
88
+ controller = null
89
+ }
90
+ })
91
+ })
92
+
93
+ /** The item's attributes. Reading the stores here makes a render follow them. */
94
+ function attrs(id: string): Record<string, string | undefined> {
95
+ void [expanded, current, focused]
96
+ return controller?.itemAttrs(id) ?? {}
97
+ }
98
+
99
+ const level = (id: string): number => id.split('.').length - 1
100
+ </script>
101
+
102
+ {#snippet nodes(list: readonly OutlineItem[])}
103
+ {#each list as entry (entry.id)}
104
+ {@const c = controller!}
105
+ {@const open = expanded.has(entry.id)}
106
+ <li
107
+ class="ink-outline-item"
108
+ data-ink-outline-item={entry.id}
109
+ data-bold={entry.bold ? '' : undefined}
110
+ data-italic={entry.italic ? '' : undefined}
111
+ style:--ink-outline-level={level(entry.id)}
112
+ {...attrs(entry.id)}
113
+ >
114
+ <!-- The tree's keys come from the controller, so the row itself is not focusable. -->
115
+ <div
116
+ class="ink-outline-row"
117
+ {...c.rowAttrs(entry.id)}
118
+ onclick={() => c.activate(entry.id)}
119
+ >
120
+ {#if entry.children.length > 0}
121
+ <span
122
+ class="ink-outline-toggle"
123
+ aria-hidden="true"
124
+ onclick={(e) => {
125
+ // Expanding is not following: the row's own click is not reached.
126
+ e.stopPropagation()
127
+ c.toggle(entry.id)
128
+ }}
129
+ >
130
+ <InkIcon icon={ChevronRight} />
131
+ </span>
132
+ {:else}
133
+ <span class="ink-outline-spacer" aria-hidden="true"></span>
134
+ {/if}
135
+ <span class="ink-outline-title" {...c.titleAttrs(entry.id)}>
136
+ {#if renderItem}
137
+ {@render renderItem(entry, { current: current === entry.id, expanded: open })}
138
+ {:else}
139
+ {entry.title}
140
+ {/if}
141
+ </span>
142
+ </div>
143
+ {#if entry.children.length > 0}
144
+ <ul class="ink-outline-group" role="group" hidden={!open}>
145
+ {@render nodes(entry.children)}
146
+ </ul>
147
+ {/if}
148
+ </li>
149
+ {/each}
150
+ {/snippet}
151
+
152
+ <div
153
+ {...rest}
154
+ bind:this={root}
155
+ class={['ink-outline', className]}
156
+ data-ink-outline=""
157
+ aria-busy={loading ? 'true' : undefined}
158
+ >
159
+ <ul
160
+ class="ink-outline-tree"
161
+ {...outlineTreeAttrs()}
162
+ hidden={items.length === 0}
163
+ onkeydown={(e) => controller?.onKeydown(e)}
164
+ >
165
+ {#if controller}{@render nodes(items)}{/if}
166
+ </ul>
167
+ {#if controller && !loading && items.length === 0}
168
+ <p class="ink-outline-empty">{emptyText}</p>
169
+ {/if}
170
+ </div>
@@ -0,0 +1,19 @@
1
+ import type { OutlineItem } from '@inkbar/core';
2
+ import type { Snippet } from 'svelte';
3
+ import type { HTMLAttributes } from 'svelte/elements';
4
+ export interface InkOutlineProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children'> {
5
+ /** Shown when the document has no outline. Default: "This document has no outline". */
6
+ emptyText?: string | undefined;
7
+ /**
8
+ * Replaces an item's title, given the item and whether it is the current one and whether it
9
+ * is expanded. Keep what it renders non-interactive.
10
+ */
11
+ item?: Snippet<[OutlineItem, {
12
+ current: boolean;
13
+ expanded: boolean;
14
+ }]> | undefined;
15
+ }
16
+ declare const InkOutline: import("svelte").Component<InkOutlineProps, {}, "">;
17
+ type InkOutline = ReturnType<typeof InkOutline>;
18
+ export default InkOutline;
19
+ //# sourceMappingURL=InkOutline.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"InkOutline.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/InkOutline.svelte.ts"],"names":[],"mappings":"AAGE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAC/C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAA;AACrC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAErD,MAAM,WAAW,eAAgB,SAAQ,IAAI,CAC3C,cAAc,CAAC,cAAc,CAAC,EAC9B,UAAU,CACX;IACC,uFAAuF;IACvF,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAC9B;;;OAGG;IACH,IAAI,CAAC,EAAE,OAAO,CAAC,CAAC,WAAW,EAAE;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,QAAQ,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC,GAAG,SAAS,CAAA;CACnF;AA+IH,QAAA,MAAM,UAAU,qDAAwC,CAAC;AACzD,KAAK,UAAU,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;AAChD,eAAe,UAAU,CAAC"}
@@ -7,7 +7,7 @@
7
7
  DocumentSource,
8
8
  FieldsChange,
9
9
  FieldValues,
10
- FormAction,
10
+ ViewerAction,
11
11
  InkDocument,
12
12
  InkError,
13
13
  InkStatus,
@@ -52,8 +52,13 @@
52
52
  * are controlled, after the bound value is written.
53
53
  */
54
54
  onFieldsChange?: ((next: FieldValues, change: FieldsChange) => void) | undefined
55
- /** The form asked to submit, open a link, send mail, print or prompt. Inkbar does none. */
56
- onformaction?: ((action: FormAction) => void) | undefined
55
+ /**
56
+ * The viewer asked the host to submit a form, open a link, send mail, print or prompt.
57
+ * Inkbar does none of these.
58
+ */
59
+ onaction?: ((action: ViewerAction) => void) | undefined
60
+ /** @deprecated Use `onaction`. Removed in 0.13.0. Ignored when `onaction` is given. */
61
+ onformaction?: ((action: ViewerAction) => void) | undefined
57
62
  /** Tints fillable form fields. Default true. A change applies to the open document in place. */
58
63
  formHighlight?: boolean | undefined
59
64
  tool?: ToolId | undefined
@@ -87,9 +92,16 @@
87
92
  */
88
93
  fonts?: string[] | undefined
89
94
  /**
90
- * Where saved signatures live. Default: memory, gone on reload.
91
- * `createLocalSignatureStore` keeps them in the browser; a host can keep them anywhere.
92
- * A new store takes effect at once, for the menus and the `signature` tool alike.
95
+ * Where the viewer keeps what lasts between visits: saved signatures, each `InkSidebar`'s
96
+ * width and the plugins' settings. Default: memory, gone on reload. `createLocalStorage`
97
+ * keeps them in the browser; a host can keep them anywhere. Read once, when the root is
98
+ * created.
99
+ */
100
+ storage?: InkStorage | undefined
101
+ /**
102
+ * Where saved signatures live, in place of `storage`. A new store takes effect at once, for
103
+ * the menus and the `signature` tool alike.
104
+ * @deprecated Pass `storage`; signatures are kept in it under `signatures`. Removed in 0.13.0.
93
105
  */
94
106
  signatures?: SignatureStore | undefined
95
107
  /** The handwriting fonts a typed signature offers; the host loads them. */
@@ -121,9 +133,10 @@
121
133
 
122
134
  <script lang="ts">
123
135
  import {
124
- createMemorySignatureStore,
136
+ createMemoryStorage,
125
137
  createSignatureLibrary,
126
138
  createSnapshotHistory,
139
+ createStorageSignatureStore,
127
140
  InkError as InkErrorClass,
128
141
  openDocument,
129
142
  readonly,
@@ -153,8 +166,9 @@
153
166
  import { registeredToolIds } from '../hooks/useRegisteredTool.svelte.js'
154
167
  import { useReadable } from '../use-readable.svelte.js'
155
168
 
156
- import type { PluginHistory } from '@inkbar/core'
169
+ import type { InkStorage, PluginHistory } from '@inkbar/core'
157
170
  import type {
171
+ Navigation,
158
172
  PluginHost,
159
173
  SearchController,
160
174
  ToolController,
@@ -171,6 +185,7 @@
171
185
  onChange,
172
186
  fields = $bindable(),
173
187
  onFieldsChange,
188
+ onaction,
174
189
  onformaction,
175
190
  formHighlight,
176
191
  tool = $bindable(),
@@ -179,6 +194,7 @@
179
194
  plugins,
180
195
  decorateAnnotation,
181
196
  fonts,
197
+ storage: storageProp,
182
198
  signatures: signatureStore,
183
199
  signatureFonts,
184
200
  page = $bindable(),
@@ -197,9 +213,11 @@
197
213
 
198
214
  type TestHookElement = HTMLElement & { __inkDocument?: InkDocument | undefined }
199
215
 
200
- const signatures = $derived(
201
- createSignatureLibrary(signatureStore ?? createMemorySignatureStore())
202
- )
216
+ // Read once, like `plugins`: the sidebars and the plugins load from it as they are created.
217
+ const storage = untrack(() => storageProp ?? createMemoryStorage())
218
+ const storageSignatures = createStorageSignatureStore(storage)
219
+ // The deprecated `signatures` wins when both are given.
220
+ const signatures = $derived(createSignatureLibrary(signatureStore ?? storageSignatures))
203
221
  $effect(() => {
204
222
  void signatures.refresh()
205
223
  })
@@ -233,6 +251,7 @@
233
251
  let activatedNow: string | null = null
234
252
  let viewport = $state.raw<Viewport | null>(null)
235
253
  let tools = $state.raw<ToolController | null>(null)
254
+ let navigation = $state.raw<Navigation | null>(null)
236
255
 
237
256
  const documentStatus = useReadable<InkStatus>(() => document?.status, IDLE)
238
257
  const status = $derived(override ?? documentStatus.current)
@@ -428,7 +447,16 @@
428
447
  zoom = z
429
448
  onZoomChange?.(z)
430
449
  }
450
+ /**
451
+ * Whether the host has given `page` a value of its own, as React's `page` prop. Until then the
452
+ * page written back below is only a report, so the next document opens at its first page
453
+ * instead of the last one's. Not reactive: it flips only with a change to `page`, which
454
+ * `pageModel`'s readers already track.
455
+ */
456
+ let hostPage = untrack(() => page) !== undefined
457
+ let reportedPage: number | undefined
431
458
  function reportPage(p: number): void {
459
+ reportedPage = p
432
460
  page = p
433
461
  onPageChange?.(p)
434
462
  }
@@ -529,6 +557,7 @@
529
557
  decorate: () => decorateAnnotation,
530
558
  viewport: () => viewport,
531
559
  search: viewerSearchOf(controller),
560
+ storage,
532
561
  })
533
562
  )
534
563
  search = controller
@@ -628,6 +657,12 @@
628
657
  get viewport() {
629
658
  return viewport
630
659
  },
660
+ get navigation() {
661
+ return navigation
662
+ },
663
+ setNavigation(n) {
664
+ navigation = n
665
+ },
631
666
  get tools() {
632
667
  return tools
633
668
  },
@@ -651,6 +686,7 @@
651
686
  return fonts ?? FONT_FAMILIES
652
687
  },
653
688
  toolOptions,
689
+ storage,
654
690
  get signatures() {
655
691
  return signatures
656
692
  },
@@ -664,7 +700,8 @@
664
700
  return zoom
665
701
  },
666
702
  get pageModel() {
667
- return page
703
+ if (page !== undefined && page !== reportedPage) hostPage = true
704
+ return hostPage ? page : undefined
668
705
  },
669
706
  reportZoom,
670
707
  reportPage,
@@ -683,7 +720,7 @@
683
720
  get formHighlight() {
684
721
  return formHighlight
685
722
  },
686
- reportFormAction: (a) => onformaction?.(a),
723
+ reportAction: (a) => (onaction ?? onformaction)?.(a),
687
724
  reportError: (e) => onError?.(e),
688
725
  }
689
726
  setInkContext(context, () => activatedNow)