@inkbar/svelte 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -12
- package/dist/components/InkAnnotation.svelte +0 -12
- package/dist/components/InkAnnotation.svelte.d.ts.map +1 -1
- package/dist/components/InkColorMenu.svelte +247 -0
- package/dist/components/InkColorMenu.svelte.d.ts +35 -0
- package/dist/components/InkColorMenu.svelte.d.ts.map +1 -0
- package/dist/components/InkHighlightMenu.svelte +175 -0
- package/dist/components/InkHighlightMenu.svelte.d.ts +16 -0
- package/dist/components/InkHighlightMenu.svelte.d.ts.map +1 -0
- package/dist/components/InkLineStyleMenu.svelte +141 -0
- package/dist/components/InkLineStyleMenu.svelte.d.ts +15 -0
- package/dist/components/InkLineStyleMenu.svelte.d.ts.map +1 -0
- package/dist/components/InkRoot.svelte +90 -3
- package/dist/components/InkRoot.svelte.d.ts +27 -2
- package/dist/components/InkRoot.svelte.d.ts.map +1 -1
- package/dist/components/InkShapeMenu.svelte +161 -0
- package/dist/components/InkShapeMenu.svelte.d.ts +15 -0
- package/dist/components/InkShapeMenu.svelte.d.ts.map +1 -0
- package/dist/components/InkSignatureDialog.svelte +70 -0
- package/dist/components/InkSignatureDialog.svelte.d.ts +22 -0
- package/dist/components/InkSignatureDialog.svelte.d.ts.map +1 -0
- package/dist/components/InkSignatureMenu.svelte +267 -0
- package/dist/components/InkSignatureMenu.svelte.d.ts +18 -0
- package/dist/components/InkSignatureMenu.svelte.d.ts.map +1 -0
- package/dist/components/InkTextStyleMenu.svelte +225 -0
- package/dist/components/InkTextStyleMenu.svelte.d.ts +15 -0
- package/dist/components/InkTextStyleMenu.svelte.d.ts.map +1 -0
- package/dist/components/{InkNotePopover.svelte → InkThreadPopover.svelte} +44 -32
- package/dist/components/InkThreadPopover.svelte.d.ts +9 -0
- package/dist/components/InkThreadPopover.svelte.d.ts.map +1 -0
- package/dist/components/InkToolButton.svelte +23 -7
- package/dist/components/InkToolButton.svelte.d.ts.map +1 -1
- package/dist/components/InkViewport.svelte +26 -2
- package/dist/components/InkViewport.svelte.d.ts.map +1 -1
- package/dist/components/SignatureForm.svelte +219 -0
- package/dist/components/SignatureForm.svelte.d.ts +10 -0
- package/dist/components/SignatureForm.svelte.d.ts.map +1 -0
- package/dist/components/ThreadForm.svelte +4 -2
- package/dist/components/ThreadForm.svelte.d.ts +1 -0
- package/dist/components/ThreadForm.svelte.d.ts.map +1 -1
- package/dist/components/child-snippet.d.ts +8 -0
- package/dist/components/child-snippet.d.ts.map +1 -1
- package/dist/components/child-snippet.js +15 -0
- package/dist/components/popover-focus.d.ts +16 -2
- package/dist/components/popover-focus.d.ts.map +1 -1
- package/dist/components/popover-focus.js +24 -3
- package/dist/context.d.ts +21 -2
- package/dist/context.d.ts.map +1 -1
- package/dist/hooks/useAnnotations.svelte.d.ts +1333 -578
- package/dist/hooks/useAnnotations.svelte.d.ts.map +1 -1
- package/dist/hooks/useAnnotations.svelte.js +1 -7
- package/dist/hooks/useToolOptions.svelte.d.ts +11 -0
- package/dist/hooks/useToolOptions.svelte.d.ts.map +1 -0
- package/dist/hooks/useToolOptions.svelte.js +19 -0
- package/dist/hooks/useToolStyle.svelte.d.ts +33 -0
- package/dist/hooks/useToolStyle.svelte.d.ts.map +1 -0
- package/dist/hooks/useToolStyle.svelte.js +33 -0
- package/dist/index.d.ts +21 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -2
- package/package.json +5 -5
- package/dist/components/InkNotePopover.svelte.d.ts +0 -9
- package/dist/components/InkNotePopover.svelte.d.ts.map +0 -1
- package/dist/components/InkSuggestionCard.svelte +0 -121
- package/dist/components/InkSuggestionCard.svelte.d.ts +0 -9
- package/dist/components/InkSuggestionCard.svelte.d.ts.map +0 -1
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
|
|
21
21
|
---
|
|
22
22
|
|
|
23
|
-
Drop a PDF viewer into your Svelte app and let people highlight text, leave notes with replies and
|
|
23
|
+
Drop a PDF viewer into your Svelte app and let people highlight text, leave notes with replies and redact. `@inkbar/svelte` gives you components to build your own toolbars, a popover for comment threads, and hooks for anything else. `@inkbar/theme` styles them all.
|
|
24
24
|
|
|
25
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
26
|
|
|
@@ -51,8 +51,7 @@ Svelte 5.46.4 or later is a peer dependency. Add the Vite plugin from `@inkbar/c
|
|
|
51
51
|
InkZoomOut,
|
|
52
52
|
InkZoomSelect,
|
|
53
53
|
InkZoomIn,
|
|
54
|
-
|
|
55
|
-
InkSuggestionCard,
|
|
54
|
+
InkThreadPopover,
|
|
56
55
|
} from '@inkbar/svelte'
|
|
57
56
|
import type { Annotation } from '@inkbar/core'
|
|
58
57
|
import { assets } from 'virtual:inkbar-assets'
|
|
@@ -65,7 +64,7 @@ Svelte 5.46.4 or later is a peer dependency. Add the Vite plugin from `@inkbar/c
|
|
|
65
64
|
|
|
66
65
|
<InkRoot src={{ url: signedUrl, key: documentId }} {assets} bind:annotations>
|
|
67
66
|
<InkToolbar>
|
|
68
|
-
<InkToolButton tool="
|
|
67
|
+
<InkToolButton tool="select-text" />
|
|
69
68
|
<InkToolButton tool="select" />
|
|
70
69
|
<InkToolButton tool="highlight" />
|
|
71
70
|
<InkToolButton tool="redact-area" />
|
|
@@ -84,8 +83,7 @@ Svelte 5.46.4 or later is a peer dependency. Add the Vite plugin from `@inkbar/c
|
|
|
84
83
|
<InkZoomSelect />
|
|
85
84
|
<InkZoomIn />
|
|
86
85
|
</InkToolbar>
|
|
87
|
-
<
|
|
88
|
-
<InkSuggestionCard />
|
|
86
|
+
<InkThreadPopover />
|
|
89
87
|
</InkRoot>
|
|
90
88
|
```
|
|
91
89
|
|
|
@@ -147,15 +145,21 @@ Inside `InkRoot`, `useHistory()` returns the same object. Its `batch(fn)` runs `
|
|
|
147
145
|
|
|
148
146
|
`key` identifies the document. Reuse the same key when only the signed URL changes and the document is not reloaded.
|
|
149
147
|
|
|
150
|
-
##
|
|
148
|
+
## Thread popover
|
|
151
149
|
|
|
152
|
-
`
|
|
150
|
+
`InkThreadPopover` opens the comment thread of the activated note or text markup (a highlight, underline, strikeout or squiggly). A new note or highlight has no text yet, so the form first writes its own `contents` (labelled "Comment"); after that it adds replies. A highlight with a comment shows a small marker at its end. Replies show in the thread only; the page draws just the note or highlight they answer.
|
|
153
151
|
|
|
154
|
-
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
|
|
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. A highlight stays: the mark is the point. Locked annotations are never removed. If you add notes yourself (`add({ type: 'text', ... })`), give them `contents`, or they will be removed the first time someone opens and closes them.
|
|
153
|
+
|
|
154
|
+
The highlight tool leaves `contents` free for the comment. `doc.text(page)` returns the page's text items; match the markup's `quads` against them to get the marked text. `InkHighlightMenu` is a split button for the tool: it picks the markup kind and its colour.
|
|
155
|
+
|
|
156
|
+
## Signatures
|
|
157
|
+
|
|
158
|
+
`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.
|
|
155
159
|
|
|
156
160
|
## Custom rendering
|
|
157
161
|
|
|
158
|
-
Slots are snippets. `InkViewport` takes `loading`, `empty`, `error`, `annotation` and `pageError`. `
|
|
162
|
+
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 }`. The props each receives are typed as `AnnotationSlotProps` and `ThreadPopoverSlotProps`, the same types the Vue and React packages export.
|
|
159
163
|
|
|
160
164
|
`InkRoot`, `InkToolbar`, `InkToolButton`, `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`.
|
|
161
165
|
|
|
@@ -173,10 +177,11 @@ The scroll area takes focus. From there:
|
|
|
173
177
|
|
|
174
178
|
- With the `highlight` or `redact-area` tool active, `Enter` turns the text selection into a mark.
|
|
175
179
|
- With the `note` tool active, `Enter` places a note at the centre of the current page and opens it.
|
|
180
|
+
- With the `signature` tool active, `Enter` places the chosen signature at the centre of the current page.
|
|
176
181
|
|
|
177
182
|
On a focused annotation:
|
|
178
183
|
|
|
179
|
-
- `Enter` or `Space` selects and activates it. A note
|
|
184
|
+
- `Enter` or `Space` selects and activates it. A note or text markup opens its thread; other types are only selected.
|
|
180
185
|
- `ArrowUp` and `ArrowDown` move to the previous or next annotation on the page.
|
|
181
186
|
- `Shift` + arrow moves it by 1 pt, or by 10 pt with `Alt` as well.
|
|
182
187
|
- `Ctrl` or `Cmd` + `Shift` + arrow resizes an area redaction, text box or shape.
|
|
@@ -190,7 +195,7 @@ Components carry state on data attributes, for example `[data-annotation][data-s
|
|
|
190
195
|
|
|
191
196
|
## Hooks
|
|
192
197
|
|
|
193
|
-
`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.
|
|
198
|
+
`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. 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.
|
|
194
199
|
|
|
195
200
|
## Licence
|
|
196
201
|
|
|
@@ -17,8 +17,6 @@
|
|
|
17
17
|
* default visual for every annotation that is not custom, so this never draws a second one.
|
|
18
18
|
* InkPage portals it into the annotation's overlay element.
|
|
19
19
|
*/
|
|
20
|
-
import { acceptSuggestion } from '@inkbar/core'
|
|
21
|
-
|
|
22
20
|
import { useInkContext } from '../context.js'
|
|
23
21
|
|
|
24
22
|
import type { AnnotationState } from '@inkbar/core/dom'
|
|
@@ -48,9 +46,6 @@
|
|
|
48
46
|
return () => observer.disconnect()
|
|
49
47
|
})
|
|
50
48
|
|
|
51
|
-
const isSuggestion = $derived(
|
|
52
|
-
annotation.type === 'redact' && annotation.origin === 'suggested'
|
|
53
|
-
)
|
|
54
49
|
const locked = $derived(annotation.locked === true)
|
|
55
50
|
const selected = $derived(ctx.selection.includes(annotation.id))
|
|
56
51
|
const store = () => ctx.document?.annotations
|
|
@@ -60,13 +55,6 @@
|
|
|
60
55
|
locked,
|
|
61
56
|
select: () => store()?.select([annotation.id]),
|
|
62
57
|
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
58
|
})
|
|
71
59
|
</script>
|
|
72
60
|
|
|
@@ -1 +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;
|
|
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;AA6DH,QAAA,MAAM,aAAa,wDAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
<script module lang="ts">
|
|
2
|
+
import type { StyleGroup } from '@inkbar/core'
|
|
3
|
+
import type { ColorProperty } from '@inkbar/core/dom'
|
|
4
|
+
import type { Snippet } from 'svelte'
|
|
5
|
+
import type { HTMLButtonAttributes } from 'svelte/elements'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A colour menu for one colour property (border, fill or text) of the selection or the
|
|
9
|
+
* active tool. The trigger shows the current colour as a swatch: split when the selection
|
|
10
|
+
* disagrees, slashed for no fill. The panel is a palette grid, plus "No fill" for `fill`
|
|
11
|
+
* and "Custom…", which opens a native colour input. The menu disables when nothing
|
|
12
|
+
* supports the property.
|
|
13
|
+
*/
|
|
14
|
+
export interface InkColorMenuProps extends Omit<HTMLButtonAttributes, 'children'> {
|
|
15
|
+
/** The colour this menu sets. */
|
|
16
|
+
property: ColorProperty
|
|
17
|
+
/** Accessible name; defaults to "Border colour", "Fill colour" or "Text colour". */
|
|
18
|
+
label?: string | undefined
|
|
19
|
+
/** Replaces the swatch. */
|
|
20
|
+
children?: Snippet | undefined
|
|
21
|
+
/**
|
|
22
|
+
* Renders the trigger outside the toolbar's roving focus, for a menu nested in another
|
|
23
|
+
* panel (the text style menu's text colour).
|
|
24
|
+
*/
|
|
25
|
+
standalone?: boolean | undefined
|
|
26
|
+
/**
|
|
27
|
+
* Scopes the menu to one style group: it shows and sets that group's colour whatever
|
|
28
|
+
* tool is active, and recolours the selection only when it is all in the group.
|
|
29
|
+
*/
|
|
30
|
+
group?: StyleGroup | undefined
|
|
31
|
+
/** The colour new annotations get while the style leaves it unset; the swatch shows it. */
|
|
32
|
+
fallback?: string | undefined
|
|
33
|
+
}
|
|
34
|
+
</script>
|
|
35
|
+
|
|
36
|
+
<script lang="ts">
|
|
37
|
+
import {
|
|
38
|
+
COLOR_COLUMNS,
|
|
39
|
+
colorMenuModel,
|
|
40
|
+
focusInitialMenuItem,
|
|
41
|
+
handleMenuKeys,
|
|
42
|
+
menuItems,
|
|
43
|
+
} from '@inkbar/core/dom'
|
|
44
|
+
import { Check } from '@inkbar/icons'
|
|
45
|
+
import { Popover, Toolbar } from 'bits-ui'
|
|
46
|
+
|
|
47
|
+
import { useToolStyle } from '../hooks/useToolStyle.svelte.js'
|
|
48
|
+
|
|
49
|
+
import { asBitsProps, composeHandlers } from './child-snippet.js'
|
|
50
|
+
import InkIcon from './InkIcon.svelte'
|
|
51
|
+
|
|
52
|
+
let {
|
|
53
|
+
property,
|
|
54
|
+
label,
|
|
55
|
+
class: className,
|
|
56
|
+
children,
|
|
57
|
+
standalone = false,
|
|
58
|
+
group,
|
|
59
|
+
fallback,
|
|
60
|
+
disabled,
|
|
61
|
+
onclick,
|
|
62
|
+
onkeydown,
|
|
63
|
+
...rest
|
|
64
|
+
}: InkColorMenuProps = $props()
|
|
65
|
+
|
|
66
|
+
const style = useToolStyle(() => group)
|
|
67
|
+
let open = $state(false)
|
|
68
|
+
|
|
69
|
+
const model = $derived(
|
|
70
|
+
colorMenuModel(property, style.current, style.supported, fallback)
|
|
71
|
+
)
|
|
72
|
+
const name = $derived(label ?? model.label)
|
|
73
|
+
const trigger = $derived(model.trigger)
|
|
74
|
+
|
|
75
|
+
let menu = $state<HTMLElement | null>(null)
|
|
76
|
+
let triggerEl = $state<HTMLButtonElement | null>(null)
|
|
77
|
+
let customInput = $state<HTMLInputElement | null>(null)
|
|
78
|
+
|
|
79
|
+
// The menu closes when the control disables (the selection cleared while it was open).
|
|
80
|
+
$effect(() => {
|
|
81
|
+
if (!model.enabled) open = false
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
/** ArrowDown opens on the first item; a click opens on the checked one. */
|
|
85
|
+
let focusFirst = false
|
|
86
|
+
// Bits runs the open-focus callback more than once, so the flag outlives it; reset on close.
|
|
87
|
+
$effect(() => {
|
|
88
|
+
if (!open) focusFirst = false
|
|
89
|
+
})
|
|
90
|
+
function onTriggerKeydown(e: KeyboardEvent): void {
|
|
91
|
+
if (e.key !== 'ArrowDown' || e.defaultPrevented || open || !model.enabled) return
|
|
92
|
+
e.preventDefault()
|
|
93
|
+
focusFirst = true
|
|
94
|
+
open = true
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const triggerAttrs = $derived({
|
|
98
|
+
class: ['ink-menu-trigger ink-color-menu', className],
|
|
99
|
+
'data-ink-color-menu': property,
|
|
100
|
+
'data-state': open ? 'open' : 'closed',
|
|
101
|
+
'data-mixed': model.mixed ? '' : undefined,
|
|
102
|
+
'aria-label': name,
|
|
103
|
+
title: name,
|
|
104
|
+
'aria-haspopup': 'menu' as const,
|
|
105
|
+
disabled: !!disabled || !model.enabled,
|
|
106
|
+
})
|
|
107
|
+
/** The trigger's handlers: the host's first, then Bits UI's and the menu's. */
|
|
108
|
+
const handlers = (props: Record<string, unknown>) => ({
|
|
109
|
+
onclick: composeHandlers(onclick, props.onclick),
|
|
110
|
+
onkeydown: composeHandlers(onkeydown, props.onkeydown, onTriggerKeydown),
|
|
111
|
+
})
|
|
112
|
+
</script>
|
|
113
|
+
|
|
114
|
+
{#snippet body()}
|
|
115
|
+
{#if children}
|
|
116
|
+
{@render children()}
|
|
117
|
+
{:else}
|
|
118
|
+
<span
|
|
119
|
+
class="ink-swatch"
|
|
120
|
+
data-ink-swatch=""
|
|
121
|
+
data-property={property}
|
|
122
|
+
data-kind={trigger.kind}
|
|
123
|
+
data-color={trigger.kind === 'color' ? trigger.color : undefined}
|
|
124
|
+
style={trigger.kind === 'color' ? `--ink-swatch: ${trigger.color}` : undefined}
|
|
125
|
+
aria-hidden="true"
|
|
126
|
+
></span>
|
|
127
|
+
{/if}
|
|
128
|
+
{/snippet}
|
|
129
|
+
|
|
130
|
+
<Popover.Root bind:open>
|
|
131
|
+
<Popover.Trigger>
|
|
132
|
+
{#snippet child({ props })}
|
|
133
|
+
{#if standalone}
|
|
134
|
+
<button
|
|
135
|
+
bind:this={triggerEl}
|
|
136
|
+
{...props}
|
|
137
|
+
{...asBitsProps(rest)}
|
|
138
|
+
{...triggerAttrs}
|
|
139
|
+
{...handlers(props)}
|
|
140
|
+
>
|
|
141
|
+
{@render body()}
|
|
142
|
+
</button>
|
|
143
|
+
{:else}
|
|
144
|
+
<Toolbar.Button
|
|
145
|
+
{...props}
|
|
146
|
+
{...asBitsProps(rest)}
|
|
147
|
+
{...triggerAttrs}
|
|
148
|
+
{...handlers(props)}
|
|
149
|
+
>
|
|
150
|
+
{@render body()}
|
|
151
|
+
</Toolbar.Button>
|
|
152
|
+
{/if}
|
|
153
|
+
{/snippet}
|
|
154
|
+
</Popover.Trigger>
|
|
155
|
+
<Popover.Portal>
|
|
156
|
+
<Popover.Content
|
|
157
|
+
bind:ref={menu}
|
|
158
|
+
role="menu"
|
|
159
|
+
aria-label={name}
|
|
160
|
+
class="ink-select-content ink-menu-content"
|
|
161
|
+
data-ink-select-content=""
|
|
162
|
+
data-ink-menu={`color-${property}`}
|
|
163
|
+
side="bottom"
|
|
164
|
+
align="start"
|
|
165
|
+
sideOffset={4}
|
|
166
|
+
collisionPadding={8}
|
|
167
|
+
onOpenAutoFocus={(e) => {
|
|
168
|
+
e.preventDefault()
|
|
169
|
+
if (menu) {
|
|
170
|
+
if (focusFirst) menuItems(menu)[0]?.focus({ preventScroll: true })
|
|
171
|
+
else focusInitialMenuItem(menu)
|
|
172
|
+
}
|
|
173
|
+
}}
|
|
174
|
+
onkeydown={(e) => {
|
|
175
|
+
// Nested in another panel, Escape closes this menu alone; Bits would otherwise pick
|
|
176
|
+
// the layer to close by registration order.
|
|
177
|
+
if (standalone && e.key === 'Escape') {
|
|
178
|
+
e.stopPropagation()
|
|
179
|
+
e.preventDefault()
|
|
180
|
+
open = false
|
|
181
|
+
triggerEl?.focus()
|
|
182
|
+
return
|
|
183
|
+
}
|
|
184
|
+
if (menu) handleMenuKeys(e, menu)
|
|
185
|
+
}}
|
|
186
|
+
>
|
|
187
|
+
{#if model.noFill}
|
|
188
|
+
{@const noFill = model.noFill}
|
|
189
|
+
<button
|
|
190
|
+
type="button"
|
|
191
|
+
role="menuitemradio"
|
|
192
|
+
class="ink-select-item ink-menu-item"
|
|
193
|
+
data-ink-select-item=""
|
|
194
|
+
tabindex={-1}
|
|
195
|
+
aria-checked={noFill.checked}
|
|
196
|
+
data-state={noFill.checked ? 'checked' : 'unchecked'}
|
|
197
|
+
onclick={() => style.set(noFill.patch)}
|
|
198
|
+
>
|
|
199
|
+
No fill
|
|
200
|
+
{#if noFill.checked}
|
|
201
|
+
<span class="ink-select-indicator"><InkIcon icon={Check} size={16} /></span>
|
|
202
|
+
{/if}
|
|
203
|
+
</button>
|
|
204
|
+
{/if}
|
|
205
|
+
<div
|
|
206
|
+
role="group"
|
|
207
|
+
aria-label="Colours"
|
|
208
|
+
class="ink-color-grid"
|
|
209
|
+
data-ink-menu-columns={COLOR_COLUMNS}
|
|
210
|
+
>
|
|
211
|
+
{#each model.swatches as sw (sw.key)}
|
|
212
|
+
<button
|
|
213
|
+
type="button"
|
|
214
|
+
role="menuitemradio"
|
|
215
|
+
class="ink-swatch-item"
|
|
216
|
+
tabindex={-1}
|
|
217
|
+
aria-label={sw.label}
|
|
218
|
+
aria-checked={sw.checked}
|
|
219
|
+
data-state={sw.checked ? 'checked' : 'unchecked'}
|
|
220
|
+
style={`--ink-swatch: ${sw.color}`}
|
|
221
|
+
onclick={() => style.set(sw.patch)}
|
|
222
|
+
></button>
|
|
223
|
+
{/each}
|
|
224
|
+
</div>
|
|
225
|
+
<button
|
|
226
|
+
type="button"
|
|
227
|
+
role="menuitem"
|
|
228
|
+
class="ink-select-item ink-menu-item"
|
|
229
|
+
data-ink-select-item=""
|
|
230
|
+
tabindex={-1}
|
|
231
|
+
onclick={() => customInput?.click()}
|
|
232
|
+
>
|
|
233
|
+
Custom…
|
|
234
|
+
</button>
|
|
235
|
+
</Popover.Content>
|
|
236
|
+
</Popover.Portal>
|
|
237
|
+
</Popover.Root>
|
|
238
|
+
<input
|
|
239
|
+
bind:this={customInput}
|
|
240
|
+
type="color"
|
|
241
|
+
class="ink-color-input"
|
|
242
|
+
aria-label="Custom colour"
|
|
243
|
+
data-ink-color-input={property}
|
|
244
|
+
tabindex={-1}
|
|
245
|
+
value={model.customValue.toLowerCase()}
|
|
246
|
+
onchange={(e) => style.set(model.custom(e.currentTarget.value))}
|
|
247
|
+
/>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { StyleGroup } from '@inkbar/core';
|
|
2
|
+
import type { ColorProperty } from '@inkbar/core/dom';
|
|
3
|
+
import type { Snippet } from 'svelte';
|
|
4
|
+
import type { HTMLButtonAttributes } from 'svelte/elements';
|
|
5
|
+
/**
|
|
6
|
+
* A colour menu for one colour property (border, fill or text) of the selection or the
|
|
7
|
+
* active tool. The trigger shows the current colour as a swatch: split when the selection
|
|
8
|
+
* disagrees, slashed for no fill. The panel is a palette grid, plus "No fill" for `fill`
|
|
9
|
+
* and "Custom…", which opens a native colour input. The menu disables when nothing
|
|
10
|
+
* supports the property.
|
|
11
|
+
*/
|
|
12
|
+
export interface InkColorMenuProps extends Omit<HTMLButtonAttributes, 'children'> {
|
|
13
|
+
/** The colour this menu sets. */
|
|
14
|
+
property: ColorProperty;
|
|
15
|
+
/** Accessible name; defaults to "Border colour", "Fill colour" or "Text colour". */
|
|
16
|
+
label?: string | undefined;
|
|
17
|
+
/** Replaces the swatch. */
|
|
18
|
+
children?: Snippet | undefined;
|
|
19
|
+
/**
|
|
20
|
+
* Renders the trigger outside the toolbar's roving focus, for a menu nested in another
|
|
21
|
+
* panel (the text style menu's text colour).
|
|
22
|
+
*/
|
|
23
|
+
standalone?: boolean | undefined;
|
|
24
|
+
/**
|
|
25
|
+
* Scopes the menu to one style group: it shows and sets that group's colour whatever
|
|
26
|
+
* tool is active, and recolours the selection only when it is all in the group.
|
|
27
|
+
*/
|
|
28
|
+
group?: StyleGroup | undefined;
|
|
29
|
+
/** The colour new annotations get while the style leaves it unset; the swatch shows it. */
|
|
30
|
+
fallback?: string | undefined;
|
|
31
|
+
}
|
|
32
|
+
declare const InkColorMenu: import("svelte").Component<InkColorMenuProps, {}, "">;
|
|
33
|
+
type InkColorMenu = ReturnType<typeof InkColorMenu>;
|
|
34
|
+
export default InkColorMenu;
|
|
35
|
+
//# sourceMappingURL=InkColorMenu.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"InkColorMenu.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/InkColorMenu.svelte.ts"],"names":[],"mappings":"AAGE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAC9C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAA;AACrD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAA;AACrC,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAA;AAE3D;;;;;;GAMG;AACH,MAAM,WAAW,iBAAkB,SAAQ,IAAI,CAAC,oBAAoB,EAAE,UAAU,CAAC;IAC/E,iCAAiC;IACjC,QAAQ,EAAE,aAAa,CAAA;IACvB,oFAAoF;IACpF,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAC1B,2BAA2B;IAC3B,QAAQ,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;IAC9B;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;IAChC;;;OAGG;IACH,KAAK,CAAC,EAAE,UAAU,GAAG,SAAS,CAAA;IAC9B,2FAA2F;IAC3F,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAC9B;AA6JH,QAAA,MAAM,YAAY,uDAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
<script module lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte'
|
|
3
|
+
import type { HTMLAttributes } from 'svelte/elements'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A split button for the highlight tool. The main part activates the tool with the current
|
|
7
|
+
* markup kind and draws that kind's icon; the chevron opens a menu of the four kinds plus
|
|
8
|
+
* the markup colour. Choosing a kind switches to the tool with it. The colour is a nested
|
|
9
|
+
* `InkColorMenu` scoped to the `markup` style group, so it works whatever tool is active.
|
|
10
|
+
*/
|
|
11
|
+
export interface InkHighlightMenuProps extends Omit<
|
|
12
|
+
HTMLAttributes<HTMLDivElement>,
|
|
13
|
+
'children'
|
|
14
|
+
> {
|
|
15
|
+
/** Replaces the main button's icon. */
|
|
16
|
+
children?: Snippet | undefined
|
|
17
|
+
}
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
<script lang="ts">
|
|
21
|
+
import {
|
|
22
|
+
focusInitialMenuItem,
|
|
23
|
+
handleMenuKeys,
|
|
24
|
+
highlightMenuModel,
|
|
25
|
+
MARKUP_ICONS,
|
|
26
|
+
MARKUP_KINDS,
|
|
27
|
+
menuItems,
|
|
28
|
+
type HighlightOptions,
|
|
29
|
+
type MarkupKind,
|
|
30
|
+
} from '@inkbar/core/dom'
|
|
31
|
+
import { ChevronDown } from '@inkbar/icons'
|
|
32
|
+
import { Popover, Toolbar } from 'bits-ui'
|
|
33
|
+
|
|
34
|
+
import { useDocument } from '../hooks/useDocument.svelte.js'
|
|
35
|
+
import { useTool } from '../hooks/useTool.svelte.js'
|
|
36
|
+
import { useToolOptions } from '../hooks/useToolOptions.svelte.js'
|
|
37
|
+
|
|
38
|
+
import InkColorMenu from './InkColorMenu.svelte'
|
|
39
|
+
import InkIcon from './InkIcon.svelte'
|
|
40
|
+
|
|
41
|
+
let { class: className, children, ...rest }: InkHighlightMenuProps = $props()
|
|
42
|
+
|
|
43
|
+
const current = useTool()
|
|
44
|
+
const inkDocument = useDocument()
|
|
45
|
+
const highlight = useToolOptions<HighlightOptions>('highlight')
|
|
46
|
+
let open = $state(false)
|
|
47
|
+
/** The split button: the panel lines up under it rather than the chevron. */
|
|
48
|
+
let group = $state<HTMLElement | null>(null)
|
|
49
|
+
|
|
50
|
+
const model = $derived(highlightMenuModel(highlight.options, MARKUP_KINDS))
|
|
51
|
+
/** The colour the chosen kind gets while the markup style leaves it unset. */
|
|
52
|
+
const fallback = $derived(inkDocument.document?.annotations.defaults.color(model.kind))
|
|
53
|
+
const active = $derived(current.tool === 'highlight')
|
|
54
|
+
|
|
55
|
+
let menu = $state<HTMLElement | null>(null)
|
|
56
|
+
|
|
57
|
+
/** ArrowDown opens on the first item; a click opens on the checked one. */
|
|
58
|
+
let focusFirst = false
|
|
59
|
+
// Bits runs the open-focus callback more than once, so the flag outlives it; reset on close.
|
|
60
|
+
$effect(() => {
|
|
61
|
+
if (!open) focusFirst = false
|
|
62
|
+
})
|
|
63
|
+
function onTriggerKeydown(e: KeyboardEvent): void {
|
|
64
|
+
if (e.key !== 'ArrowDown' || e.defaultPrevented || open) return
|
|
65
|
+
e.preventDefault()
|
|
66
|
+
focusFirst = true
|
|
67
|
+
open = true
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function choose(kind: MarkupKind): void {
|
|
71
|
+
highlight.setOptions({ kind })
|
|
72
|
+
current.setTool('highlight')
|
|
73
|
+
open = false
|
|
74
|
+
}
|
|
75
|
+
</script>
|
|
76
|
+
|
|
77
|
+
<div
|
|
78
|
+
bind:this={group}
|
|
79
|
+
role="group"
|
|
80
|
+
aria-label="Highlight"
|
|
81
|
+
class={['ink-split-button ink-highlight-menu', className]}
|
|
82
|
+
data-ink-highlight-menu=""
|
|
83
|
+
{...rest}
|
|
84
|
+
>
|
|
85
|
+
<Toolbar.Button
|
|
86
|
+
class="ink-tool-button ink-highlight-button"
|
|
87
|
+
data-ink-tool-button=""
|
|
88
|
+
data-ink-highlight-button=""
|
|
89
|
+
data-tool="highlight"
|
|
90
|
+
data-kind={model.kind}
|
|
91
|
+
data-state={active ? 'on' : 'off'}
|
|
92
|
+
aria-pressed={active}
|
|
93
|
+
aria-label={model.label}
|
|
94
|
+
title={model.label}
|
|
95
|
+
onclick={() => current.setTool('highlight')}
|
|
96
|
+
>
|
|
97
|
+
{#if children}{@render children()}{:else}<InkIcon
|
|
98
|
+
icon={MARKUP_ICONS[model.kind]}
|
|
99
|
+
/>{/if}
|
|
100
|
+
</Toolbar.Button>
|
|
101
|
+
<Popover.Root bind:open>
|
|
102
|
+
<Popover.Trigger>
|
|
103
|
+
{#snippet child({ props })}
|
|
104
|
+
<Toolbar.Button
|
|
105
|
+
{...props}
|
|
106
|
+
class="ink-menu-trigger ink-highlight-menu-trigger"
|
|
107
|
+
data-ink-highlight-menu-trigger=""
|
|
108
|
+
data-state={open ? 'open' : 'closed'}
|
|
109
|
+
aria-label="Choose highlight"
|
|
110
|
+
title="Choose highlight"
|
|
111
|
+
aria-haspopup="menu"
|
|
112
|
+
onkeydown={onTriggerKeydown}
|
|
113
|
+
>
|
|
114
|
+
<InkIcon icon={ChevronDown} />
|
|
115
|
+
</Toolbar.Button>
|
|
116
|
+
{/snippet}
|
|
117
|
+
</Popover.Trigger>
|
|
118
|
+
<Popover.Portal>
|
|
119
|
+
<Popover.Content
|
|
120
|
+
bind:ref={menu}
|
|
121
|
+
role="menu"
|
|
122
|
+
aria-label="Highlights"
|
|
123
|
+
class="ink-select-content ink-menu-content"
|
|
124
|
+
data-ink-select-content=""
|
|
125
|
+
data-ink-menu="highlight"
|
|
126
|
+
customAnchor={group}
|
|
127
|
+
side="bottom"
|
|
128
|
+
align="start"
|
|
129
|
+
sideOffset={4}
|
|
130
|
+
collisionPadding={8}
|
|
131
|
+
onOpenAutoFocus={(e) => {
|
|
132
|
+
e.preventDefault()
|
|
133
|
+
if (menu) {
|
|
134
|
+
if (focusFirst) menuItems(menu)[0]?.focus({ preventScroll: true })
|
|
135
|
+
else focusInitialMenuItem(menu)
|
|
136
|
+
}
|
|
137
|
+
}}
|
|
138
|
+
onkeydowncapture={(e: KeyboardEvent) => menu && handleMenuKeys(e, menu)}
|
|
139
|
+
>
|
|
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"
|
|
169
|
+
tabindex={-1}
|
|
170
|
+
/>
|
|
171
|
+
</div>
|
|
172
|
+
</Popover.Content>
|
|
173
|
+
</Popover.Portal>
|
|
174
|
+
</Popover.Root>
|
|
175
|
+
</div>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
|
+
/**
|
|
4
|
+
* A split button for the highlight tool. The main part activates the tool with the current
|
|
5
|
+
* markup kind and draws that kind's icon; the chevron opens a menu of the four kinds plus
|
|
6
|
+
* the markup colour. Choosing a kind switches to the tool with it. The colour is a nested
|
|
7
|
+
* `InkColorMenu` scoped to the `markup` style group, so it works whatever tool is active.
|
|
8
|
+
*/
|
|
9
|
+
export interface InkHighlightMenuProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children'> {
|
|
10
|
+
/** Replaces the main button's icon. */
|
|
11
|
+
children?: Snippet | undefined;
|
|
12
|
+
}
|
|
13
|
+
declare const InkHighlightMenu: import("svelte").Component<InkHighlightMenuProps, {}, "">;
|
|
14
|
+
type InkHighlightMenu = ReturnType<typeof InkHighlightMenu>;
|
|
15
|
+
export default InkHighlightMenu;
|
|
16
|
+
//# sourceMappingURL=InkHighlightMenu.svelte.d.ts.map
|
|
@@ -0,0 +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;AAkHH,QAAA,MAAM,gBAAgB,2DAAwC,CAAC;AAC/D,KAAK,gBAAgB,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAC5D,eAAe,gBAAgB,CAAC"}
|