@inkbar/svelte 0.0.0-stage → 0.2.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 (117) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +181 -2
  3. package/dist/components/InkAnnotation.svelte +73 -0
  4. package/dist/components/InkAnnotation.svelte.d.ts +13 -0
  5. package/dist/components/InkAnnotation.svelte.d.ts.map +1 -0
  6. package/dist/components/InkIcon.svelte +24 -0
  7. package/dist/components/InkIcon.svelte.d.ts +9 -0
  8. package/dist/components/InkIcon.svelte.d.ts.map +1 -0
  9. package/dist/components/InkNextPage.svelte +38 -0
  10. package/dist/components/InkNextPage.svelte.d.ts +5 -0
  11. package/dist/components/InkNextPage.svelte.d.ts.map +1 -0
  12. package/dist/components/InkNotePopover.svelte +193 -0
  13. package/dist/components/InkNotePopover.svelte.d.ts +9 -0
  14. package/dist/components/InkNotePopover.svelte.d.ts.map +1 -0
  15. package/dist/components/InkPage.svelte +125 -0
  16. package/dist/components/InkPage.svelte.d.ts +22 -0
  17. package/dist/components/InkPage.svelte.d.ts.map +1 -0
  18. package/dist/components/InkPageCount.svelte +20 -0
  19. package/dist/components/InkPageCount.svelte.d.ts +6 -0
  20. package/dist/components/InkPageCount.svelte.d.ts.map +1 -0
  21. package/dist/components/InkPageInput.svelte +49 -0
  22. package/dist/components/InkPageInput.svelte.d.ts +6 -0
  23. package/dist/components/InkPageInput.svelte.d.ts.map +1 -0
  24. package/dist/components/InkPrevPage.svelte +38 -0
  25. package/dist/components/InkPrevPage.svelte.d.ts +5 -0
  26. package/dist/components/InkPrevPage.svelte.d.ts.map +1 -0
  27. package/dist/components/InkRedo.svelte +38 -0
  28. package/dist/components/InkRedo.svelte.d.ts +5 -0
  29. package/dist/components/InkRedo.svelte.d.ts.map +1 -0
  30. package/dist/components/InkRoot.svelte +358 -0
  31. package/dist/components/InkRoot.svelte.d.ts +51 -0
  32. package/dist/components/InkRoot.svelte.d.ts.map +1 -0
  33. package/dist/components/InkSuggestionCard.svelte +121 -0
  34. package/dist/components/InkSuggestionCard.svelte.d.ts +9 -0
  35. package/dist/components/InkSuggestionCard.svelte.d.ts.map +1 -0
  36. package/dist/components/InkToolButton.svelte +68 -0
  37. package/dist/components/InkToolButton.svelte.d.ts +17 -0
  38. package/dist/components/InkToolButton.svelte.d.ts.map +1 -0
  39. package/dist/components/InkToolbar.svelte +40 -0
  40. package/dist/components/InkToolbar.svelte.d.ts +13 -0
  41. package/dist/components/InkToolbar.svelte.d.ts.map +1 -0
  42. package/dist/components/InkToolbarSeparator.svelte +18 -0
  43. package/dist/components/InkToolbarSeparator.svelte.d.ts +6 -0
  44. package/dist/components/InkToolbarSeparator.svelte.d.ts.map +1 -0
  45. package/dist/components/InkUndo.svelte +38 -0
  46. package/dist/components/InkUndo.svelte.d.ts +5 -0
  47. package/dist/components/InkUndo.svelte.d.ts.map +1 -0
  48. package/dist/components/InkViewport.svelte +306 -0
  49. package/dist/components/InkViewport.svelte.d.ts +27 -0
  50. package/dist/components/InkViewport.svelte.d.ts.map +1 -0
  51. package/dist/components/InkZoomIn.svelte +36 -0
  52. package/dist/components/InkZoomIn.svelte.d.ts +5 -0
  53. package/dist/components/InkZoomIn.svelte.d.ts.map +1 -0
  54. package/dist/components/InkZoomOut.svelte +36 -0
  55. package/dist/components/InkZoomOut.svelte.d.ts +5 -0
  56. package/dist/components/InkZoomOut.svelte.d.ts.map +1 -0
  57. package/dist/components/InkZoomSelect.svelte +105 -0
  58. package/dist/components/InkZoomSelect.svelte.d.ts +7 -0
  59. package/dist/components/InkZoomSelect.svelte.d.ts.map +1 -0
  60. package/dist/components/ThreadForm.svelte +42 -0
  61. package/dist/components/ThreadForm.svelte.d.ts +11 -0
  62. package/dist/components/ThreadForm.svelte.d.ts.map +1 -0
  63. package/dist/components/annotation-element.svelte.d.ts +9 -0
  64. package/dist/components/annotation-element.svelte.d.ts.map +1 -0
  65. package/dist/components/annotation-element.svelte.js +25 -0
  66. package/dist/components/child-snippet.d.ts +22 -0
  67. package/dist/components/child-snippet.d.ts.map +1 -0
  68. package/dist/components/child-snippet.js +14 -0
  69. package/dist/components/history-button.d.ts +10 -0
  70. package/dist/components/history-button.d.ts.map +1 -0
  71. package/dist/components/history-button.js +1 -0
  72. package/dist/components/page-button.d.ts +10 -0
  73. package/dist/components/page-button.d.ts.map +1 -0
  74. package/dist/components/page-button.js +1 -0
  75. package/dist/components/popover-focus.d.ts +33 -0
  76. package/dist/components/popover-focus.d.ts.map +1 -0
  77. package/dist/components/popover-focus.js +63 -0
  78. package/dist/components/portal.d.ts +26 -0
  79. package/dist/components/portal.d.ts.map +1 -0
  80. package/dist/components/portal.js +55 -0
  81. package/dist/components/zoom-button.d.ts +10 -0
  82. package/dist/components/zoom-button.d.ts.map +1 -0
  83. package/dist/components/zoom-button.js +1 -0
  84. package/dist/context.d.ts +57 -0
  85. package/dist/context.d.ts.map +1 -0
  86. package/dist/context.js +34 -0
  87. package/dist/hooks/useAnnotations.svelte.d.ts +1683 -0
  88. package/dist/hooks/useAnnotations.svelte.d.ts.map +1 -0
  89. package/dist/hooks/useAnnotations.svelte.js +33 -0
  90. package/dist/hooks/useDocument.svelte.d.ts +12 -0
  91. package/dist/hooks/useDocument.svelte.d.ts.map +1 -0
  92. package/dist/hooks/useDocument.svelte.js +28 -0
  93. package/dist/hooks/useEmbedded.svelte.d.ts +7 -0
  94. package/dist/hooks/useEmbedded.svelte.d.ts.map +1 -0
  95. package/dist/hooks/useEmbedded.svelte.js +17 -0
  96. package/dist/hooks/useHistory.svelte.d.ts +7 -0
  97. package/dist/hooks/useHistory.svelte.d.ts.map +1 -0
  98. package/dist/hooks/useHistory.svelte.js +8 -0
  99. package/dist/hooks/usePage.svelte.d.ts +10 -0
  100. package/dist/hooks/usePage.svelte.d.ts.map +1 -0
  101. package/dist/hooks/usePage.svelte.js +29 -0
  102. package/dist/hooks/useSelection.svelte.d.ts +6 -0
  103. package/dist/hooks/useSelection.svelte.d.ts.map +1 -0
  104. package/dist/hooks/useSelection.svelte.js +12 -0
  105. package/dist/hooks/useTool.svelte.d.ts +6 -0
  106. package/dist/hooks/useTool.svelte.d.ts.map +1 -0
  107. package/dist/hooks/useTool.svelte.js +10 -0
  108. package/dist/hooks/useZoom.svelte.d.ts +9 -0
  109. package/dist/hooks/useZoom.svelte.d.ts.map +1 -0
  110. package/dist/hooks/useZoom.svelte.js +18 -0
  111. package/dist/index.d.ts +53 -0
  112. package/dist/index.d.ts.map +1 -0
  113. package/dist/index.js +29 -0
  114. package/dist/use-readable.svelte.d.ts +13 -0
  115. package/dist/use-readable.svelte.d.ts.map +1 -0
  116. package/dist/use-readable.svelte.js +28 -0
  117. package/package.json +56 -3
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Redactr Platforms Ltd
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,182 @@
1
- # Temporary Holding Version
1
+ <p align="center">
2
+ <picture>
3
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/inkbar/inkbar/main/.github/assets/inkbar-mark-on-dark.svg">
4
+ <img src="https://raw.githubusercontent.com/inkbar/inkbar/main/.github/assets/inkbar-mark-on-light.svg" alt="Inkbar" width="72" height="72">
5
+ </picture>
6
+ </p>
2
7
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
8
+ <h1 align="center">@inkbar/svelte</h1>
9
+
10
+ <p align="center">
11
+ A PDF viewer and editor for Svelte 5. No licence fee.
12
+ </p>
13
+
14
+ <p align="center">
15
+ <a href="https://inkbar.dev">Website</a> ·
16
+ <a href="https://inkbar.dev/docs/">Docs</a> ·
17
+ <a href="https://sandbox.inkbar.dev">Sandbox</a> ·
18
+ <a href="https://github.com/inkbar/inkbar">GitHub</a>
19
+ </p>
20
+
21
+ ---
22
+
23
+ Drop a PDF viewer into your Svelte app and let people highlight text, leave notes with replies and review redactions. `@inkbar/svelte` gives you components to build your own toolbars, popovers for notes and suggested redactions, and hooks for anything else. `@inkbar/theme` styles them all.
24
+
25
+ Annotations are plain typed objects. Bind them with `bind:annotations` to keep them in your own state, or let Inkbar hold them with undo and redo.
26
+
27
+ ## Install
28
+
29
+ ```sh
30
+ pnpm add @inkbar/svelte @inkbar/core @inkbar/theme
31
+ ```
32
+
33
+ Svelte 5.46.4 or later is a peer dependency. Add the Vite plugin from `@inkbar/core/vite` so the PDF engine 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
+
35
+ ## Usage
36
+
37
+ ```svelte
38
+ <script lang="ts">
39
+ import {
40
+ InkRoot,
41
+ InkViewport,
42
+ InkToolbar,
43
+ InkToolButton,
44
+ InkToolbarSeparator,
45
+ InkUndo,
46
+ InkRedo,
47
+ InkPrevPage,
48
+ InkPageInput,
49
+ InkPageCount,
50
+ InkNextPage,
51
+ InkZoomOut,
52
+ InkZoomSelect,
53
+ InkZoomIn,
54
+ InkNotePopover,
55
+ InkSuggestionCard,
56
+ } from '@inkbar/svelte'
57
+ import type { Annotation } from '@inkbar/core'
58
+ import { assets } from 'virtual:inkbar-assets'
59
+ import '@inkbar/theme/theme.css'
60
+
61
+ let { signedUrl, documentId }: { signedUrl: string; documentId: string } = $props()
62
+
63
+ let annotations = $state.raw<Annotation[]>([])
64
+ </script>
65
+
66
+ <InkRoot src={{ url: signedUrl, key: documentId }} {assets} bind:annotations>
67
+ <InkToolbar>
68
+ <InkToolButton tool="select" />
69
+ <InkToolButton tool="highlight" />
70
+ <InkToolButton tool="note" />
71
+ <InkToolbarSeparator />
72
+ <InkUndo />
73
+ <InkRedo />
74
+ </InkToolbar>
75
+ <InkViewport />
76
+ <InkToolbar label="Page and zoom">
77
+ <InkPrevPage />
78
+ <InkPageInput />
79
+ <InkPageCount />
80
+ <InkNextPage />
81
+ <InkZoomOut />
82
+ <InkZoomSelect />
83
+ <InkZoomIn />
84
+ </InkToolbar>
85
+ <InkNotePopover />
86
+ <InkSuggestionCard />
87
+ </InkRoot>
88
+ ```
89
+
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.
91
+
92
+ ## Controlled mode
93
+
94
+ With `bind:annotations`, Inkbar proposes each change by writing the binding and shows whatever list it holds. To change annotations yourself, replace the array: `annotations = [...annotations, next]`. In-place mutation (`push`, `splice`, editing an item) is not observed, so the viewer never sees it. Declare the list with `$state.raw`, since Inkbar replaces it whole.
95
+
96
+ To decide on each proposal before it applies, use a function binding. The setter receives the proposed list; apply it, change it or drop it:
97
+
98
+ ```svelte
99
+ <InkRoot
100
+ src={{ url: signedUrl, key: documentId }}
101
+ {assets}
102
+ bind:annotations={() => annotations, (next) => (annotations = next.filter(allowed))}
103
+ >
104
+ <InkViewport />
105
+ </InkRoot>
106
+ ```
107
+
108
+ `onAnnotationsChange` fires alongside the binding, and `onChange` carries the change itself. An `annotations` prop passed without a binding is overridden inside the component as proposals arrive, so bind it to keep control.
109
+
110
+ Undo and redo are off in controlled mode unless you pass `history={true}`. Inkbar then keeps a stack of your list's past values and applies undo and redo through the binding. Every change to the list is a step, including the list you load from your server when the document opens. Clear the history once that load has reached the viewer, so the first undo does not empty the list:
111
+
112
+ ```svelte
113
+ <script lang="ts">
114
+ import { tick } from 'svelte'
115
+ import { InkRoot, InkViewport } from '@inkbar/svelte'
116
+ import type { InkRootHandle } from '@inkbar/svelte'
117
+ import type { Annotation, InkDocument } from '@inkbar/core'
118
+ import { assets } from 'virtual:inkbar-assets'
119
+
120
+ let { signedUrl, documentId }: { signedUrl: string; documentId: string } = $props()
121
+
122
+ let annotations = $state.raw<Annotation[]>([])
123
+ let root = $state<InkRootHandle | undefined>()
124
+
125
+ async function onReady(doc: InkDocument) {
126
+ annotations = await loadAnnotations(doc.key)
127
+ await tick()
128
+ root?.history.clear()
129
+ }
130
+ </script>
131
+
132
+ <InkRoot
133
+ bind:this={root}
134
+ bind:annotations
135
+ history={true}
136
+ src={{ url: signedUrl, key: documentId }}
137
+ {assets}
138
+ {onReady}
139
+ >
140
+ <InkViewport />
141
+ </InkRoot>
142
+ ```
143
+
144
+ Inside `InkRoot`, `useHistory()` returns the same object. Its `batch(fn)` runs `fn` and records what it changes as one undo step. With `history={true}` on a controlled list, you still get one write to `bind:annotations` per add. The step lands when the current task ends, and `canUndo` updates then. Any other change you apply to the list in that window joins the step. In both modes, `fn` must be synchronous. The instance also exposes `getDocument()` and `getStatus()`, and `bind:ref` gives you the root element.
145
+
146
+ `key` identifies the document. Reuse the same key when only the signed URL changes and the document is not reloaded.
147
+
148
+ ## Note popover
149
+
150
+ `InkNotePopover` opens the thread of the activated note. A new note has no text yet, so the form first writes the note's own text (labelled "Comment"); after that it adds replies.
151
+
152
+ A root note with no text and no replies is removed when its popover closes, or when activation moves to another annotation, so abandoned notes do not stay on the page. Locked notes are never removed. If you add notes yourself (`add({ type: 'note', ... })`), give them `contents`, or they will be removed the first time someone opens and closes them.
153
+
154
+ ## Custom rendering
155
+
156
+ Slots are snippets. `InkViewport` takes `loading`, `empty`, `error`, `annotation` and `pageError`. `InkNotePopover` and `InkSuggestionCard` take `children` as a snippet that receives the popover's props. `InkToolButton` takes `children` as a snippet of `{ active, label }`. The props each receives are typed as `AnnotationSlotProps`, `NotePopoverSlotProps` and `SuggestionCardSlotProps`, the same types the Vue and React packages export.
157
+
158
+ `InkRoot`, `InkToolbar`, `InkToolButton`, `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`.
159
+
160
+ ```svelte
161
+ <InkToolbar>
162
+ {#snippet child({ props })}
163
+ <nav {...props} class="my-toolbar">…</nav>
164
+ {/snippet}
165
+ </InkToolbar>
166
+ ```
167
+
168
+ ## Keyboard
169
+
170
+ The scroll element takes focus. With the highlight or redact tool active, `Enter` turns the text selection into a mark; with the note tool, `Enter` places a note in the middle of the current page and opens it. On a focused annotation, `Enter` or `Space` selects and activates it (a note opens its thread and a suggested redaction its card; other types are only selected), `ArrowUp` and `ArrowDown` move between annotations, `Shift` + arrow moves it by 1 pt (add `Alt` for 10 pt), `Ctrl` or `Cmd` + `Shift` + arrow resizes an area mark, and `Delete` removes it. The full table is in the [`@inkbar/core` README](https://github.com/inkbar/inkbar/tree/main/packages/core#tools-and-keyboard).
171
+
172
+ ## Styling
173
+
174
+ Components carry state on data attributes, for example `[data-annotation][data-state="selected"]` and `[data-tool="highlight"][data-state="on"]`. `@inkbar/theme` gives a default look. Your own CSS overrides it without `!important`. `InkRoot`, `InkViewport`, `InkToolbar` and the toolbar controls accept `class`.
175
+
176
+ ## Hooks
177
+
178
+ `useDocument`, `useAnnotations`, `useTool`, `useSelection`, `useZoom`, `usePage`, `useHistory` and `useEmbedded` 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.
179
+
180
+ ## Licence
181
+
182
+ [MIT](https://github.com/inkbar/inkbar/blob/main/LICENSE). From the team behind [Redactr](https://redactr.io).
@@ -0,0 +1,73 @@
1
+ <script module lang="ts">
2
+ import type { Annotation } from '@inkbar/core'
3
+ import type { AnnotationSlotProps } from '@inkbar/core/dom'
4
+ import type { Snippet } from 'svelte'
5
+
6
+ export interface InkAnnotationProps {
7
+ annotation: Annotation
8
+ /** The overlay element; its `data-state` (which includes hover) drives `state`. */
9
+ element?: HTMLElement | undefined
10
+ children: Snippet<[AnnotationSlotProps]>
11
+ }
12
+ </script>
13
+
14
+ <script lang="ts">
15
+ /*
16
+ * Slot props for one annotation. It renders only its children: the overlay already draws the
17
+ * default visual for every annotation that is not custom, so this never draws a second one.
18
+ * InkPage portals it into the annotation's overlay element.
19
+ */
20
+ import { acceptSuggestion } from '@inkbar/core'
21
+
22
+ import { useInkContext } from '../context.js'
23
+
24
+ import type { AnnotationState } from '@inkbar/core/dom'
25
+
26
+ let { annotation, element, children }: InkAnnotationProps = $props()
27
+
28
+ const ctx = useInkContext('InkAnnotation')
29
+
30
+ function readState(el: HTMLElement): AnnotationState | null {
31
+ const s = el.dataset.state
32
+ return s === 'idle' || s === 'hover' || s === 'selected' ? s : null
33
+ }
34
+
35
+ let observed = $state.raw<AnnotationState | null>(null)
36
+ const observedEl = $derived(element)
37
+ $effect(() => {
38
+ const el = observedEl
39
+ if (!el) {
40
+ observed = null
41
+ return
42
+ }
43
+ observed = readState(el)
44
+ const observer = new MutationObserver(() => {
45
+ observed = readState(el)
46
+ })
47
+ observer.observe(el, { attributes: true, attributeFilter: ['data-state'] })
48
+ return () => observer.disconnect()
49
+ })
50
+
51
+ const isSuggestion = $derived(
52
+ annotation.type === 'redact' && annotation.origin === 'suggested'
53
+ )
54
+ const locked = $derived(annotation.locked === true)
55
+ const selected = $derived(ctx.selection.includes(annotation.id))
56
+ const store = () => ctx.document?.annotations
57
+ const slotProps = $derived<AnnotationSlotProps>({
58
+ annotation,
59
+ state: observed ?? (selected ? 'selected' : 'idle'),
60
+ locked,
61
+ select: () => store()?.select([annotation.id]),
62
+ remove: () => void store()?.remove(annotation.id),
63
+ accept: () => {
64
+ const s = store()
65
+ if (s && isSuggestion) acceptSuggestion(s, annotation.id)
66
+ },
67
+ dismiss: () => {
68
+ if (isSuggestion) store()?.remove(annotation.id)
69
+ },
70
+ })
71
+ </script>
72
+
73
+ {@render children(slotProps)}
@@ -0,0 +1,13 @@
1
+ import type { Annotation } from '@inkbar/core';
2
+ import type { AnnotationSlotProps } from '@inkbar/core/dom';
3
+ import type { Snippet } from 'svelte';
4
+ export interface InkAnnotationProps {
5
+ annotation: Annotation;
6
+ /** The overlay element; its `data-state` (which includes hover) drives `state`. */
7
+ element?: HTMLElement | undefined;
8
+ children: Snippet<[AnnotationSlotProps]>;
9
+ }
10
+ declare const InkAnnotation: import("svelte").Component<InkAnnotationProps, {}, "">;
11
+ type InkAnnotation = ReturnType<typeof InkAnnotation>;
12
+ export default InkAnnotation;
13
+ //# sourceMappingURL=InkAnnotation.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"InkAnnotation.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/InkAnnotation.svelte.ts"],"names":[],"mappings":"AAGE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAC9C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAA;AAC3D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAA;AAErC,MAAM,WAAW,kBAAkB;IACjC,UAAU,EAAE,UAAU,CAAA;IACtB,mFAAmF;IACnF,OAAO,CAAC,EAAE,WAAW,GAAG,SAAS,CAAA;IACjC,QAAQ,EAAE,OAAO,CAAC,CAAC,mBAAmB,CAAC,CAAC,CAAA;CACzC;AA2EH,QAAA,MAAM,aAAa,wDAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
@@ -0,0 +1,24 @@
1
+ <script module lang="ts">
2
+ import type { IconNode } from '@inkbar/icons'
3
+
4
+ export interface InkIconProps {
5
+ icon: IconNode
6
+ size?: number | undefined
7
+ }
8
+ </script>
9
+
10
+ <script lang="ts">
11
+ /*
12
+ * An icon from `@inkbar/icons`, hidden from assistive technology: the control it sits in
13
+ * carries the name. The theme sizes it with `--ink-icon-size`.
14
+ */
15
+ import { ICON_ATTRIBUTES } from '@inkbar/icons'
16
+
17
+ let { icon, size = 18 }: InkIconProps = $props()
18
+ </script>
19
+
20
+ <svg {...ICON_ATTRIBUTES} width={size} height={size} class="ink-icon" aria-hidden="true">
21
+ {#each icon as [tag, attrs], i (i)}
22
+ <svelte:element this={tag} {...attrs} />
23
+ {/each}
24
+ </svg>
@@ -0,0 +1,9 @@
1
+ import type { IconNode } from '@inkbar/icons';
2
+ export interface InkIconProps {
3
+ icon: IconNode;
4
+ size?: number | undefined;
5
+ }
6
+ declare const InkIcon: import("svelte").Component<InkIconProps, {}, "">;
7
+ type InkIcon = ReturnType<typeof InkIcon>;
8
+ export default InkIcon;
9
+ //# sourceMappingURL=InkIcon.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"InkIcon.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/InkIcon.svelte.ts"],"names":[],"mappings":"AAGE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AAE7C,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,QAAQ,CAAA;IACd,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAC1B;AA0BH,QAAA,MAAM,OAAO,kDAAwC,CAAC;AACtD,KAAK,OAAO,GAAG,UAAU,CAAC,OAAO,OAAO,CAAC,CAAC;AAC1C,eAAe,OAAO,CAAC"}
@@ -0,0 +1,38 @@
1
+ <script lang="ts">
2
+ import { ChevronRight } from '@inkbar/icons'
3
+ import { Toolbar } from 'bits-ui'
4
+
5
+ import { usePage } from '../hooks/usePage.svelte.js'
6
+
7
+ import { asBitsProps } from './child-snippet.js'
8
+ import InkIcon from './InkIcon.svelte'
9
+
10
+ import type { InkPageButtonProps } from './page-button.js'
11
+
12
+ let {
13
+ class: className,
14
+ children,
15
+ child,
16
+ disabled,
17
+ onclick,
18
+ ...rest
19
+ }: InkPageButtonProps = $props()
20
+
21
+ const page = usePage()
22
+ </script>
23
+
24
+ <Toolbar.Button
25
+ class={['ink-next-page', className]}
26
+ data-ink-next-page=""
27
+ aria-label="Next page"
28
+ title="Next page"
29
+ disabled={page.current >= page.count - 1 || !!disabled}
30
+ {...child ? { child } : {}}
31
+ {...asBitsProps(rest)}
32
+ onclick={(e) => {
33
+ onclick?.(e)
34
+ if (!e.defaultPrevented) page.next()
35
+ }}
36
+ >
37
+ {#if children}{@render children()}{:else}<InkIcon icon={ChevronRight} />{/if}
38
+ </Toolbar.Button>
@@ -0,0 +1,5 @@
1
+ import type { InkPageButtonProps } from './page-button.js';
2
+ declare const InkNextPage: import("svelte").Component<InkPageButtonProps, {}, "">;
3
+ type InkNextPage = ReturnType<typeof InkNextPage>;
4
+ export default InkNextPage;
5
+ //# sourceMappingURL=InkNextPage.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"InkNextPage.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/InkNextPage.svelte.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAkC3D,QAAA,MAAM,WAAW,wDAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}
@@ -0,0 +1,193 @@
1
+ <script module lang="ts">
2
+ import type { NotePopoverSlotProps } from '@inkbar/core/dom'
3
+ import type { Snippet } from 'svelte'
4
+
5
+ export interface InkNotePopoverProps {
6
+ children?: Snippet<[NotePopoverSlotProps]> | undefined
7
+ }
8
+ </script>
9
+
10
+ <script lang="ts">
11
+ /*
12
+ * The thread for the activated note, anchored to the note's overlay element. Activating a
13
+ * reply opens the whole thread from its root. Closing (Close, Escape, a press outside) clears
14
+ * `activated`.
15
+ *
16
+ * A new note has no text. While its root is empty (and unlocked) the default form writes the
17
+ * root's own `contents`, labelled "Comment", and the thread list stays hidden until there is
18
+ * something in it; after that the form adds replies. Closing a root that is still empty and
19
+ * has no replies removes it, so an abandoned note does not stay on the page.
20
+ */
21
+ import { isAbandonedNote, threadOf, threadRoot } from '@inkbar/core'
22
+ import { Popover } from 'bits-ui'
23
+ import { untrack } from 'svelte'
24
+
25
+ import { useInkContext } from '../context.js'
26
+ import { useAnnotations } from '../hooks/useAnnotations.svelte.js'
27
+
28
+ import { useAnnotationElement } from './annotation-element.svelte.js'
29
+ import { keepOpenOnAnnotation, useFocusReturn } from './popover-focus.js'
30
+ import ThreadForm from './ThreadForm.svelte'
31
+
32
+ import type { Annotation, NoteAnnotation } from '@inkbar/core'
33
+
34
+ let { children }: InkNotePopoverProps = $props()
35
+
36
+ const ctx = useInkContext('InkNotePopover')
37
+ const store = useAnnotations()
38
+
39
+ const note = $derived.by<NoteAnnotation | null>(() => {
40
+ const a = ctx.activated
41
+ ? store.annotations.find((x) => x.id === ctx.activated)
42
+ : undefined
43
+ return a?.type === 'note' ? threadRoot(store.annotations, a) : null
44
+ })
45
+ const anchorRef = useAnnotationElement(
46
+ () => ctx.rootEl,
47
+ () => note?.id ?? null
48
+ )
49
+ const anchor = $derived(anchorRef.current)
50
+ const focusReturn = useFocusReturn(ctx)
51
+ const open = $derived(note !== null && anchor !== null)
52
+ /** The root, then every reply below it, depth first, each level in creation order. */
53
+ const thread = $derived<Annotation[]>(note ? threadOf(store.annotations, note) : [])
54
+ /** True while the default form writes the root note's own text rather than a reply. */
55
+ const editingRoot = $derived(note !== null && !note.locked && !note.contents?.trim())
56
+
57
+ /**
58
+ * Notes this popover wrote text or a reply for. In controlled mode the write is a proposal
59
+ * the host may not have applied yet, so the applied list still shows the note empty; these
60
+ * are never dropped as abandoned. Bookkeeping only: nothing renders from it.
61
+ */
62
+ // eslint-disable-next-line svelte/prefer-svelte-reactivity
63
+ const written = new Set<string>()
64
+ /** Notes this popover has already asked to remove, so a slow host is asked once. */
65
+ // eslint-disable-next-line svelte/prefer-svelte-reactivity
66
+ const dropped = new Set<string>()
67
+
68
+ /** Removes an unlocked root note with no text and no replies. True when it did. */
69
+ function dropIfEmpty(id: string): boolean {
70
+ if (dropped.has(id) || written.has(id) || !isAbandonedNote(store.annotations, id))
71
+ return false
72
+ dropped.add(id)
73
+ store.remove(id)
74
+ return true
75
+ }
76
+
77
+ // Activation moved on from a note that never got any text: drop it.
78
+ let previousId: string | undefined
79
+ $effect(() => {
80
+ const id = note?.id
81
+ const previous = previousId
82
+ if (previous === id) return
83
+ previousId = id
84
+ if (previous !== undefined) untrack(() => dropIfEmpty(previous))
85
+ })
86
+
87
+ function reply(text: string): void {
88
+ const root = note
89
+ const contents = text.trim()
90
+ if (!root || !contents) return
91
+ written.add(root.id)
92
+ store.add({
93
+ type: 'note',
94
+ page: root.page,
95
+ rect: root.rect,
96
+ contents,
97
+ inReplyTo: root.id,
98
+ })
99
+ }
100
+ function isEmptyRoot(root: NoteAnnotation): boolean {
101
+ return (
102
+ !written.has(root.id) &&
103
+ !root.locked &&
104
+ !root.contents?.trim() &&
105
+ thread.length === 1
106
+ )
107
+ }
108
+ function close(): void {
109
+ const root = note
110
+ if (root && store.annotations.some((a) => a.id === root.id) && isEmptyRoot(root)) {
111
+ focusReturn.beforeRemove(anchor)
112
+ dropIfEmpty(root.id)
113
+ }
114
+ ctx.setActivated(null)
115
+ }
116
+ function updateContents(contents: string): void {
117
+ if (!note || note.locked) return
118
+ if (contents.trim()) written.add(note.id)
119
+ store.update(note.id, { contents })
120
+ }
121
+ /** Locked notes cannot be removed: removing one does nothing and keeps the popover. */
122
+ function removeThread(): void {
123
+ const root = note
124
+ if (!root || root.locked) return
125
+ focusReturn.beforeRemove(anchor)
126
+ // Under a slow controlled host the note stays listed for a while; it is not asked for again.
127
+ dropped.add(root.id)
128
+ store.remove(root.id)
129
+ ctx.setActivated(null)
130
+ }
131
+ function onOpenChange(next: boolean): void {
132
+ if (!next) close()
133
+ }
134
+ </script>
135
+
136
+ <Popover.Root bind:open={() => open, onOpenChange}>
137
+ <Popover.Portal>
138
+ {#if note && anchor}
139
+ <Popover.Content
140
+ class="ink-note-popover"
141
+ data-ink-note-popover=""
142
+ role="dialog"
143
+ side="right"
144
+ sideOffset={8}
145
+ collisionPadding={8}
146
+ aria-label="Note thread"
147
+ customAnchor={anchor}
148
+ onInteractOutside={keepOpenOnAnnotation}
149
+ onOpenAutoFocus={focusReturn.onOpenAutoFocus}
150
+ onCloseAutoFocus={(e) => focusReturn.onCloseAutoFocus(e, anchor)}
151
+ >
152
+ {#if children}
153
+ {@render children({
154
+ note,
155
+ replies: thread.slice(1),
156
+ reply,
157
+ update: updateContents,
158
+ remove: removeThread,
159
+ close,
160
+ })}
161
+ {:else}
162
+ {#if !editingRoot || thread.length > 1}
163
+ <ol class="ink-thread" data-ink-thread="">
164
+ {#each thread as item (item.id)}
165
+ <li
166
+ class="ink-thread-item"
167
+ data-ink-thread-item=""
168
+ data-reply={item.inReplyTo ? '' : undefined}
169
+ >
170
+ <span class="ink-thread-author">{item.author ?? 'Anonymous'}</span>
171
+ <time class="ink-thread-time" datetime={item.createdAt}>
172
+ {new Date(item.createdAt).toLocaleString()}
173
+ </time>
174
+ <p class="ink-thread-text">{item.contents}</p>
175
+ </li>
176
+ {/each}
177
+ </ol>
178
+ {/if}
179
+ <!-- Keyed by note: moving to another note starts with an empty draft at once. -->
180
+ {#key note.id}
181
+ <ThreadForm
182
+ {editingRoot}
183
+ locked={!!note.locked}
184
+ onSubmit={(text) => (editingRoot ? updateContents(text) : reply(text))}
185
+ onRemove={removeThread}
186
+ onClose={close}
187
+ />
188
+ {/key}
189
+ {/if}
190
+ </Popover.Content>
191
+ {/if}
192
+ </Popover.Portal>
193
+ </Popover.Root>
@@ -0,0 +1,9 @@
1
+ import type { NotePopoverSlotProps } from '@inkbar/core/dom';
2
+ import type { Snippet } from 'svelte';
3
+ export interface InkNotePopoverProps {
4
+ children?: Snippet<[NotePopoverSlotProps]> | undefined;
5
+ }
6
+ declare const InkNotePopover: import("svelte").Component<InkNotePopoverProps, {}, "">;
7
+ type InkNotePopover = ReturnType<typeof InkNotePopover>;
8
+ export default InkNotePopover;
9
+ //# sourceMappingURL=InkNotePopover.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"InkNotePopover.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/InkNotePopover.svelte.ts"],"names":[],"mappings":"AAGE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAA;AAC5D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAA;AAErC,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,EAAE,OAAO,CAAC,CAAC,oBAAoB,CAAC,CAAC,GAAG,SAAS,CAAA;CACvD;AAwLH,QAAA,MAAM,cAAc,yDAAwC,CAAC;AAC7D,KAAK,cAAc,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC;AACxD,eAAe,cAAc,CAAC"}