@inkbar/svelte 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.
Files changed (86) hide show
  1. package/README.md +71 -6
  2. package/dist/components/InkDownloadButton.svelte +62 -0
  3. package/dist/components/InkDownloadButton.svelte.d.ts +14 -0
  4. package/dist/components/InkDownloadButton.svelte.d.ts.map +1 -0
  5. package/dist/components/InkFindBar.svelte +18 -15
  6. package/dist/components/InkFindBar.svelte.d.ts.map +1 -1
  7. package/dist/components/InkHighlightMenu.svelte +101 -93
  8. package/dist/components/InkHighlightMenu.svelte.d.ts.map +1 -1
  9. package/dist/components/InkPanel.svelte +73 -0
  10. package/dist/components/InkPanel.svelte.d.ts +18 -0
  11. package/dist/components/InkPanel.svelte.d.ts.map +1 -0
  12. package/dist/components/InkPrintButton.svelte +57 -0
  13. package/dist/components/InkPrintButton.svelte.d.ts +12 -0
  14. package/dist/components/InkPrintButton.svelte.d.ts.map +1 -0
  15. package/dist/components/InkRecordList.svelte +91 -0
  16. package/dist/components/InkRecordList.svelte.d.ts +72 -0
  17. package/dist/components/InkRecordList.svelte.d.ts.map +1 -0
  18. package/dist/components/InkRoot.svelte +53 -5
  19. package/dist/components/InkRoot.svelte.d.ts +7 -2
  20. package/dist/components/InkRoot.svelte.d.ts.map +1 -1
  21. package/dist/components/InkSearchResults.svelte +72 -0
  22. package/dist/components/InkSearchResults.svelte.d.ts +9 -0
  23. package/dist/components/InkSearchResults.svelte.d.ts.map +1 -0
  24. package/dist/components/InkShapeMenu.svelte +89 -84
  25. package/dist/components/InkShapeMenu.svelte.d.ts.map +1 -1
  26. package/dist/components/InkSidebar.svelte +179 -0
  27. package/dist/components/InkSidebar.svelte.d.ts +22 -0
  28. package/dist/components/InkSidebar.svelte.d.ts.map +1 -0
  29. package/dist/components/InkSignatureDialog.svelte +7 -3
  30. package/dist/components/InkSignatureDialog.svelte.d.ts +4 -3
  31. package/dist/components/InkSignatureDialog.svelte.d.ts.map +1 -1
  32. package/dist/components/InkSignatureMenu.svelte +85 -77
  33. package/dist/components/InkSignatureMenu.svelte.d.ts.map +1 -1
  34. package/dist/components/InkThumbnail.svelte +74 -0
  35. package/dist/components/InkThumbnail.svelte.d.ts +19 -0
  36. package/dist/components/InkThumbnail.svelte.d.ts.map +1 -0
  37. package/dist/components/InkThumbnails.svelte +146 -0
  38. package/dist/components/InkThumbnails.svelte.d.ts +24 -0
  39. package/dist/components/InkThumbnails.svelte.d.ts.map +1 -0
  40. package/dist/components/InkToolButton.svelte +23 -24
  41. package/dist/components/InkToolButton.svelte.d.ts.map +1 -1
  42. package/dist/components/InkToolbarButton.svelte +33 -0
  43. package/dist/components/InkToolbarButton.svelte.d.ts +12 -0
  44. package/dist/components/InkToolbarButton.svelte.d.ts.map +1 -0
  45. package/dist/components/InkViewport.svelte +4 -0
  46. package/dist/components/InkViewport.svelte.d.ts.map +1 -1
  47. package/dist/components/InkZoomSelect.svelte +1 -1
  48. package/dist/components/RecordCards.svelte +173 -0
  49. package/dist/components/RecordCards.svelte.d.ts +16 -0
  50. package/dist/components/RecordCards.svelte.d.ts.map +1 -0
  51. package/dist/components/SidebarTrigger.svelte +20 -0
  52. package/dist/components/SidebarTrigger.svelte.d.ts +9 -0
  53. package/dist/components/SidebarTrigger.svelte.d.ts.map +1 -0
  54. package/dist/components/SignatureForm.svelte +9 -3
  55. package/dist/components/SignatureForm.svelte.d.ts.map +1 -1
  56. package/dist/components/body-lock.d.ts +3 -0
  57. package/dist/components/body-lock.d.ts.map +1 -0
  58. package/dist/components/body-lock.js +43 -0
  59. package/dist/components/sidebar-context.d.ts +24 -0
  60. package/dist/components/sidebar-context.d.ts.map +1 -0
  61. package/dist/components/sidebar-context.js +13 -0
  62. package/dist/context.d.ts +6 -0
  63. package/dist/context.d.ts.map +1 -1
  64. package/dist/hooks/useAnnotations.svelte.d.ts +78 -0
  65. package/dist/hooks/useAnnotations.svelte.d.ts.map +1 -1
  66. package/dist/hooks/useRecordSelection.svelte.d.ts +11 -0
  67. package/dist/hooks/useRecordSelection.svelte.d.ts.map +1 -0
  68. package/dist/hooks/useRecordSelection.svelte.js +19 -0
  69. package/dist/hooks/useRecords.svelte.d.ts +14 -0
  70. package/dist/hooks/useRecords.svelte.d.ts.map +1 -0
  71. package/dist/hooks/useRecords.svelte.js +24 -0
  72. package/dist/hooks/useRegisteredTool.svelte.d.ts +24 -0
  73. package/dist/hooks/useRegisteredTool.svelte.d.ts.map +1 -0
  74. package/dist/hooks/useRegisteredTool.svelte.js +47 -0
  75. package/dist/hooks/useSearch.svelte.d.ts.map +1 -1
  76. package/dist/hooks/useSearch.svelte.js +1 -0
  77. package/dist/hooks/useToolOptions.svelte.d.ts +8 -0
  78. package/dist/hooks/useToolOptions.svelte.d.ts.map +1 -1
  79. package/dist/hooks/useToolOptions.svelte.js +12 -2
  80. package/dist/index.d.ts +20 -0
  81. package/dist/index.d.ts.map +1 -1
  82. package/dist/index.js +11 -0
  83. package/dist/to-readable.svelte.d.ts +8 -0
  84. package/dist/to-readable.svelte.d.ts.map +1 -0
  85. package/dist/to-readable.svelte.js +15 -0
  86. package/package.json +5 -5
