@inkbar/svelte 0.9.0 → 0.11.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 (59) hide show
  1. package/README.md +64 -3
  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/InkPanel.svelte +73 -0
  8. package/dist/components/InkPanel.svelte.d.ts +18 -0
  9. package/dist/components/InkPanel.svelte.d.ts.map +1 -0
  10. package/dist/components/InkPrintButton.svelte +57 -0
  11. package/dist/components/InkPrintButton.svelte.d.ts +12 -0
  12. package/dist/components/InkPrintButton.svelte.d.ts.map +1 -0
  13. package/dist/components/InkRecordList.svelte +91 -0
  14. package/dist/components/InkRecordList.svelte.d.ts +72 -0
  15. package/dist/components/InkRecordList.svelte.d.ts.map +1 -0
  16. package/dist/components/InkRoot.svelte +4 -0
  17. package/dist/components/InkRoot.svelte.d.ts.map +1 -1
  18. package/dist/components/InkSearchResults.svelte +72 -0
  19. package/dist/components/InkSearchResults.svelte.d.ts +9 -0
  20. package/dist/components/InkSearchResults.svelte.d.ts.map +1 -0
  21. package/dist/components/InkSidebar.svelte +179 -0
  22. package/dist/components/InkSidebar.svelte.d.ts +22 -0
  23. package/dist/components/InkSidebar.svelte.d.ts.map +1 -0
  24. package/dist/components/InkThumbnail.svelte +74 -0
  25. package/dist/components/InkThumbnail.svelte.d.ts +19 -0
  26. package/dist/components/InkThumbnail.svelte.d.ts.map +1 -0
  27. package/dist/components/InkThumbnails.svelte +146 -0
  28. package/dist/components/InkThumbnails.svelte.d.ts +24 -0
  29. package/dist/components/InkThumbnails.svelte.d.ts.map +1 -0
  30. package/dist/components/InkViewport.svelte +21 -4
  31. package/dist/components/InkViewport.svelte.d.ts +2 -0
  32. package/dist/components/InkViewport.svelte.d.ts.map +1 -1
  33. package/dist/components/InkZoomSelect.svelte +1 -1
  34. package/dist/components/RecordCards.svelte +173 -0
  35. package/dist/components/RecordCards.svelte.d.ts +16 -0
  36. package/dist/components/RecordCards.svelte.d.ts.map +1 -0
  37. package/dist/components/SidebarTrigger.svelte +20 -0
  38. package/dist/components/SidebarTrigger.svelte.d.ts +9 -0
  39. package/dist/components/SidebarTrigger.svelte.d.ts.map +1 -0
  40. package/dist/components/sidebar-context.d.ts +24 -0
  41. package/dist/components/sidebar-context.d.ts.map +1 -0
  42. package/dist/components/sidebar-context.js +13 -0
  43. package/dist/hooks/useAnnotations.svelte.d.ts +78 -0
  44. package/dist/hooks/useAnnotations.svelte.d.ts.map +1 -1
  45. package/dist/hooks/useRecordSelection.svelte.d.ts +11 -0
  46. package/dist/hooks/useRecordSelection.svelte.d.ts.map +1 -0
  47. package/dist/hooks/useRecordSelection.svelte.js +19 -0
  48. package/dist/hooks/useRecords.svelte.d.ts +14 -0
  49. package/dist/hooks/useRecords.svelte.d.ts.map +1 -0
  50. package/dist/hooks/useRecords.svelte.js +24 -0
  51. package/dist/hooks/useSearch.svelte.d.ts.map +1 -1
  52. package/dist/hooks/useSearch.svelte.js +1 -0
  53. package/dist/index.d.ts +18 -0
  54. package/dist/index.d.ts.map +1 -1
  55. package/dist/index.js +10 -0
  56. package/dist/to-readable.svelte.d.ts +8 -0
  57. package/dist/to-readable.svelte.d.ts.map +1 -0
  58. package/dist/to-readable.svelte.js +15 -0
  59. package/package.json +5 -5
