@inkbar/svelte 0.4.0 → 0.6.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 (46) hide show
  1. package/README.md +23 -7
  2. package/dist/components/InkAnnotation.svelte +0 -12
  3. package/dist/components/InkAnnotation.svelte.d.ts.map +1 -1
  4. package/dist/components/InkColorMenu.svelte +13 -19
  5. package/dist/components/InkColorMenu.svelte.d.ts +2 -0
  6. package/dist/components/InkColorMenu.svelte.d.ts.map +1 -1
  7. package/dist/components/InkCommand.svelte +63 -0
  8. package/dist/components/InkCommand.svelte.d.ts +17 -0
  9. package/dist/components/InkCommand.svelte.d.ts.map +1 -0
  10. package/dist/components/InkHighlightMenu.svelte +15 -13
  11. package/dist/components/InkLineStyleMenu.svelte +7 -17
  12. package/dist/components/InkLineStyleMenu.svelte.d.ts.map +1 -1
  13. package/dist/components/InkRoot.svelte +111 -3
  14. package/dist/components/InkRoot.svelte.d.ts +14 -1
  15. package/dist/components/InkRoot.svelte.d.ts.map +1 -1
  16. package/dist/components/InkShapeMenu.svelte +10 -13
  17. package/dist/components/InkShapeMenu.svelte.d.ts.map +1 -1
  18. package/dist/components/InkSignatureMenu.svelte +10 -13
  19. package/dist/components/InkSignatureMenu.svelte.d.ts.map +1 -1
  20. package/dist/components/InkThreadPopover.svelte +7 -0
  21. package/dist/components/InkThreadPopover.svelte.d.ts.map +1 -1
  22. package/dist/components/InkToolButton.svelte +11 -3
  23. package/dist/components/InkToolButton.svelte.d.ts.map +1 -1
  24. package/dist/components/InkViewport.svelte +32 -2
  25. package/dist/components/InkViewport.svelte.d.ts.map +1 -1
  26. package/dist/components/menu-open-focus.d.ts +12 -0
  27. package/dist/components/menu-open-focus.d.ts.map +1 -0
  28. package/dist/components/menu-open-focus.js +35 -0
  29. package/dist/components/popover-focus.d.ts +16 -2
  30. package/dist/components/popover-focus.d.ts.map +1 -1
  31. package/dist/components/popover-focus.js +24 -3
  32. package/dist/context.d.ts +22 -4
  33. package/dist/context.d.ts.map +1 -1
  34. package/dist/hooks/useAnnotations.svelte.d.ts +795 -1258
  35. package/dist/hooks/useAnnotations.svelte.d.ts.map +1 -1
  36. package/dist/hooks/useAnnotations.svelte.js +1 -7
  37. package/dist/hooks/useCommand.svelte.d.ts +23 -0
  38. package/dist/hooks/useCommand.svelte.d.ts.map +1 -0
  39. package/dist/hooks/useCommand.svelte.js +44 -0
  40. package/dist/index.d.ts +6 -4
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +2 -1
  43. package/package.json +5 -5
  44. package/dist/components/InkSuggestionCard.svelte +0 -121
  45. package/dist/components/InkSuggestionCard.svelte.d.ts +0 -9
  46. 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 review redactions. `@inkbar/svelte` gives you components to build your own toolbars, popovers for comment threads and suggested redactions, and hooks for anything else. `@inkbar/theme` styles them all.
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
 
@@ -52,7 +52,6 @@ Svelte 5.46.4 or later is a peer dependency. Add the Vite plugin from `@inkbar/c
52
52
  InkZoomSelect,
53
53
  InkZoomIn,
54
54
  InkThreadPopover,
55
- InkSuggestionCard,
56
55
  } from '@inkbar/svelte'
57
56
  import type { Annotation } from '@inkbar/core'
58
57
  import { assets } from 'virtual:inkbar-assets'
@@ -85,7 +84,6 @@ Svelte 5.46.4 or later is a peer dependency. Add the Vite plugin from `@inkbar/c
85
84
  <InkZoomIn />
86
85
  </InkToolbar>
87
86
  <InkThreadPopover />
88
- <InkSuggestionCard />
89
87
  </InkRoot>