package/README.md CHANGED
@@ -53,6 +53,7 @@ Svelte 5.46.4 or later is a peer dependency. Add the Vite plugin from `@inkbar/c
53
53
  InkZoomIn,
54
54
  InkThreadPopover,
55
55
  } from '@inkbar/svelte'
56
+ import { highlightTool, noteTool, redactAreaTool } from '@inkbar/core/dom'
56
57
  import type { Annotation } from '@inkbar/core'
57
58
  import { assets } from 'virtual:inkbar-assets'
58
59
  import '@inkbar/theme/theme.css'
@@ -60,9 +61,10 @@ Svelte 5.46.4 or later is a peer dependency. Add the Vite plugin from `@inkbar/c
60
61
  let { signedUrl, documentId }: { signedUrl: string; documentId: string } = $props()
61
62
 
62
63
  let annotations = $state.raw<Annotation[]>([])
64
+ const tools = [highlightTool(), redactAreaTool(), noteTool()]
63
65
  </script>
64
66
 
65
- <InkRoot src={{ url: signedUrl, key: documentId }} {assets} bind:annotations>
67
+ <InkRoot src={{ url: signedUrl, key: documentId }} {assets} {tools} bind:annotations>
66
68
  <InkToolbar>
67
69
  <InkToolButton tool="select-text" />
68
70
  <InkToolButton tool="select" />
@@ -87,7 +89,9 @@ Svelte 5.46.4 or later is a peer dependency. Add the Vite plugin from `@inkbar/c
87
89
  </InkRoot>
88
90
  ```
89
91
 