@@ -0,0 +1,72 @@
1
+ <script module lang="ts">
2
+ import type { HTMLAttributes } from 'svelte/elements'
3
+
4
+ export interface InkSearchResultsProps extends Omit<
5
+ HTMLAttributes<HTMLDivElement>,
6
+ 'children'
7
+ > {
8
+ /** Show the query field. Default true; turn it off when a find bar is on the page. */
9
+ field?: boolean | undefined
10
+ }
11
+ </script>
12
+
13
+ <script lang="ts">
14
+ /*
15
+ * The search hits as a list of cards, for a sidebar panel: `createSearchResults` on core's
16
+ * record list controller over the root's search. One card per hit, with its page and the text
17
+ * around it. Choosing a card makes its hit current, as the find bar's next and previous do, and
18
+ * the current hit is the selected card. Above the cards, a query field (`field={false}`
19
+ * leaves it out when a find bar is on the page); under them, the find bar's match count.
20
+ * Field and find bar share the search state, so they never disagree. It fills its parent's
21
+ * height. Inside `InkRoot`; it renders empty on the server.
22
+ */
23
+ import {
24
+ findCountText,
25
+ handleResultsFieldKey,
26
+ isBlankQuery,
27
+ searchSource,
28
+ } from '@inkbar/core/dom'
29
+ import { Search } from '@inkbar/icons'
30
+
31
+ import { useInkContext } from '../context.js'
32
+ import { useSearch } from '../hooks/useSearch.svelte.js'
33
+
34
+ import InkIcon from './InkIcon.svelte'
35
+ import RecordCards from './RecordCards.svelte'
36
+
37
+ let { field = true, class: className, ...rest }: InkSearchResultsProps = $props()
38
+
39
+ const ctx = useInkContext('InkSearchResults')
40
+ const search = useSearch()
41
+ const source = $derived(ctx.search ? searchSource(ctx.search) : null)
42
+ // Nothing found yet is the count's to say while a query is in: "No results", or a scan.
43
+ const blank = $derived(isBlankQuery(search.state.query))
44
+ </script>
45
+
46
+ <div {...rest} class={['ink-search-results', className]} data-ink-search-results="">
47
+ {#if field}
48
+ <div class="ink-search-results-bar" role="search">
49
+ <!-- The field is the search state's: a query typed in the find bar shows here. -->
50
+ <div class="ink-input-group">
51
+ <span class="ink-input-accessory"><InkIcon icon={Search} /></span>
52
+ <input
53
+ type="text"
54
+ aria-label="Find in document"
55
+ placeholder="Search for a word or phrase"
56
+ autocomplete="off"
57
+ spellcheck={false}
58
+ value={search.state.query}
59
+ oninput={(e) => search.setQuery(e.currentTarget.value)}
60
+ onkeydown={(e) => handleResultsFieldKey(e, search)}
61
+ />
62
+ </div>
63
+ </div>
64
+ {/if}
65
+ <RecordCards {source} emptyShown={blank}>
66
+ {#snippet empty()}Search to list every match.{/snippet}
67
+ </RecordCards>
68
+ <!-- A footer under the cards, hidden while empty. -->
69
+ <div class="ink-search-results-count" data-ink-search-results-count="">
70
+ {findCountText(search.state)}
71
+ </div>
72
+ </div>
@@ -0,0 +1,9 @@
1
+ import type { HTMLAttributes } from 'svelte/elements';
2
+ export interface InkSearchResultsProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children'> {
3
+ /** Show the query field. Default true; turn it off when a find bar is on the page. */
4
+ field?: boolean | undefined;
5
+ }
6
+ declare const InkSearchResults: import("svelte").Component<InkSearchResultsProps, {}, "">;
7
+ type InkSearchResults = ReturnType<typeof InkSearchResults>;
8
+ export default InkSearchResults;
9
+ //# sourceMappingURL=InkSearchResults.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"InkSearchResults.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/InkSearchResults.svelte.ts"],"names":[],"mappings":"AAGE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAErD,MAAM,WAAW,qBAAsB,SAAQ,IAAI,CACjD,cAAc,CAAC,cAAc,CAAC,EAC9B,UAAU,CACX;IACC,sFAAsF;IACtF,KAAK,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;CAC5B;AAqEH,QAAA,MAAM,gBAAgB,2DAAwC,CAAC;AAC/D,KAAK,gBAAgB,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAC5D,eAAe,gBAAgB,CAAC"}
@@ -0,0 +1,179 @@
1
+ <script module lang="ts">
2
+ import type { SidebarLayout, SidebarOpen, SidebarSide } from '@inkbar/core/dom'
3
+ import type { Snippet } from 'svelte'
4
+ import type { HTMLAttributes } from 'svelte/elements'
5
+
6
+ export interface InkSidebarProps extends Omit<
7
+ HTMLAttributes<HTMLDivElement>,
8
+ 'children'
9
+ > {
10
+ /** Which edge the rail and border sit on. Default `'left'`. */
11
+ side?: SidebarSide | undefined
12
+ /** `'rail'` (default), `'tabs'` or `'stack'`. */
13
+ layout?: SidebarLayout | undefined
14
+ /** Default true. False hides the whole sidebar and keeps its open panel, content and scroll for when it shows again. */
15
+ visible?: boolean | undefined
16
+ /**
17
+ * Bindable: the open panel's id (or null) for rail and tabs, the expanded ids for stack.
18
+ * Left out, the first panel starts open.
19
+ */
20
+ open?: SidebarOpen | undefined
21
+ /** The `InkPanel`s. */
22
+ children?: Snippet | undefined
23
+ }
24
+ </script>
25
+
26
+ <script lang="ts">
27
+ /*
28
+ * A sidebar of `InkPanel`s: `createSidebar`'s markup on core's sidebar controller, which holds
29
+ * the open state, the ARIA, the keys and the clicks. Rail and tabs render their triggers here;
30
+ * in a stack each panel renders its own header. Panels register as they initialise and take
31
+ * their place in DOM order once mounted, so a panel added later with `{#if}` slots in where
32
+ * it stands. A closed panel stays mounted and hidden, and the panels' content stays in place
33
+ * across layouts, so its state survives switching.
34
+ *
35
+ * `bind:open` is the open panel's id (or null) for rail and tabs, and the expanded ids for
36
+ * stack. Left out, the first panel starts open. It needs no document, so it works outside
37
+ * `InkRoot`. The server renders the root, the panels (the open one shown) and the stack
38
+ * headers; rail and tab buttons follow once mounted, as the panels register after the root
39
+ * has rendered.
40
+ */
41
+ import { createSidebarController } from '@inkbar/core/dom'
42
+ import { untrack } from 'svelte'
43
+
44
+ import { useReadable } from '../use-readable.svelte.js'
45
+
46
+ import { setSidebarContext, type SidebarPanelEntry } from './sidebar-context.js'
47
+ import SidebarTrigger from './SidebarTrigger.svelte'
48
+
49
+ let {
50
+ side = 'left',
51
+ layout = 'rail',
52
+ visible = true,
53
+ open = $bindable(),
54
+ class: className,
55
+ children,
56
+ ...rest
57
+ }: InkSidebarProps = $props()
58
+
59
+ // The same ids on the server and the client.
60
+ const idPrefix = $props.id()
61
+
62
+ let entries = $state.raw<SidebarPanelEntry[]>([])
63
+ const infos = () => entries.map((e) => e.info())
64
+
65
+ // Pending until mounted: the panels register one by one as they initialise, and an open id
66
+ // not registered yet is not a removed panel.
67
+ const controller = untrack(() =>
68
+ createSidebarController({
69
+ layout,
70
+ side,
71
+ visible,
72
+ panels: [],
73
+ ...(open !== undefined && { open }),
74
+ pending: true,
75
+ onOpenChange: (value) => {
76
+ open = value
77
+ },
78
+ idPrefix,
79
+ })
80
+ )
81
+ const current = useReadable(() => controller.state, controller.state.get())
82
+ // Destroyed before the panels go: this pre-effect comes before the template's, so its
83
+ // teardown runs first. Unregistering into a live controller would take each panel for a
84
+ // removed one and write the fallback to the bound `open`.
85
+ $effect.pre(() => () => controller.destroy())
86
+
87
+ // Before the DOM updates, so the panels render the attributes these give.
88
+ $effect.pre(() => {
89
+ const next = { layout, side, visible, open }
90
+ untrack(() =>
91
+ controller.update({
92
+ layout: next.layout,
93
+ side: next.side,
94
+ visible: next.visible,
95
+ ...(next.open !== undefined && { open: next.open }),
96
+ })
97
+ )
98
+ })
99
+ // A panel's label, icon or badge changing.
100
+ $effect.pre(() => {
101
+ const panels = infos()
102
+ untrack(() => controller.update({ panels }))
103
+ })
104
+
105
+ // Panels register while they initialise, inside the parent's render: untracked, so the
106
+ // writes are not taken for state changed by a template expression. On the server too.
107
+ function register(entry: SidebarPanelEntry): () => void {
108
+ untrack(() => {
109
+ entries = [...entries, entry]
110
+ controller.update({ panels: infos() })
111
+ })
112
+ return () =>
113
+ untrack(() => {
114
+ entries = entries.filter((e) => e !== entry)
115
+ controller.update({ panels: infos() })
116
+ })
117
+ }
118
+
119
+ function arrange(): void {
120
+ untrack(() => {
121
+ const list = entries
122
+ const sorted = [...list].sort((a, b) => {
123
+ const x = a.element()
124
+ const y = b.element()
125
+ if (!x || !y || x === y) return 0
126
+ return x.compareDocumentPosition(y) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1
127
+ })
128
+ if (sorted.some((e, i) => e !== list[i])) {
129
+ entries = sorted
130
+ controller.update({ panels: infos() })
131
+ }
132
+ })
133
+ }
134
+
135
+ setSidebarContext({ controller, state: current, register, arrange })
136
+
137
+ const view = $derived.by(() => {
138
+ const s = current.current
139
+ return {
140
+ layout: s.layout,
141
+ root: controller.rootAttrs(),
142
+ tablist: controller.tablistAttrs(),
143
+ triggers: s.panels.map((panel) => ({
144
+ panel,
145
+ attrs: controller.triggerAttrs(panel.id),
146
+ })),
147
+ }
148
+ })
149
+
150
+ let root = $state<HTMLDivElement | null>(null)
151
+ // The controller handles the triggers' clicks and keys, stack headers included. The panels
152
+ // have registered by now.
153
+ $effect(() => {
154
+ const el = root
155
+ if (!el) return
156
+ const unbind = untrack(() => controller.bindTriggers(el))
157
+ untrack(() => controller.update({ pending: false }))
158
+ return unbind
159
+ })
160
+ </script>
161
+
162
+ <div {...rest} bind:this={root} class={['ink-sidebar', className]} {...view.root}>
163
+ {#if view.layout !== 'stack'}
164
+ <div
165
+ class={view.layout === 'rail' ? 'ink-sidebar-rail' : 'ink-sidebar-tabs'}
166
+ {...view.tablist}
167
+ >
168
+ {#each view.triggers as t (t.panel.id)}
169
+ <button {...t.attrs}>
170
+ <SidebarTrigger panel={t.panel} rail={view.layout === 'rail'} />
171
+ </button>
172
+ {/each}
173
+ </div>
174
+ {/if}
175
+ <!-- One element in every layout, so the panels are never remounted. -->
176
+ <div class={{ 'ink-sidebar-panels': view.layout !== 'stack' }}>
177
+ {@render children?.()}
178
+ </div>
179
+ </div>
@@ -0,0 +1,22 @@
1
+ import type { SidebarLayout, SidebarOpen, SidebarSide } from '@inkbar/core/dom';
2
+ import type { Snippet } from 'svelte';
3
+ import type { HTMLAttributes } from 'svelte/elements';
4
+ export interface InkSidebarProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children'> {
5
+ /** Which edge the rail and border sit on. Default `'left'`. */
6
+ side?: SidebarSide | undefined;
7
+ /** `'rail'` (default), `'tabs'` or `'stack'`. */
8
+ layout?: SidebarLayout | undefined;
9
+ /** Default true. False hides the whole sidebar and keeps its open panel, content and scroll for when it shows again. */
10
+ visible?: boolean | undefined;
11
+ /**
12
+ * Bindable: the open panel's id (or null) for rail and tabs, the expanded ids for stack.
13
+ * Left out, the first panel starts open.
14
+ */
15
+ open?: SidebarOpen | undefined;
16
+ /** The `InkPanel`s. */
17
+ children?: Snippet | undefined;
18
+ }
19
+ declare const InkSidebar: import("svelte").Component<InkSidebarProps, {}, "open">;
20
+ type InkSidebar = ReturnType<typeof InkSidebar>;
21
+ export default InkSidebar;
22
+ //# sourceMappingURL=InkSidebar.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"InkSidebar.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/InkSidebar.svelte.ts"],"names":[],"mappings":"AAGE,OAAO,KAAK,EAAE,aAAa,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAC/E,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,+DAA+D;IAC/D,IAAI,CAAC,EAAE,WAAW,GAAG,SAAS,CAAA;IAC9B,iDAAiD;IACjD,MAAM,CAAC,EAAE,aAAa,GAAG,SAAS,CAAA;IAClC,wHAAwH;IACxH,OAAO,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;IAC7B;;;OAGG;IACH,IAAI,CAAC,EAAE,WAAW,GAAG,SAAS,CAAA;IAC9B,uBAAuB;IACvB,QAAQ,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;CAC/B;AAwKH,QAAA,MAAM,UAAU,yDAAwC,CAAC;AACzD,KAAK,UAAU,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;AAChD,eAAe,UAAU,CAAC"}
@@ -0,0 +1,74 @@
1
+ <script module lang="ts">
2
+ import type { HTMLAttributes } from 'svelte/elements'
3
+
4
+ export interface InkThumbnailProps extends Omit<
5
+ HTMLAttributes<HTMLDivElement>,
6
+ 'children'
7
+ > {
8
+ /** 0-based. */
9
+ page: number
10
+ /** Width in CSS pixels; the height follows the page. Default 120 unless `height` is given. */
11
+ width?: number | undefined
12
+ /** Height in CSS pixels; the width follows the page. */
13
+ height?: number | undefined
14
+ /** Draw the page's annotations. Default true. */
15
+ annotations?: boolean | undefined
16
+ /** Draw plugin marks. Default true. */
17
+ marks?: boolean | undefined
18
+ /** The accessible name. Default "Page N". */
19
+ label?: string | undefined
20
+ }
21
+ </script>
22
+
23
+ <script lang="ts">
24
+ /*
25
+ * One page as a static image, with its live annotations and plugin marks: core's thumbnail
26
+ * painter on this element, which sizes it, names it ("Page N" unless `label` says otherwise)
27
+ * and draws it once it nears the visible area. It is not focusable and does nothing on click:
28
+ * wrap it in the control you need. Inside `InkRoot`; it renders empty on the server.
29
+ */
30
+ import { createThumbnailPainter, type ThumbnailPainter } from '@inkbar/core/dom'
31
+ import { untrack } from 'svelte'
32
+
33
+ import { useInkContext } from '../context.js'
34
+ import { toReadable } from '../to-readable.svelte.js'
35
+
36
+ let {
37
+ page,
38
+ width,
39
+ height,
40
+ annotations = true,
41
+ marks = true,
42
+ label,
43
+ class: className,
44
+ ...rest
45
+ }: InkThumbnailProps = $props()
46
+
47
+ const ctx = useInkContext('InkThumbnail')
48
+ const deps = {
49
+ document: toReadable(() => ctx.document),
50
+ pluginHost: toReadable(() => ctx.pluginHost),
51
+ }
52
+ const options = () => ({ page, width, height, annotations, marks, label })
53
+
54
+ let el = $state<HTMLDivElement | null>(null)
55
+ let painter: ThumbnailPainter | null = null
56
+ // Before the painter is made, so a new painter does not take its first options twice.
57
+ $effect(() => {
58
+ const next = options()
59
+ untrack(() => painter?.update(next))
60
+ })
61
+ $effect(() => {
62
+ const target = el
63
+ if (!target) return
64
+ const p = untrack(() => createThumbnailPainter(target, deps, options()))
65
+ painter = p
66
+ return () => {
67
+ p.destroy()
68
+ painter = null
69
+ }
70
+ })
71
+ </script>
72
+
73
+ <!-- The class is the painter's too: kept here, a re-render never drops it. -->
74
+ <div {...rest} bind:this={el} class={['ink-thumbnail', className]}></div>
@@ -0,0 +1,19 @@
1
+ import type { HTMLAttributes } from 'svelte/elements';
2
+ export interface InkThumbnailProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children'> {
3
+ /** 0-based. */
4
+ page: number;
5
+ /** Width in CSS pixels; the height follows the page. Default 120 unless `height` is given. */
6
+ width?: number | undefined;
7
+ /** Height in CSS pixels; the width follows the page. */
8
+ height?: number | undefined;
9
+ /** Draw the page's annotations. Default true. */
10
+ annotations?: boolean | undefined;
11
+ /** Draw plugin marks. Default true. */
12
+ marks?: boolean | undefined;
13
+ /** The accessible name. Default "Page N". */
14
+ label?: string | undefined;
15
+ }
16
+ declare const InkThumbnail: import("svelte").Component<InkThumbnailProps, {}, "">;
17
+ type InkThumbnail = ReturnType<typeof InkThumbnail>;
18
+ export default InkThumbnail;
19
+ //# sourceMappingURL=InkThumbnail.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"InkThumbnail.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/InkThumbnail.svelte.ts"],"names":[],"mappings":"AAGE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAErD,MAAM,WAAW,iBAAkB,SAAQ,IAAI,CAC7C,cAAc,CAAC,cAAc,CAAC,EAC9B,UAAU,CACX;IACC,eAAe;IACf,IAAI,EAAE,MAAM,CAAA;IACZ,8FAA8F;IAC9F,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAC1B,wDAAwD;IACxD,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAC3B,iDAAiD;IACjD,WAAW,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;IACjC,uCAAuC;IACvC,KAAK,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;IAC3B,6CAA6C;IAC7C,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAC3B;AAmEH,QAAA,MAAM,YAAY,uDAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
@@ -0,0 +1,146 @@
1
+ <script module lang="ts">
2
+ import type { Snippet } from 'svelte'
3
+ import type { HTMLAttributes } from 'svelte/elements'
4
+
5
+ export interface InkThumbnailsProps extends Omit<
6
+ HTMLAttributes<HTMLDivElement>,
7
+ 'children'
8
+ > {
9
+ /** 0-based pages to list, in order. Default: every page. */
10
+ pages?: readonly number[] | undefined
11
+ /** Thumbnail width in CSS pixels. Default: the list's width less a 16px gutter each side. */
12
+ width?: number | undefined
13
+ /** Draw each page's annotations. Default true. */
14
+ annotations?: boolean | undefined
15
+ /** Draw plugin marks. Default true. */
16
+ marks?: boolean | undefined
17
+ /**
18
+ * Renders inside each item, after its page number. The item's height is fixed from the
19
+ * page's, so keep what you add out of the flow (position it absolutely) or small.
20
+ */
21
+ item?: Snippet<[{ page: number; current: boolean }]> | undefined
22
+ }
23
+ </script>
24
+
25
+ <script lang="ts">
26
+ /*
27
+ * The pages as a list of thumbnails: `createThumbnails`' markup on core's thumbnails
28
+ * controller, which holds the current page, the keys, the clicks, the Tab stop and the window.
29
+ * Only the items near the visible part are mounted, each placed at its offset over a spacer of
30
+ * the full height, and each holds an `InkThumbnail`. An item is a button that scrolls the
31
+ * viewport to its page; the current page's has `aria-current="page"`. Inside `InkRoot`; it
32
+ * renders empty on the server.
33
+ */
34
+ import {
35
+ createThumbnailsController,
36
+ THUMBNAIL_WIDTH,
37
+ type ThumbnailsController,
38
+ type ThumbnailsState,
39
+ } from '@inkbar/core/dom'
40
+ import { untrack } from 'svelte'
41
+
42
+ import { useInkContext } from '../context.js'
43
+ import { toReadable } from '../to-readable.svelte.js'
44
+
45
+ import InkThumbnail from './InkThumbnail.svelte'
46
+
47
+ let {
48
+ pages,
49
+ width,
50
+ annotations = true,
51
+ marks = true,
52
+ item,
53
+ class: className,
54
+ ...rest
55
+ }: InkThumbnailsProps = $props()
56
+
57
+ const ctx = useInkContext('InkThumbnails')
58
+ const deps = {
59
+ document: toReadable(() => ctx.document),
60
+ pluginHost: toReadable(() => ctx.pluginHost),
61
+ viewport: toReadable(() => ctx.viewport),
62
+ }
63
+ const options = () => ({ pages, width, annotations, marks })
64
+
65
+ let scroller = $state<HTMLDivElement | null>(null)
66
+ let controller = $state.raw<ThumbnailsController | null>(null)
67
+ /** Until the list mounts, and on the server: nothing listed. */
68
+ let listed = $state.raw<ThumbnailsState>({
69
+ pages: [],
70
+ current: -1,
71
+ window: { start: 0, end: 0 },
72
+ offsets: [0],
73
+ width: THUMBNAIL_WIDTH,
74
+ })
75
+
76
+ // A list unchanged in content publishes nothing, so an inline `pages` array cannot loop.
77
+ $effect(() => {
78
+ const c = controller
79
+ const next = options()
80
+ if (c) untrack(() => c.update(next))
81
+ })
82
+ $effect(() => {
83
+ const el = scroller
84
+ if (!el) return
85
+ return untrack(() => {
86
+ const c = createThumbnailsController(deps, options())
87
+ controller = c
88
+ listed = c.state.get()
89
+ const stop = c.state.subscribe((s) => {
90
+ listed = s
91
+ })
92
+ c.attach(el)
93
+ return () => {
94
+ stop()
95
+ c.destroy()
96
+ controller = null
97
+ }
98
+ })
99
+ })
100
+
101
+ const items = $derived.by(() => {
102
+ const { pages: list, current, window, offsets } = listed
103
+ return list.slice(window.start, window.end).map((page, k) => {
104
+ const top = offsets[window.start + k]!
105
+ const bottom = offsets[window.start + k + 1]!
106
+ // Only a mounted controller has a window, so `controller` is set here.
107
+ const attrs = controller?.itemAttrs(window.start + k) ?? {}
108
+ return { page, current: page === current, top, height: bottom - top, attrs }
109
+ })
110
+ })
111
+ </script>
112
+
113
+ <div
114
+ {...rest}
115
+ bind:this={scroller}
116
+ class={['ink-thumbnails', className]}
117
+ data-ink-thumbnails=""
118
+ role="list"
119
+ >
120
+ <div
121
+ class="ink-thumbnails-spacer"
122
+ aria-hidden="true"
123
+ style:height="{listed.offsets.at(-1) ?? 0}px"
124
+ ></div>
125
+ {#each items as entry (entry.page)}
126
+ <div
127
+ class="ink-thumbnails-entry"
128
+ {...entry.attrs}
129
+ style:top="{entry.top}px"
130
+ style:height="{entry.height}px"
131
+ >
132
+ <!-- The controller sets the tabindex. The thumbnail names the button "Page N"; the
133
+ visible number repeats it. -->
134
+ <button
135
+ type="button"
136
+ class="ink-thumbnails-item"
137
+ data-ink-thumbnail-item={entry.page}
138
+ aria-current={entry.current ? 'page' : undefined}
139
+ >
140
+ <InkThumbnail page={entry.page} width={listed.width} {annotations} {marks} />
141
+ <span class="ink-thumbnails-label" aria-hidden="true">{entry.page + 1}</span>
142
+ {@render item?.({ page: entry.page, current: entry.current })}
143
+ </button>
144
+ </div>
145
+ {/each}
146
+ </div>
@@ -0,0 +1,24 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ export interface InkThumbnailsProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children'> {
4
+ /** 0-based pages to list, in order. Default: every page. */
5
+ pages?: readonly number[] | undefined;
6
+ /** Thumbnail width in CSS pixels. Default: the list's width less a 16px gutter each side. */
7
+ width?: number | undefined;
8
+ /** Draw each page's annotations. Default true. */
9
+ annotations?: boolean | undefined;
10
+ /** Draw plugin marks. Default true. */
11
+ marks?: boolean | undefined;
12
+ /**
13
+ * Renders inside each item, after its page number. The item's height is fixed from the
14
+ * page's, so keep what you add out of the flow (position it absolutely) or small.
15
+ */
16
+ item?: Snippet<[{
17
+ page: number;
18
+ current: boolean;
19
+ }]> | undefined;
20
+ }
21
+ declare const InkThumbnails: import("svelte").Component<InkThumbnailsProps, {}, "">;
22
+ type InkThumbnails = ReturnType<typeof InkThumbnails>;
23
+ export default InkThumbnails;
24
+ //# sourceMappingURL=InkThumbnails.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"InkThumbnails.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/InkThumbnails.svelte.ts"],"names":[],"mappings":"AAGE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAA;AACrC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAErD,MAAM,WAAW,kBAAmB,SAAQ,IAAI,CAC9C,cAAc,CAAC,cAAc,CAAC,EAC9B,UAAU,CACX;IACC,4DAA4D;IAC5D,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,CAAA;IACrC,6FAA6F;IAC7F,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAC1B,kDAAkD;IAClD,WAAW,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;IACjC,uCAAuC;IACvC,KAAK,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;IAC3B;;;OAGG;IACH,IAAI,CAAC,EAAE,OAAO,CAAC,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC,GAAG,SAAS,CAAA;CACjE;AAsHH,QAAA,MAAM,aAAa,wDAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
@@ -14,6 +14,8 @@
14
14
  gap?: number
15
15
  /** Pages kept mounted either side of the visible ones. */
16
16
  buffer?: number
17
+ /** Pinch to zoom the pages. Off leaves the browser's own touch handling alone. */
18
+ pinchZoom?: boolean
17
19
  loading?: Snippet | undefined
18
20
  empty?: Snippet | undefined
19
21
  error?: Snippet<[{ error: InkError }]> | undefined
@@ -63,6 +65,7 @@
63
65
  rotation = 0,
64
66
  gap = 16,
65
67
  buffer = 2,
68
+ pinchZoom = true,
66
69
  loading,
67
70
  empty,
68
71
  error,
@@ -100,7 +103,12 @@
100
103
  */
101
104
  let build = 0
102
105
  /** The construction options the engine was last built with, so a rebuild runs only on a change. */
103
- let builtWith: { gap: number; buffer: number; custom: boolean } | null = null
106
+ let builtWith: {
107
+ gap: number
108
+ buffer: number
109
+ pinchZoom: boolean
110
+ custom: boolean
111
+ } | null = null
104
112
  let stops: (() => void)[] = []
105
113
  /** The zoom and page last reported to the host. */
106
114
  let lastReportedZoom: ZoomValue | undefined
@@ -135,7 +143,7 @@
135
143
  let first = true
136
144
  build += 1
137
145
  const custom = !!annotation
138
- builtWith = { gap, buffer, custom }
146
+ builtWith = { gap, buffer, pinchZoom, custom }
139
147
  // The tools this viewer registers, from the same inputs; a later `tools` waits for a rebuild.
140
148
  ctx.setOpenedToolIds(toolIdsOf(ctx.hostTools, ctx.pluginHost))
141
149
  engine = mountViewer({
@@ -147,6 +155,7 @@
147
155
  rotation,
148
156
  gap,
149
157
  buffer,
158
+ pinchZoom,
150
159
  tool: ctx.tool,
151
160
  ...(ctx.hostTools ? { tools: ctx.hostTools } : {}),
152
161
  // The engine adds the plugins' tools and layers, and decorates through the host, which
@@ -253,7 +262,7 @@
253
262
 
254
263
  // Construction-only options rebuild the engine for the same document, keeping zoom and page.
255
264
  // The deriveds notify only on a real change: a host that spreads its props invalidates `gap`
256
- // and `buffer` without changing them. One effect for all three, so a gap and a snippet change
265
+ // and `buffer` without changing them. One effect for all of them, so a gap and a snippet change
257
266
  // in the same tick rebuild once. It skips its first run (and any run after a build that
258
267
  // already used these values) by comparing with `builtWith`.
259
268
  //
@@ -264,15 +273,22 @@
264
273
  // `pages` for the new build's pages. The rebuild keeps the scroll position.
265
274
  const buildGap = $derived(gap)
266
275
  const buildBuffer = $derived(buffer)
276
+ const buildPinchZoom = $derived(pinchZoom)
267
277
  const buildCustom = $derived(!!annotation)
268
278
  $effect(() => {
269
- const next = { gap: buildGap, buffer: buildBuffer, custom: buildCustom }
279
+ const next = {
280
+ gap: buildGap,
281
+ buffer: buildBuffer,
282
+ pinchZoom: buildPinchZoom,
283
+ custom: buildCustom,
284
+ }
270
285
  untrack(() => {
271
286
  const built = builtWith
272
287
  if (!engine || !built) return
273
288
  if (
274
289
  built.gap === next.gap &&
275
290
  built.buffer === next.buffer &&
291
+ built.pinchZoom === next.pinchZoom &&
276
292
  built.custom === next.custom
277
293
  )
278
294
  return
@@ -281,6 +297,7 @@
281
297
  engine.rebuild({
282
298
  gap: next.gap,
283
299
  buffer: next.buffer,
300
+ pinchZoom: next.pinchZoom,
284
301
  customAnnotations: next.custom,
285
302
  })
286
303
  })
@@ -9,6 +9,8 @@ export interface InkViewportProps extends Omit<HTMLAttributes<HTMLDivElement>, '
9
9
  gap?: number;
10
10
  /** Pages kept mounted either side of the visible ones. */
11
11
  buffer?: number;
12
+ /** Pinch to zoom the pages. Off leaves the browser's own touch handling alone. */
13
+ pinchZoom?: boolean;
12
14
  loading?: Snippet | undefined;
13
15
  empty?: Snippet | undefined;
14
16
  error?: Snippet<[{
@@ -1 +1 @@
1
- {"version":3,"file":"InkViewport.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/InkViewport.svelte.ts"],"names":[],"mappings":"AAGE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAA;AAC5C,OAAO,KAAK,EAAE,mBAAmB,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAA;AACrF,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAA;AACrC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAErD,MAAM,WAAW,gBAAiB,SAAQ,IAAI,CAC5C,cAAc,CAAC,cAAc,CAAC,EAC9B,UAAU,CACX;IACC,IAAI,CAAC,EAAE,UAAU,CAAA;IACjB,QAAQ,CAAC,EAAE,YAAY,CAAA;IACvB,iCAAiC;IACjC,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,0DAA0D;IAC1D,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,OAAO,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;IAC7B,KAAK,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;IAC3B,KAAK,CAAC,EAAE,OAAO,CAAC,CAAC;QAAE,KAAK,EAAE,QAAQ,CAAA;KAAE,CAAC,CAAC,GAAG,SAAS,CAAA;IAClD,UAAU,CAAC,EAAE,OAAO,CAAC,CAAC,mBAAmB,CAAC,CAAC,GAAG,SAAS,CAAA;IACvD,wFAAwF;IACxF,SAAS,CAAC,EAAE,OAAO,CAAC,CAAC;QAAE,KAAK,EAAE,QAAQ,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC,GAAG,SAAS,CAAA;CACrE;AA2WH,QAAA,MAAM,WAAW,sDAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}
1
+ {"version":3,"file":"InkViewport.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/InkViewport.svelte.ts"],"names":[],"mappings":"AAGE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAA;AAC5C,OAAO,KAAK,EAAE,mBAAmB,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAA;AACrF,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAA;AACrC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAErD,MAAM,WAAW,gBAAiB,SAAQ,IAAI,CAC5C,cAAc,CAAC,cAAc,CAAC,EAC9B,UAAU,CACX;IACC,IAAI,CAAC,EAAE,UAAU,CAAA;IACjB,QAAQ,CAAC,EAAE,YAAY,CAAA;IACvB,iCAAiC;IACjC,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,0DAA0D;IAC1D,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,kFAAkF;IAClF,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,OAAO,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;IAC7B,KAAK,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;IAC3B,KAAK,CAAC,EAAE,OAAO,CAAC,CAAC;QAAE,KAAK,EAAE,QAAQ,CAAA;KAAE,CAAC,CAAC,GAAG,SAAS,CAAA;IAClD,UAAU,CAAC,EAAE,OAAO,CAAC,CAAC,mBAAmB,CAAC,CAAC,GAAG,SAAS,CAAA;IACvD,wFAAwF;IACxF,SAAS,CAAC,EAAE,OAAO,CAAC,CAAC;QAAE,KAAK,EAAE,QAAQ,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC,GAAG,SAAS,CAAA;CACrE;AA0XH,QAAA,MAAM,WAAW,sDAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}