90
88
  ```
91
89
 
@@ -159,11 +157,29 @@ The highlight tool leaves `contents` free for the comment. `doc.text(page)` retu
159
157
 
160
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.
161
159
 
160
+ ## Plugins
161
+
162
+ A plugin from `definePlugin` (in `@inkbar/core/dom`) adds commands, tools, decorations, page layers and its own record stores. Pass plugins to `InkRoot` once: the prop is read when the root is created. Nothing a plugin registers shows until you place it:
163
+
164
+ ```svelte
165
+ <InkRoot {src} {assets} plugins={[suggestions]}>
166
+ <InkToolbar>
167
+ <InkCommand id="sandbox.suggest" />
168
+ <InkToolButton tool="sandbox.stamp" />
169
+ </InkToolbar>
170
+ <InkViewport />
171
+ </InkRoot>
172
+ ```
173
+
174
+ `InkCommand` is a button named by the command's label, disabled while the command is, and its status in a live region beside it. While the command runs, the button keeps focus and has `aria-busy` and `aria-disabled`, and pressing it does nothing. It shows the command's icon, or its label as text when the command has no icon. To build your own, `useCommand(id)` returns `run`, `enabled`, `busy`, `status`, `label` and `icon`. Plugins are set up once `InkRoot` has mounted in the browser, never on the server. Until then the command is disabled and has no label, and `InkCommand` keeps its button out of the accessibility tree (`aria-hidden`). Read `label` and `icon` off the handle, as you read `enabled`, so they fill in then. An id no plugin registered stays that way: `InkCommand` and `useCommand` log an `InkError('UnknownCommand')` to the console once, naming the id and the registered ones, and nothing throws. `InkToolButton` takes a plugin tool's own label and icon.
175
+
176
+ Plugin stores undo with annotations. In controlled mode with `history`, each undo step also snapshots the plugin stores, so accepting something and undoing it restores both.
177
+
162
178
  ## Custom rendering
163
179
 
164
- Slots are snippets. `InkViewport` takes `loading`, `empty`, `error`, `annotation` and `pageError`. `InkThreadPopover` 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`, `ThreadPopoverSlotProps` and `SuggestionCardSlotProps`, the same types the Vue and React packages export.
180
+ Slots are snippets. `InkViewport` takes `loading`, `empty`, `error`, `annotation` and `pageError`. `InkThreadPopover` takes `children` as a snippet that receives the popover's props. `InkToolButton` takes `children` as a snippet of `{ active, label }`, and `InkCommand` one of `{ label, busy }`. The props each receives are typed as `AnnotationSlotProps` and `ThreadPopoverSlotProps`, the same types the Vue and React packages export.
165
181
 
166
- `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`.
182
+ `InkRoot`, `InkToolbar`, `InkToolButton`, `InkCommand`, `InkUndo`, `InkRedo`, `InkPrevPage`, `InkNextPage`, `InkZoomIn` and `InkZoomOut` take a `child` snippet to render your own element. Spread the `props` it gives you on that element. The snippet type is exported as `ChildSnippet`.
167
183
 