90
- 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. `bind:annotations` makes your list the source of truth. Leave it off and Inkbar keeps the state itself, with undo and redo.
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`.
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.
91
95
 
92
96
  ## Controlled mode
93
97
 
@@ -161,7 +165,7 @@ The highlight tool leaves `contents` free for the comment. `doc.text(page)` retu
161
165
 
162
166
  ## Signatures
163
167
 
164
- `InkSignatureMenu` lists saved signatures, picks one for the `signature` tool, removes them, and opens `InkSignatureDialog` to draw, type or upload a new one. 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.
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.
165
169
 
166
170
  ## Plugins
167
171
 
@@ -187,11 +191,71 @@ Plugin stores undo with annotations. In controlled mode with `history`, each und
187
191
 
188
192
  The bar handles Escape itself and stops it there, so an `onkeydown` you pass to `InkFindBar` never sees Escape.
189
193
 
194
+ ## Sidebars
195
+
196
+ `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
+
198
+ ```svelte
199
+ <InkRoot {src} {assets}>
200
+ <div class="app">
201
+ <InkSidebar side="left" layout="rail" bind:open>
202
+ <InkPanel id="pages" label="Pages" icon={Square}>
203
+ <InkThumbnails />
204
+ </InkPanel>
205
+ <InkPanel id="notes" label="Notes" icon={MessageSquare} badge={noteCount}>
206
+ <MyNotes />
207
+ </InkPanel>
208
+ </InkSidebar>
209
+ <InkViewport />
210
+ </div>
211
+ </InkRoot>
212
+ ```
213
+
214
+ `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
+
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.
217
+
218
+ ## Thumbnails
219
+
220
+ `InkThumbnail` draws one page (0-based `page`) as a small image, with its annotations and plugin marks. Give it a `width` or a `height` and the other follows the page; with neither it is 120px wide. `annotations={false}` and `marks={false}` leave those out, and `label` replaces its accessible name ("Page 4" for page 3). It is an image with no focus or click behaviour, so wrap it in the control you need, a checkbox label in a dialog for instance.
221
+
222
+ `InkThumbnails` lists the pages as thumbnails, each a button that scrolls the viewport to its page, with the current page marked `aria-current="page"`. It fills its parent's height and scrolls. `pages` lists the pages to show, in order. 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` pass to each thumbnail. Only the items near the visible part are mounted. The `item` snippet adds to each item after its page number:
223
+
224
+ ```svelte
225
+ <InkThumbnails>
226
+ {#snippet item({ page, current })}
227
+ <span class="page-count">{counts[page]}</span>
228
+ {/snippet}
229
+ </InkThumbnails>
230
+ ```
231
+
232
+ 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
+
234
+ ## Record lists
235
+
236
+ `InkRecordList` lists a plugin's records as cards, the same cards their marks open, with the same actions. Name the records `<plugin>.<name>`:
237
+
238
+ ```svelte
239
+ <InkPanel id="suggestions" label="Suggestions" icon={MessageSquare} badge={count}>
240
+ <InkRecordList records="review.suggestions" bind:count>
241
+ {#snippet empty()}No suggestions left.{/snippet}
242
+ </InkRecordList>
243
+ </InkPanel>
244
+ ```
245
+
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.
247
+
248
+ `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
+
250
+ To build your own, `useRecords<T>(name)` returns the live `records` and the `handle` to read and write them, each write one undo step, and `useRecordSelection(name)` returns `selected` and `select(id | null)`. Both need `InkRoot`. Plugins are set up once it has mounted, so until then `handle` is null, `records` is empty and nothing is selected. Both lists fill their parent's height and render empty on the server. The keys, focus and announcements are core's: see the [`@inkbar/core` README](https://github.com/inkbar/inkbar/tree/main/packages/core#record-lists).
251
+
190
252
  ## Custom rendering
191
253
 
192
- 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 }`. The props each receives are typed as `AnnotationSlotProps` and `ThreadPopoverSlotProps`, the same types the Vue and React packages export.
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.
255
+
256
+ `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.
193
257
 
194
- `InkRoot`, `InkToolbar`, `InkToolButton`, `InkCommand`, `InkUndo`, `InkRedo`, `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`.
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`.
195
259
 
196
260
  ```svelte
197
261
  <InkToolbar>
@@ -209,6 +273,7 @@ The scroll area takes focus. From there:
209
273
  - With the `note` tool active, `Enter` places a note at the centre of the current page and opens it.
210
274
  - With the `signature` tool active, `Enter` places the chosen signature at the centre of the current page.
211
275
  - `Ctrl` or `Cmd` + `Z` undoes. `Ctrl` or `Cmd` + `Shift` + `Z`, or `Ctrl` + `Y`, redoes. `Ctrl` and `Cmd` both work on every platform. The keys do what `InkUndo` and `InkRedo` do, from anywhere in `InkRoot`, toolbars included, and act only while that button is enabled. In a text field or editable content the keys stay with the element, so a field keeps its own text undo. In a `<select>` they are left to the browser, which does nothing with them.
276
+ - `Ctrl` or `Cmd` + `P` prints the PDF and `Ctrl` or `Cmd` + `S` downloads it, as `InkPrintButton` and `InkDownloadButton` do, from anywhere in `InkRoot` once the document is ready. Outside `InkRoot` the browser keeps them.
212
277
 
213
278
  On a focused annotation:
214
279
 
@@ -226,7 +291,7 @@ Components carry state on data attributes, for example `[data-annotation][data-s
226
291
 
227
292
  ## Hooks
228
293
 
229
- `useDocument`, `useAnnotations`, `useTool`, `useSelection`, `useZoom`, `usePage`, `useHistory`, `useEmbedded` and `useCommand` 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.
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.
230
295
 
231
296
  ## Licence
232
297
 
@@ -0,0 +1,62 @@
1
+ <script lang="ts" module>
2
+ import type { ChildSnippet } from './child-snippet.js'
3
+ import type { Snippet } from 'svelte'
4
+ import type { HTMLButtonAttributes } from 'svelte/elements'
5
+
6
+ export interface InkDownloadButtonProps extends Omit<HTMLButtonAttributes, 'children'> {
7
+ children?: Snippet | undefined
8
+ /** Render your own button: spread `props` on it. */
9
+ child?: ChildSnippet | undefined
10
+ /** The name to save under. Default: the document's `fileName`. */
11
+ filename?: string | undefined
12
+ }
13
+ </script>
14
+
15
+ <script lang="ts">
16
+ /*
17
+ * Saves the open document's file through the browser's download, under its `fileName`
18
+ * or `filename`. Disabled until the document is ready. Today that is the file as it was opened,
19
+ * without this session's annotations.
20
+ */
21
+ import { downloadDocument } from '@inkbar/core/dom'
22
+ import { Download } from '@inkbar/icons'
23
+ import { Toolbar } from 'bits-ui'
24
+
25
+ import { useInkContext } from '../context.js'
26
+
27
+ import { asBitsProps } from './child-snippet.js'
28
+ import InkIcon from './InkIcon.svelte'
29
+
30
+ let {
31
+ class: className,
32
+ children,
33
+ child,
34
+ disabled,
35
+ onclick,
36
+ filename,
37
+ ...rest
38
+ }: InkDownloadButtonProps = $props()
39
+
40
+ const ctx = useInkContext('InkDownloadButton')
41
+ const ready = $derived(ctx.document !== null && ctx.status.state === 'ready')
42
+ </script>
43
+
44
+ <Toolbar.Button
45
+ class={['ink-download', className]}
46
+ data-ink-download=""
47
+ aria-label="Download"
48
+ title="Download"
49
+ disabled={!ready || !!disabled}
50
+ {...child ? { child } : {}}
51
+ {...asBitsProps(rest)}
52
+ onclick={(e) => {
53
+ onclick?.(e)
54
+ const doc = ctx.document
55
+ if (e.defaultPrevented || !doc) return
56
+ void downloadDocument(doc, filename === undefined ? {} : { filename }).catch(
57
+ () => undefined
58
+ )
59
+ }}
60
+ >
61
+ {#if children}{@render children()}{:else}<InkIcon icon={Download} />{/if}
62
+ </Toolbar.Button>
@@ -0,0 +1,14 @@
1
+ import type { ChildSnippet } from './child-snippet.js';
2
+ import type { Snippet } from 'svelte';
3
+ import type { HTMLButtonAttributes } from 'svelte/elements';
4
+ export interface InkDownloadButtonProps extends Omit<HTMLButtonAttributes, 'children'> {
5
+ children?: Snippet | undefined;
6
+ /** Render your own button: spread `props` on it. */
7
+ child?: ChildSnippet | undefined;
8
+ /** The name to save under. Default: the document's `fileName`. */
9
+ filename?: string | undefined;
10
+ }
11
+ declare const InkDownloadButton: import("svelte").Component<InkDownloadButtonProps, {}, "">;
12
+ type InkDownloadButton = ReturnType<typeof InkDownloadButton>;
13
+ export default InkDownloadButton;
14
+ //# sourceMappingURL=InkDownloadButton.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"InkDownloadButton.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/InkDownloadButton.svelte.ts"],"names":[],"mappings":"AAGE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AACtD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAA;AACrC,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAA;AAE3D,MAAM,WAAW,sBAAuB,SAAQ,IAAI,CAAC,oBAAoB,EAAE,UAAU,CAAC;IACpF,QAAQ,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;IAC9B,oDAAoD;IACpD,KAAK,CAAC,EAAE,YAAY,GAAG,SAAS,CAAA;IAChC,kEAAkE;IAClE,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAC9B;AAyDH,QAAA,MAAM,iBAAiB,4DAAwC,CAAC;AAChE,KAAK,iBAAiB,GAAG,UAAU,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAC9D,eAAe,iBAAiB,CAAC"}
@@ -18,7 +18,7 @@
18
18
  * Closing returns focus to where it was before the bar opened, else to the document.
19
19
  */
20
20
  import { findAnnouncer, findCountText } from '@inkbar/core/dom'
21
- import { CaseSensitive, ChevronDown, ChevronUp, X } from '@inkbar/icons'
21
+ import { CaseSensitive, ChevronDown, ChevronUp, Search, X } from '@inkbar/icons'
22
22
  import { untrack } from 'svelte'
23
23
 
24
24
  import { useInkContext } from '../context.js'
@@ -149,20 +149,23 @@
149
149
  hidden={!open}
150
150
  {@attach into(target)}
151
151
  >
152
- <input
153
- bind:this={field}
154
- type="text"
155
- aria-label="Find in document"
156
- autocomplete="off"
157
- spellcheck={false}
158
- oninput={(e) => search.setQuery(e.currentTarget.value)}
159
- onkeydown={(e) => {
160
- if (e.key !== 'Enter' || e.isComposing) return
161
- e.preventDefault()
162
- if (e.shiftKey) search.previous()
163
- else search.next()
164
- }}
165
- />
152
+ <div class="ink-input-group">
153
+ <span class="ink-input-accessory"><InkIcon icon={Search} /></span>
154
+ <input
155
+ bind:this={field}
156
+ type="text"
157
+ aria-label="Find in document"
158
+ autocomplete="off"
159
+ spellcheck={false}
160
+ oninput={(e) => search.setQuery(e.currentTarget.value)}
161
+ onkeydown={(e) => {
162
+ if (e.key !== 'Enter' || e.isComposing) return
163
+ e.preventDefault()
164
+ if (e.shiftKey) search.previous()
165
+ else search.next()
166
+ }}
167
+ />
168
+ </div>
166
169
  <span class="ink-find-count" data-ink-find-count=""
167
170
  >{findCountText(search.state)}</span
168
171
  >
@@ -1 +1 @@
1
- {"version":3,"file":"InkFindBar.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/InkFindBar.svelte.ts"],"names":[],"mappings":"AAGE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAErD,MAAM,MAAM,eAAe,GAAG,IAAI,CAAC,cAAc,CAAC,cAAc,CAAC,EAAE,UAAU,CAAC,CAC/E;AAmLD,QAAA,MAAM,UAAU,qDAAwC,CAAC;AACzD,KAAK,UAAU,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;AAChD,eAAe,UAAU,CAAC"}
1
+ {"version":3,"file":"InkFindBar.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/InkFindBar.svelte.ts"],"names":[],"mappings":"AAGE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAErD,MAAM,MAAM,eAAe,GAAG,IAAI,CAAC,cAAc,CAAC,cAAc,CAAC,EAAE,UAAU,CAAC,CAC/E;AAsLD,QAAA,MAAM,UAAU,qDAAwC,CAAC;AACzD,KAAK,UAAU,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;AAChD,eAAe,UAAU,CAAC"}
@@ -30,8 +30,9 @@
30
30
  import { Popover, Toolbar } from 'bits-ui'