168
184
  ```svelte
169
185
  <InkToolbar>
@@ -183,7 +199,7 @@ The scroll area takes focus. From there:
183
199
 
184
200
  On a focused annotation:
185
201
 
186
- - `Enter` or `Space` selects and activates it. A note opens its thread and a suggested redaction its card; other types are only selected.
202
+ - `Enter` or `Space` selects and activates it. A note or text markup opens its thread; other types are only selected.
187
203
  - `ArrowUp` and `ArrowDown` move to the previous or next annotation on the page.
188
204
  - `Shift` + arrow moves it by 1 pt, or by 10 pt with `Alt` as well.
189
205
  - `Ctrl` or `Cmd` + `Shift` + arrow resizes an area redaction, text box or shape.
@@ -197,7 +213,7 @@ Components carry state on data attributes, for example `[data-annotation][data-s
197
213
 
198
214
  ## Hooks
199
215
 
200
- `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.
216
+ `useDocument`, `useAnnotations`, `useTool`, `useSelection`, `useZoom`, `usePage`, `useHistory`, `useEmbedded` and `useCommand` read the state of the nearest `InkRoot`. Call them during component initialisation, as you would `getContext`. Each returns an object whose reactive members are getters: read `useZoom().zoom` in your template or an effect and it tracks; destructure it (`const { zoom } = useZoom()`) and you copy the value once. Keep the object and read through it. Tool options, such as the shape kind, last as long as `InkRoot`. The tool style that `useToolStyle` reads and sets (colours, line style, text style) belongs to the document: opening another one starts again from the theme's defaults.
201
217
 
202
218
  ## Licence
203
219
 
@@ -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;AA2EH,QAAA,MAAM,aAAa,wDAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
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"}
@@ -28,17 +28,13 @@
28
28
  * tool is active, and recolours the selection only when it is all in the group.
29
29
  */
30
30
  group?: StyleGroup | undefined
31
+ /** The colour new annotations get while the style leaves it unset; the swatch shows it. */
32
+ fallback?: string | undefined
31
33
  }
32
34
  </script>
33
35
 
34
36
  <script lang="ts">
35
- import {
36
- COLOR_COLUMNS,
37
- colorMenuModel,
38
- focusInitialMenuItem,
39
- handleMenuKeys,
40
- menuItems,
41
- } from '@inkbar/core/dom'
37
+ import { COLOR_COLUMNS, colorMenuModel, handleMenuKeys } from '@inkbar/core/dom'
42
38
  import { Check } from '@inkbar/icons'
43
39
  import { Popover, Toolbar } from 'bits-ui'
44
40
 
@@ -46,6 +42,7 @@
46
42
 
47
43
  import { asBitsProps, composeHandlers } from './child-snippet.js'
48
44
  import InkIcon from './InkIcon.svelte'
45
+ import { createMenuOpenFocus } from './menu-open-focus.js'
49
46
 
50
47
  let {
51
48
  property,
@@ -54,6 +51,7 @@
54
51
  children,
55
52
  standalone = false,
56
53
  group,
54
+ fallback,
57
55
  disabled,
58
56
  onclick,
59
57
  onkeydown,
@@ -63,7 +61,9 @@
63
61
  const style = useToolStyle(() => group)
64
62
  let open = $state(false)
65
63
 
66
- const model = $derived(colorMenuModel(property, style.current, style.supported))
64
+ const model = $derived(
65
+ colorMenuModel(property, style.current, style.supported, fallback)
66
+ )
67
67
  const name = $derived(label ?? model.label)
68
68
  const trigger = $derived(model.trigger)
69
69
 
@@ -77,15 +77,14 @@
77
77
  })
78
78
 
79
79
  /** ArrowDown opens on the first item; a click opens on the checked one. */
80
- let focusFirst = false
81
- // Bits runs the open-focus callback more than once, so the flag outlives it; reset on close.
80
+ const openFocus = createMenuOpenFocus()
82
81
  $effect(() => {
83
- if (!open) focusFirst = false
82
+ if (!open) openFocus.reset()
84
83
  })
85
84
  function onTriggerKeydown(e: KeyboardEvent): void {
86
85
  if (e.key !== 'ArrowDown' || e.defaultPrevented || open || !model.enabled) return
87
86
  e.preventDefault()
88
- focusFirst = true
87
+ openFocus.first()
89
88
  open = true
90
89
  }
91
90
 
@@ -159,13 +158,8 @@
159
158
  align="start"
160
159
  sideOffset={4}
161
160
  collisionPadding={8}
162
- onOpenAutoFocus={(e) => {
163
- e.preventDefault()
164
- if (menu) {
165
- if (focusFirst) menuItems(menu)[0]?.focus({ preventScroll: true })
166
- else focusInitialMenuItem(menu)
167
- }
168
- }}
161
+ onOpenAutoFocus={(e) => openFocus.onOpenAutoFocus(e, menu)}
162
+ onfocusin={openFocus.onfocusin}
169
163
  onkeydown={(e) => {
170
164
  // Nested in another panel, Escape closes this menu alone; Bits would otherwise pick
171
165
  // the layer to close by registration order.
@@ -26,6 +26,8 @@ export interface InkColorMenuProps extends Omit<HTMLButtonAttributes, 'children'
26
26
  * tool is active, and recolours the selection only when it is all in the group.
27
27
  */
28
28
  group?: StyleGroup | undefined;
29
+ /** The colour new annotations get while the style leaves it unset; the swatch shows it. */
30
+ fallback?: string | undefined;
29
31
  }
30
32
  declare const InkColorMenu: import("svelte").Component<InkColorMenuProps, {}, "">;
31
33
  type InkColorMenu = ReturnType<typeof InkColorMenu>;
@@ -1 +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;CAC/B;AA0JH,QAAA,MAAM,YAAY,uDAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
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;AAkJH,QAAA,MAAM,YAAY,uDAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
@@ -0,0 +1,63 @@
1
+ <script module lang="ts">
2
+ import type { ChildSnippet } from './child-snippet.js'
3
+ import type { Snippet } from 'svelte'
4
+ import type { HTMLButtonAttributes } from 'svelte/elements'
5
+
6
+ export interface InkCommandProps extends Omit<HTMLButtonAttributes, 'id' | 'children'> {
7
+ /** The command's namespaced id, such as `sandbox.suggest`. Read once. */
8
+ id: string
9
+ children?: Snippet<[{ label: string; busy: boolean }]> | undefined
10
+ /** Render your own button: spread `props` on it. */
11
+ child?: ChildSnippet | undefined
12
+ }
13
+ </script>
14
+
15
+ <script lang="ts">
16
+ /*
17
+ * A plugin command as a toolbar button with its status beside it: the markup of core's
18
+ * `createCommandButton`, so the theme styles both alike. The button is named by the command's
19
+ * label and disabled while the command is, except while it runs: then it keeps focus, with
20
+ * `aria-busy` and `aria-disabled`, and a press does nothing. The status is a live region. A command without an icon shows its label as the button's text. Until the plugins
21
+ * are set up (once InkRoot has mounted; never on the server) the button is disabled and
22
+ * `aria-hidden`. An id no plugin registered stays that way and logs
23
+ * `InkError('UnknownCommand')` to the console once.
24
+ */
25
+ import { Toolbar } from 'bits-ui'
26
+ import { untrack } from 'svelte'
27
+
28
+ import { useCommand } from '../hooks/useCommand.svelte.js'
29
+
30
+ import { asBitsProps } from './child-snippet.js'
31
+ import InkIcon from './InkIcon.svelte'
32
+
33
+ let { id, children, child, disabled, onclick, ...rest }: InkCommandProps = $props()
34
+
35
+ // The id is read once, as `useCommand` does: a different command is a different component.
36
+ const commandId = untrack(() => id)
37
+ const command = useCommand(commandId)
38
+ </script>
39
+
40
+ <div class="ink-command">
41
+ <Toolbar.Button
42
+ type="button"
43
+ data-command={commandId}
44
+ aria-label={command.label || undefined}
45
+ title={command.label || undefined}
46
+ aria-hidden={command.label ? undefined : 'true'}
47
+ aria-busy={command.busy}
48
+ aria-disabled={command.busy ? 'true' : undefined}
49
+ disabled={(!command.enabled && !command.busy) || !!disabled}
50
+ {...child ? { child } : {}}
51
+ {...asBitsProps(rest)}
52
+ onclick={(e) => {
53
+ onclick?.(e)
54
+ if (!e.defaultPrevented) void command.run()
55
+ }}
56
+ >
57
+ {#if children}{@render children({
58
+ label: command.label,
59
+ busy: command.busy,
60
+ })}{:else if command.icon}<InkIcon icon={command.icon} />{:else}{command.label}{/if}
61
+ </Toolbar.Button>
62
+ <span class="ink-command-status" role="status">{command.status}</span>
63
+ </div>
@@ -0,0 +1,17 @@
1
+ import type { ChildSnippet } from './child-snippet.js';
2
+ import type { Snippet } from 'svelte';
3
+ import type { HTMLButtonAttributes } from 'svelte/elements';
4
+ export interface InkCommandProps extends Omit<HTMLButtonAttributes, 'id' | 'children'> {
5
+ /** The command's namespaced id, such as `sandbox.suggest`. Read once. */
6
+ id: string;
7
+ children?: Snippet<[{
8
+ label: string;
9
+ busy: boolean;
10
+ }]> | undefined;
11
+ /** Render your own button: spread `props` on it. */
12
+ child?: ChildSnippet | undefined;
13
+ }
14
+ declare const InkCommand: import("svelte").Component<InkCommandProps, {}, "">;
15
+ type InkCommand = ReturnType<typeof InkCommand>;
16
+ export default InkCommand;
17
+ //# sourceMappingURL=InkCommand.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"InkCommand.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/InkCommand.svelte.ts"],"names":[],"mappings":"AAGE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AACtD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAA;AACrC,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAA;AAE3D,MAAM,WAAW,eAAgB,SAAQ,IAAI,CAAC,oBAAoB,EAAE,IAAI,GAAG,UAAU,CAAC;IACpF,yEAAyE;IACzE,EAAE,EAAE,MAAM,CAAA;IACV,QAAQ,CAAC,EAAE,OAAO,CAAC,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC,GAAG,SAAS,CAAA;IAClE,oDAAoD;IACpD,KAAK,CAAC,EAAE,YAAY,GAAG,SAAS,CAAA;CACjC;AAsDH,QAAA,MAAM,UAAU,qDAAwC,CAAC;AACzD,KAAK,UAAU,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;AAChD,eAAe,UAAU,CAAC"}
@@ -19,45 +19,49 @@
19
19
 
20
20
  <script lang="ts">
21
21
  import {
22
- focusInitialMenuItem,
23
22
  handleMenuKeys,
24
23
  highlightMenuModel,
25
24
  MARKUP_ICONS,
26
25
  MARKUP_KINDS,
27
- menuItems,
28
26
  type HighlightOptions,
29
27
  type MarkupKind,
30
28
  } from '@inkbar/core/dom'
31
29
  import { ChevronDown } from '@inkbar/icons'
32
30
  import { Popover, Toolbar } from 'bits-ui'
33
31
 
32
+ import { useDocument } from '../hooks/useDocument.svelte.js'
34
33
  import { useTool } from '../hooks/useTool.svelte.js'
35
34
  import { useToolOptions } from '../hooks/useToolOptions.svelte.js'
36
35
 
37
36
  import InkColorMenu from './InkColorMenu.svelte'
38
37
  import InkIcon from './InkIcon.svelte'
38
+ import { createMenuOpenFocus } from './menu-open-focus.js'
39
39
 
40
40
  let { class: className, children, ...rest }: InkHighlightMenuProps = $props()
41
41
 
42
42
  const current = useTool()
43
+ const inkDocument = useDocument()
43
44
  const highlight = useToolOptions<HighlightOptions>('highlight')
44
45
  let open = $state(false)
46
+ /** The split button: the panel lines up under it rather than the chevron. */
47
+ let group = $state<HTMLElement | null>(null)
45
48
 
46
49
  const model = $derived(highlightMenuModel(highlight.options, MARKUP_KINDS))
50
+ /** The colour the chosen kind gets while the markup style leaves it unset. */
51
+ const fallback = $derived(inkDocument.document?.annotations.defaults.color(model.kind))
47
52
  const active = $derived(current.tool === 'highlight')
48
53
 
49
54
  let menu = $state<HTMLElement | null>(null)
50
55
 
51
56
  /** ArrowDown opens on the first item; a click opens on the checked one. */
52
- let focusFirst = false
53
- // Bits runs the open-focus callback more than once, so the flag outlives it; reset on close.
57
+ const openFocus = createMenuOpenFocus()
54
58
  $effect(() => {
55
- if (!open) focusFirst = false
59
+ if (!open) openFocus.reset()
56
60
  })
57
61
  function onTriggerKeydown(e: KeyboardEvent): void {
58
62
  if (e.key !== 'ArrowDown' || e.defaultPrevented || open) return
59
63
  e.preventDefault()
60
- focusFirst = true
64
+ openFocus.first()
61
65
  open = true
62
66
  }
63
67
 
@@ -69,6 +73,7 @@
69
73
  </script>
70
74
 
71
75
  <div
76
+ bind:this={group}
72
77
  role="group"
73
78
  aria-label="Highlight"
74
79
  class={['ink-split-button ink-highlight-menu', className]}
@@ -116,17 +121,13 @@
116
121
  class="ink-select-content ink-menu-content"
117
122
  data-ink-select-content=""
118
123
  data-ink-menu="highlight"
124
+ customAnchor={group}
119
125
  side="bottom"
120
126
  align="start"
121
127
  sideOffset={4}
122
128
  collisionPadding={8}
123
- onOpenAutoFocus={(e) => {
124
- e.preventDefault()
125
- if (menu) {
126
- if (focusFirst) menuItems(menu)[0]?.focus({ preventScroll: true })
127
- else focusInitialMenuItem(menu)
128
- }
129
- }}
129
+ onOpenAutoFocus={(e) => openFocus.onOpenAutoFocus(e, menu)}
130
+ onfocusin={openFocus.onfocusin}
130
131
  onkeydowncapture={(e: KeyboardEvent) => menu && handleMenuKeys(e, menu)}
131
132
  >
132
133
  <div role="group" aria-label="Kinds" class="ink-highlight-kinds">
@@ -156,6 +157,7 @@
156
157
  group="markup"
157
158
  label="Highlight colour"
158
159
  standalone
160
+ {fallback}
159
161
  role="menuitem"
160
162
  tabindex={-1}
161
163
  />
@@ -14,12 +14,7 @@
14
14
  </script>
15
15
 
16
16
  <script lang="ts">
17
- import {
18
- focusInitialMenuItem,
19
- handleMenuKeys,
20
- lineStyleMenuModel,
21
- menuItems,
22
- } from '@inkbar/core/dom'
17
+ import { handleMenuKeys, lineStyleMenuModel } from '@inkbar/core/dom'
23
18
  import { Check, Minus } from '@inkbar/icons'
24
19
  import { Popover, Toolbar } from 'bits-ui'
25
20
 
@@ -27,6 +22,7 @@
27
22
 
28
23
  import { asBitsProps, composeHandlers } from './child-snippet.js'
29
24
  import InkIcon from './InkIcon.svelte'
25
+ import { createMenuOpenFocus } from './menu-open-focus.js'
30
26
 
31
27
  let {
32
28
  class: className,
@@ -50,15 +46,14 @@
50
46
  })
51
47
 
52
48
  /** ArrowDown opens on the first item; a click opens on the checked one. */
53
- let focusFirst = false
54
- // Bits runs the open-focus callback more than once, so the flag outlives it; reset on close.
49
+ const openFocus = createMenuOpenFocus()
55
50
  $effect(() => {
56
- if (!open) focusFirst = false
51
+ if (!open) openFocus.reset()
57
52
  })
58
53
  function onTriggerKeydown(e: KeyboardEvent): void {
59
54
  if (e.key !== 'ArrowDown' || e.defaultPrevented || open || !model.enabled) return
60
55
  e.preventDefault()
61
- focusFirst = true
56
+ openFocus.first()
62
57
  open = true
63
58
  }
64
59
  </script>
@@ -96,13 +91,8 @@
96
91
  align="start"
97
92
  sideOffset={4}
98
93
  collisionPadding={8}
99
- onOpenAutoFocus={(e) => {
100
- e.preventDefault()
101
- if (menu) {
102
- if (focusFirst) menuItems(menu)[0]?.focus({ preventScroll: true })
103
- else focusInitialMenuItem(menu)
104
- }
105
- }}
94
+ onOpenAutoFocus={(e) => openFocus.onOpenAutoFocus(e, menu)}
95
+ onfocusin={openFocus.onfocusin}
106
96
  onkeydown={(e) => menu && handleMenuKeys(e, menu)}
107
97
  >
108
98
  {#each model.sections as section (section.label)}
@@ -1 +1 @@
1
- {"version":3,"file":"InkLineStyleMenu.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/InkLineStyleMenu.svelte.ts"],"names":[],"mappings":"AAGE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAA;AACrC,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAA;AAE3D;;;;;GAKG;AACH,MAAM,WAAW,qBAAsB,SAAQ,IAAI,CAAC,oBAAoB,EAAE,UAAU,CAAC;IACnF,QAAQ,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;CAC/B;AAmGH,QAAA,MAAM,gBAAgB,2DAAwC,CAAC;AAC/D,KAAK,gBAAgB,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAC5D,eAAe,gBAAgB,CAAC"}
1
+ {"version":3,"file":"InkLineStyleMenu.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/InkLineStyleMenu.svelte.ts"],"names":[],"mappings":"AAGE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAA;AACrC,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAA;AAE3D;;;;;GAKG;AACH,MAAM,WAAW,qBAAsB,SAAQ,IAAI,CAAC,oBAAoB,EAAE,UAAU,CAAC;IACnF,QAAQ,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;CAC/B;AAyFH,QAAA,MAAM,gBAAgB,2DAAwC,CAAC;AAC/D,KAAK,gBAAgB,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAC5D,eAAe,gBAAgB,CAAC"}