31
31
 
32
32
  import { useDocument } from '../hooks/useDocument.svelte.js'
33
+ import { useRegisteredTool } from '../hooks/useRegisteredTool.svelte.js'
33
34
  import { useTool } from '../hooks/useTool.svelte.js'
34
- import { useToolOptions } from '../hooks/useToolOptions.svelte.js'
35
+ import { useToolOptionsWhile } from '../hooks/useToolOptions.svelte.js'
35
36
 
36
37
  import InkColorMenu from './InkColorMenu.svelte'
37
38
  import InkIcon from './InkIcon.svelte'
@@ -41,7 +42,12 @@
41
42
 
42
43
  const current = useTool()
43
44
  const inkDocument = useDocument()
44
- const highlight = useToolOptions<HighlightOptions>('highlight')
45
+ // Nothing renders while no `highlight` tool is registered.
46
+ const registered = useRegisteredTool('highlight')
47
+ const highlight = useToolOptionsWhile<HighlightOptions>(
48
+ 'highlight',
49
+ () => registered.current
50
+ )
45
51
  let open = $state(false)
46
52
  /** The split button: the panel lines up under it rather than the chevron. */
47
53
  let group = $state<HTMLElement | null>(null)
@@ -72,97 +78,99 @@
72
78
  }
73
79
  </script>
74
80
 
75
- <div
76
- bind:this={group}
77
- role="group"
78
- aria-label="Highlight"
79
- class={['ink-split-button ink-highlight-menu', className]}
80
- data-ink-highlight-menu=""
81
- {...rest}
82
- >
83
- <Toolbar.Button
84
- class="ink-tool-button ink-highlight-button"
85
- data-ink-tool-button=""
86
- data-ink-highlight-button=""
87
- data-tool="highlight"
88
- data-kind={model.kind}
89
- data-state={active ? 'on' : 'off'}
90
- aria-pressed={active}
91
- aria-label={model.label}
92
- title={model.label}
93
- onclick={() => current.setTool('highlight')}
81
+ {#if registered.current}
82
+ <div
83
+ bind:this={group}
84
+ role="group"
85
+ aria-label="Highlight"
86
+ class={['ink-split-button ink-highlight-menu', className]}
87
+ data-ink-highlight-menu=""
88
+ {...rest}
94
89
  >
95
- {#if children}{@render children()}{:else}<InkIcon
96
- icon={MARKUP_ICONS[model.kind]}
97
- />{/if}
98
- </Toolbar.Button>
99
- <Popover.Root bind:open>
100
- <Popover.Trigger>
101
- {#snippet child({ props })}
102
- <Toolbar.Button
103
- {...props}
104
- class="ink-menu-trigger ink-highlight-menu-trigger"
105
- data-ink-highlight-menu-trigger=""
106
- data-state={open ? 'open' : 'closed'}
107
- aria-label="Choose highlight"
108
- title="Choose highlight"
109
- aria-haspopup="menu"
110
- onkeydown={onTriggerKeydown}
90
+ <Toolbar.Button
91
+ class="ink-tool-button ink-highlight-button"
92
+ data-ink-tool-button=""
93
+ data-ink-highlight-button=""
94
+ data-tool="highlight"
95
+ data-kind={model.kind}
96
+ data-state={active ? 'on' : 'off'}
97
+ aria-pressed={active}
98
+ aria-label={model.label}
99
+ title={model.label}
100
+ onclick={() => current.setTool('highlight')}
101
+ >
102
+ {#if children}{@render children()}{:else}<InkIcon
103
+ icon={MARKUP_ICONS[model.kind]}
104
+ />{/if}
105
+ </Toolbar.Button>
106
+ <Popover.Root bind:open>
107
+ <Popover.Trigger>
108
+ {#snippet child({ props })}
109
+ <Toolbar.Button
110
+ {...props}
111
+ class="ink-menu-trigger ink-highlight-menu-trigger"
112
+ data-ink-highlight-menu-trigger=""
113
+ data-state={open ? 'open' : 'closed'}
114
+ aria-label="Choose highlight"
115
+ title="Choose highlight"
116
+ aria-haspopup="menu"
117
+ onkeydown={onTriggerKeydown}
118
+ >
119
+ <InkIcon icon={ChevronDown} />
120
+ </Toolbar.Button>
121
+ {/snippet}
122
+ </Popover.Trigger>
123
+ <Popover.Portal>
124
+ <Popover.Content
125
+ bind:ref={menu}
126
+ role="menu"
127
+ aria-label="Highlights"
128
+ class="ink-select-content ink-menu-content"
129
+ data-ink-select-content=""
130
+ data-ink-menu="highlight"
131
+ customAnchor={group}
132
+ side="bottom"
133
+ align="start"
134
+ sideOffset={4}
135
+ collisionPadding={8}
136
+ onOpenAutoFocus={(e) => openFocus.onOpenAutoFocus(e, menu)}
137
+ onfocusin={openFocus.onfocusin}
138
+ onkeydowncapture={(e: KeyboardEvent) => menu && handleMenuKeys(e, menu)}
111
139
  >
112
- <InkIcon icon={ChevronDown} />
113
- </Toolbar.Button>
114
- {/snippet}
115
- </Popover.Trigger>
116
- <Popover.Portal>
117
- <Popover.Content
118
- bind:ref={menu}
119
- role="menu"
120
- aria-label="Highlights"
121
- class="ink-select-content ink-menu-content"
122
- data-ink-select-content=""
123
- data-ink-menu="highlight"
124
- customAnchor={group}
125
- side="bottom"
126
- align="start"
127
- sideOffset={4}
128
- collisionPadding={8}
129
- onOpenAutoFocus={(e) => openFocus.onOpenAutoFocus(e, menu)}
130
- onfocusin={openFocus.onfocusin}
131
- onkeydowncapture={(e: KeyboardEvent) => menu && handleMenuKeys(e, menu)}
132
- >
133
- <div role="group" aria-label="Kinds" class="ink-highlight-kinds">
134
- {#each model.items as item (item.kind)}
135
- <button
136
- type="button"
137
- role="menuitemradio"
138
- class="ink-select-item ink-menu-item ink-highlight-item"
139
- data-ink-select-item=""
140
+ <div role="group" aria-label="Kinds" class="ink-highlight-kinds">
141
+ {#each model.items as item (item.kind)}
142
+ <button
143
+ type="button"
144
+ role="menuitemradio"
145
+ class="ink-select-item ink-menu-item ink-highlight-item"
146
+ data-ink-select-item=""
147
+ tabindex={-1}
148
+ data-key={item.kind}
149
+ aria-checked={item.checked}
150
+ data-state={item.checked ? 'checked' : 'unchecked'}
151
+ onclick={() => choose(item.kind)}
152
+ >
153
+ <InkIcon icon={MARKUP_ICONS[item.kind]} size={16} />
154
+ {item.label}
155
+ </button>
156
+ {/each}
157
+ </div>
158
+ <div role="separator" class="ink-select-separator"></div>
159
+ <div role="group" aria-label="Colour" class="ink-highlight-colour">
160
+ <span class="ink-highlight-colour-label" aria-hidden="true">Colour</span>
161
+ <!-- Capture on the panel moves on from this item before its own ArrowDown opens it. -->
162
+ <InkColorMenu
163
+ property="border"
164
+ group="markup"
165
+ label="Highlight colour"
166
+ standalone
167
+ {fallback}
168
+ role="menuitem"
140
169
  tabindex={-1}
141
- data-key={item.kind}
142
- aria-checked={item.checked}
143
- data-state={item.checked ? 'checked' : 'unchecked'}
144
- onclick={() => choose(item.kind)}
145
- >
146
- <InkIcon icon={MARKUP_ICONS[item.kind]} size={16} />
147
- {item.label}
148
- </button>
149
- {/each}
150
- </div>
151
- <div role="separator" class="ink-select-separator"></div>
152
- <div role="group" aria-label="Colour" class="ink-highlight-colour">
153
- <span class="ink-highlight-colour-label" aria-hidden="true">Colour</span>
154
- <!-- Capture on the panel moves on from this item before its own ArrowDown opens it. -->
155
- <InkColorMenu
156
- property="border"
157
- group="markup"
158
- label="Highlight colour"
159
- standalone
160
- {fallback}
161
- role="menuitem"
162
- tabindex={-1}
163
- />
164
- </div>
165
- </Popover.Content>
166
- </Popover.Portal>
167
- </Popover.Root>
168
- </div>
170
+ />
171
+ </div>
172
+ </Popover.Content>
173
+ </Popover.Portal>
174
+ </Popover.Root>
175
+ </div>
176
+ {/if}
@@ -1 +1 @@
1
- {"version":3,"file":"InkHighlightMenu.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/InkHighlightMenu.svelte.ts"],"names":[],"mappings":"AAGE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAA;AACrC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAErD;;;;;GAKG;AACH,MAAM,WAAW,qBAAsB,SAAQ,IAAI,CACjD,cAAc,CAAC,cAAc,CAAC,EAC9B,UAAU,CACX;IACC,uCAAuC;IACvC,QAAQ,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;CAC/B;AA2GH,QAAA,MAAM,gBAAgB,2DAAwC,CAAC;AAC/D,KAAK,gBAAgB,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAC5D,eAAe,gBAAgB,CAAC"}
1
+ {"version":3,"file":"InkHighlightMenu.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/InkHighlightMenu.svelte.ts"],"names":[],"mappings":"AAGE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAA;AACrC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAErD;;;;;GAKG;AACH,MAAM,WAAW,qBAAsB,SAAQ,IAAI,CACjD,cAAc,CAAC,cAAc,CAAC,EAC9B,UAAU,CACX;IACC,uCAAuC;IACvC,QAAQ,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;CAC/B;AAoHH,QAAA,MAAM,gBAAgB,2DAAwC,CAAC;AAC/D,KAAK,gBAAgB,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAC5D,eAAe,gBAAgB,CAAC"}
@@ -0,0 +1,73 @@
1
+ <script module lang="ts">
2
+ import type { IconNode } from '@inkbar/icons'
3
+ import type { Snippet } from 'svelte'
4
+
5
+ export interface InkPanelProps {
6
+ /** Names the panel in the sidebar's `open`. */
7
+ id: string
8
+ /** Names its trigger. */
9
+ label: string
10
+ /** An `@inkbar/icons` node, shown on the rail. */
11
+ icon?: IconNode | undefined
12
+ /** A count or short text beside it, hidden when empty or 0. */
13
+ badge?: string | number | undefined
14
+ /** The panel's content. */
15
+ children?: Snippet | undefined
16
+ }
17
+ </script>
18
+
19
+ <script lang="ts">
20
+ /*
21
+ * One panel of an `InkSidebar`, holding `children` as its content. Closed, the panel stays
22
+ * mounted and hidden. In a stack it renders its own header above its content.
23
+ */
24
+ import { useSidebarContext } from './sidebar-context.js'
25
+ import SidebarTrigger from './SidebarTrigger.svelte'
26
+
27
+ import type { SidebarPanelInfo } from '@inkbar/core/dom'
28
+
29
+ let { id, label, icon, badge, children }: InkPanelProps = $props()
30
+
31
+ const ctx = useSidebarContext('InkPanel')
32
+ let wrapper = $state<HTMLDivElement | null>(null)
33
+
34
+ const info = (): SidebarPanelInfo => ({
35
+ id,
36
+ label,
37
+ ...(icon !== undefined && { icon }),
38
+ ...(badge !== undefined && { badge }),
39
+ })
40
+ // Registered now, so the server renders this panel's attributes; ordered once in the DOM.
41
+ const unregister = ctx.register({ info, element: () => wrapper })
42
+ $effect(() => {
43
+ ctx.arrange()
44
+ return unregister
45
+ })
46
+
47
+ const view = $derived.by(() => {
48
+ const s = ctx.state.current
49
+ return {
50
+ stack: s.layout === 'stack',
51
+ trigger: ctx.controller.triggerAttrs(id),
52
+ panel: ctx.controller.panelAttrs(id),
53
+ }
54
+ })
55
+ </script>
56
+
57
+ <!-- A section in a stack; in rail and tabs the theme leaves the layout to the panel. Kept in
58
+ every layout, so the content is never remounted. -->
59
+ <div
60
+ bind:this={wrapper}
61
+ class={['ink-sidebar-slot', view.stack && 'ink-sidebar-section']}
62
+ >
63
+ {#if view.stack}
64
+ <button class="ink-sidebar-header" {...view.trigger}>
65
+ <SidebarTrigger panel={info()} rail={false} />
66
+ </button>
67
+ {/if}
68
+ <div class="ink-sidebar-panel" {...view.panel}>
69
+ <!-- The rail button names the panel for assistive technology; this repeats it on screen. -->
70
+ <div class="ink-sidebar-panel-title" aria-hidden="true">{label}</div>
71
+ <div class="ink-sidebar-panel-body">{@render children?.()}</div>
72
+ </div>
73
+ </div>
@@ -0,0 +1,18 @@
1
+ import type { IconNode } from '@inkbar/icons';
2
+ import type { Snippet } from 'svelte';
3
+ export interface InkPanelProps {
4
+ /** Names the panel in the sidebar's `open`. */
5
+ id: string;
6
+ /** Names its trigger. */
7
+ label: string;
8
+ /** An `@inkbar/icons` node, shown on the rail. */
9
+ icon?: IconNode | undefined;
10
+ /** A count or short text beside it, hidden when empty or 0. */
11
+ badge?: string | number | undefined;
12
+ /** The panel's content. */
13
+ children?: Snippet | undefined;
14
+ }
15
+ declare const InkPanel: import("svelte").Component<InkPanelProps, {}, "">;
16
+ type InkPanel = ReturnType<typeof InkPanel>;
17
+ export default InkPanel;
18
+ //# sourceMappingURL=InkPanel.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"InkPanel.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/InkPanel.svelte.ts"],"names":[],"mappings":"AAGE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AAC7C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAA;AAErC,MAAM,WAAW,aAAa;IAC5B,+CAA+C;IAC/C,EAAE,EAAE,MAAM,CAAA;IACV,yBAAyB;IACzB,KAAK,EAAE,MAAM,CAAA;IACb,kDAAkD;IAClD,IAAI,CAAC,EAAE,QAAQ,GAAG,SAAS,CAAA;IAC3B,+DAA+D;IAC/D,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAA;IACnC,2BAA2B;IAC3B,QAAQ,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;CAC/B;AAiEH,QAAA,MAAM,QAAQ,mDAAwC,CAAC;AACvD,KAAK,QAAQ,GAAG,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC;AAC5C,eAAe,QAAQ,CAAC"}