@inkbar/svelte 0.5.0 → 0.7.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 (45) hide show
  1. package/README.md +27 -3
  2. package/dist/components/InkColorMenu.svelte +7 -18
  3. package/dist/components/InkColorMenu.svelte.d.ts.map +1 -1
  4. package/dist/components/InkCommand.svelte +63 -0
  5. package/dist/components/InkCommand.svelte.d.ts +17 -0
  6. package/dist/components/InkCommand.svelte.d.ts.map +1 -0
  7. package/dist/components/InkFindBar.svelte +213 -0
  8. package/dist/components/InkFindBar.svelte.d.ts +6 -0
  9. package/dist/components/InkFindBar.svelte.d.ts.map +1 -0
  10. package/dist/components/InkHighlightMenu.svelte +6 -13
  11. package/dist/components/InkHighlightMenu.svelte.d.ts.map +1 -1
  12. package/dist/components/InkLineStyleMenu.svelte +7 -17
  13. package/dist/components/InkLineStyleMenu.svelte.d.ts.map +1 -1
  14. package/dist/components/InkRoot.svelte +138 -3
  15. package/dist/components/InkRoot.svelte.d.ts +7 -1
  16. package/dist/components/InkRoot.svelte.d.ts.map +1 -1
  17. package/dist/components/InkSearchButton.svelte +56 -0
  18. package/dist/components/InkSearchButton.svelte.d.ts +5 -0
  19. package/dist/components/InkSearchButton.svelte.d.ts.map +1 -0
  20. package/dist/components/InkShapeMenu.svelte +6 -13
  21. package/dist/components/InkShapeMenu.svelte.d.ts.map +1 -1
  22. package/dist/components/InkSignatureMenu.svelte +6 -13
  23. package/dist/components/InkSignatureMenu.svelte.d.ts.map +1 -1
  24. package/dist/components/InkToolButton.svelte +11 -3
  25. package/dist/components/InkToolButton.svelte.d.ts.map +1 -1
  26. package/dist/components/InkViewport.svelte +15 -0
  27. package/dist/components/InkViewport.svelte.d.ts.map +1 -1
  28. package/dist/components/menu-open-focus.d.ts +12 -0
  29. package/dist/components/menu-open-focus.d.ts.map +1 -0
  30. package/dist/components/menu-open-focus.js +35 -0
  31. package/dist/components/search-button.d.ts +10 -0
  32. package/dist/components/search-button.d.ts.map +1 -0
  33. package/dist/components/search-button.js +1 -0
  34. package/dist/context.d.ts +21 -4
  35. package/dist/context.d.ts.map +1 -1
  36. package/dist/hooks/useCommand.svelte.d.ts +23 -0
  37. package/dist/hooks/useCommand.svelte.d.ts.map +1 -0
  38. package/dist/hooks/useCommand.svelte.js +44 -0
  39. package/dist/hooks/useSearch.svelte.d.ts +11 -0
  40. package/dist/hooks/useSearch.svelte.d.ts.map +1 -0
  41. package/dist/hooks/useSearch.svelte.js +31 -0
  42. package/dist/index.d.ts +10 -1
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js +5 -0
  45. package/package.json +6 -5
package/README.md CHANGED
@@ -157,11 +157,35 @@ The highlight tool leaves `contents` free for the comment. `doc.text(page)` retu
157
157
 
158
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.
159
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
+
178
+ ## Search
179
+
180
+ `InkFindBar` is the find bar and `InkSearchButton` opens it. Place the bar anywhere inside `InkRoot`, once `InkViewport` is there; Ctrl/Cmd+F opens it while it is mounted. `useSearch()` returns the search `state` and its actions (`open`, `setQuery`, `setMatchCase`, `next`, `previous`, `close`) for your own control. See the [`@inkbar/core` README](https://github.com/inkbar/inkbar/tree/main/packages/core#search) for the API.
181
+
182
+ The bar handles Escape itself and stops it there, so an `onkeydown` you pass to `InkFindBar` never sees Escape.
183
+
160
184
  ## Custom rendering
161
185
 
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.
186
+ 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.
163
187
 
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`.
188
+ `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`.
165
189
 
166
190
  ```svelte
167
191
  <InkToolbar>
@@ -195,7 +219,7 @@ Components carry state on data attributes, for example `[data-annotation][data-s
195
219
 
196
220
  ## Hooks
197
221
 
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.
222
+ `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.
199
223
 
200
224
  ## Licence
201
225
 
@@ -34,13 +34,7 @@
34
34
  </script>
35
35
 
36
36
  <script lang="ts">
37
- import {
38
- COLOR_COLUMNS,
39
- colorMenuModel,
40
- focusInitialMenuItem,
41
- handleMenuKeys,
42
- menuItems,
43
- } from '@inkbar/core/dom'
37
+ import { COLOR_COLUMNS, colorMenuModel, handleMenuKeys } from '@inkbar/core/dom'
44
38
  import { Check } from '@inkbar/icons'
45
39
  import { Popover, Toolbar } from 'bits-ui'
46
40
 
@@ -48,6 +42,7 @@
48
42
 
49
43
  import { asBitsProps, composeHandlers } from './child-snippet.js'
50
44
  import InkIcon from './InkIcon.svelte'
45
+ import { createMenuOpenFocus } from './menu-open-focus.js'
51
46
 
52
47
  let {
53
48
  property,
@@ -82,15 +77,14 @@
82
77
  })
83
78
 
84
79
  /** 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.
80
+ const openFocus = createMenuOpenFocus()
87
81
  $effect(() => {
88
- if (!open) focusFirst = false
82
+ if (!open) openFocus.reset()
89
83
  })
90
84
  function onTriggerKeydown(e: KeyboardEvent): void {
91
85
  if (e.key !== 'ArrowDown' || e.defaultPrevented || open || !model.enabled) return
92
86
  e.preventDefault()
93
- focusFirst = true
87
+ openFocus.first()
94
88
  open = true
95
89
  }
96
90
 
@@ -164,13 +158,8 @@
164
158
  align="start"
165
159
  sideOffset={4}
166
160
  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
- }}
161
+ onOpenAutoFocus={(e) => openFocus.onOpenAutoFocus(e, menu)}
162
+ onfocusin={openFocus.onfocusin}
174
163
  onkeydown={(e) => {
175
164
  // Nested in another panel, Escape closes this menu alone; Bits would otherwise pick
176
165
  // the layer to close by registration order.
@@ -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;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"}
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"}
@@ -0,0 +1,213 @@
1
+ <script module lang="ts">
2
+ import type { HTMLAttributes } from 'svelte/elements'
3
+
4
+ export type InkFindBarProps = Omit<HTMLAttributes<HTMLDivElement>, 'children'>
5
+ </script>
6
+
7
+ <script lang="ts">
8
+ /*
9
+ * The find bar: a field, the match count, previous and next, a match-case toggle and close,
10
+ * a view of the root's search state. Place it anywhere inside `InkRoot`: it portals itself
11
+ * into `[data-ink-viewport-root]`, the positioned ancestor the theme floats it from over the
12
+ * top-right of the pages (as `InkThreadPopover` anchors to the root's overlay rather than
13
+ * where it is written). It renders nothing until `InkViewport` has mounted, and is hidden
14
+ * while the search is closed. While it is mounted it registers with the search controller,
15
+ * which is what lets Ctrl/Cmd+F open it.
16
+ *
17
+ * Enter in the field steps to the next match, Shift+Enter to the previous, Escape closes.
18
+ * Closing returns focus to where it was before the bar opened, else to the document.
19
+ */
20
+ import { findAnnouncer, findCountText } from '@inkbar/core/dom'
21
+ import { CaseSensitive, ChevronDown, ChevronUp, X } from '@inkbar/icons'
22
+ import { untrack } from 'svelte'
23
+
24
+ import { useInkContext } from '../context.js'
25
+ import { useSearch } from '../hooks/useSearch.svelte.js'
26
+ import { useReadable } from '../use-readable.svelte.js'
27
+
28
+ import InkIcon from './InkIcon.svelte'
29
+
30
+ import type { SearchState } from '@inkbar/core/dom'
31
+
32
+ let { class: className, ...rest }: InkFindBarProps = $props()
33
+
34
+ const ctx = useInkContext('InkFindBar')
35
+ const search = useSearch()
36
+ const scroll = useReadable(() => ctx.viewportEl, null)
37
+ /** The viewport root: the scroll element's parent, once InkViewport has mounted. */
38
+ const target = $derived(scroll.current?.parentElement ?? null)
39
+
40
+ let bar = $state<HTMLDivElement | null>(null)
41
+ let field = $state<HTMLInputElement | null>(null)
42
+ let live = $state<HTMLDivElement | null>(null)
43
+ /** Where focus was when the bar opened, and whether it was open on the last pass. */
44
+ let before: HTMLElement | null = null
45
+ let wasOpen = false
46
+
47
+ const open = $derived(search.state.open)
48
+ const query = $derived(search.state.query)
49
+
50
+ // After the update, so the bar is shown (and can take focus) when it opens.
51
+ $effect(() => {
52
+ const el = bar
53
+ const input = field
54
+ const isOpen = open
55
+ const text = query
56
+ if (!el || !input) return
57
+ untrack(() => {
58
+ if (isOpen !== wasOpen) {
59
+ wasOpen = isOpen
60
+ const doc = el.ownerDocument
61
+ if (isOpen) {
62
+ const active = doc.activeElement
63
+ before =
64
+ active instanceof HTMLElement && active !== doc.body && !el.contains(active)
65
+ ? active
66
+ : null
67
+ input.focus()
68
+ input.select()
69
+ } else {
70
+ // Focus inside the bar is about to be lost with it; focus elsewhere is left alone.
71
+ const hadFocus =
72
+ el.contains(doc.activeElement) || doc.activeElement === doc.body
73
+ if (hadFocus) {
74
+ const to =
75
+ before?.isConnected && !el.contains(before) ? before : scroll.current
76
+ to?.focus({ preventScroll: true })
77
+ }
78
+ before = null
79
+ }
80
+ }
81
+ if (input.value !== text) {
82
+ input.value = text
83
+ // A query set from code arrives after the bar opened: keep the new text selected.
84
+ if (el.ownerDocument.activeElement === input) input.select()
85
+ }
86
+ })
87
+ })
88
+
89
+ // Registered while mounted.
90
+ $effect(() => {
91
+ const controller = ctx.search
92
+ if (!controller || !target) return
93
+ return controller.attachBar()
94
+ })
95
+
96
+ // The live region follows the controller's state change by change, as the vanilla bar does,
97
+ // and is written directly. An update shows only the last of the changes batched into it, so
98
+ // two steps in one task that end where they began would otherwise say nothing.
99
+ $effect(() => {
100
+ const controller = ctx.search
101
+ const el = live
102
+ if (!controller || !el) return
103
+ return untrack(() => {
104
+ const announce = findAnnouncer()
105
+ const say = (s: SearchState): void => {
106
+ const text = announce(s)
107
+ if (el.textContent !== text) el.textContent = text
108
+ }
109
+ say(controller.state.get())
110
+ return controller.state.subscribe(say)
111
+ })
112
+ })
113
+
114
+ // A native listener, so Escape stops at the bar as it does in the vanilla and Vue bars;
115
+ // Svelte's delegated events would stop it only after the root's own listeners saw it.
116
+ $effect(() => {
117
+ const controller = ctx.search
118
+ const el = bar
119
+ if (!controller || !el) return
120
+ const onKeydown = (e: KeyboardEvent): void => {
121
+ if (e.key !== 'Escape' || e.defaultPrevented || e.isComposing) return
122
+ e.preventDefault()
123
+ e.stopPropagation()
124
+ controller.close()
125
+ }
126
+ el.addEventListener('keydown', onKeydown)
127
+ return () => el.removeEventListener('keydown', onKeydown)
128
+ })
129
+
130
+ /**
131
+ * Moves the bar itself into the viewport root, leaving nothing where it is written. Svelte
132
+ * removes it when its own `{#if}` branch goes, but not when an ancestor's block goes and
133
+ * takes only the nodes still in place, so the cleanup removes it too. A keyed `{#each}` that
134
+ * reorders would move the bar back to where it is written, so keep `InkFindBar` out of one.
135
+ */
136
+ const into = (to: Element) => (el: HTMLElement) => {
137
+ to.appendChild(el)
138
+ return () => el.remove()
139
+ }
140
+ </script>
141
+
142
+ {#if target}
143
+ <div
144
+ {...rest}
145
+ bind:this={bar}
146
+ class={['ink-find-bar', className]}
147
+ data-ink-find-bar=""
148
+ role="search"
149
+ hidden={!open}
150
+ {@attach into(target)}
151
+ >
152
+ <input
153
+ bind:this={field}
154
+ type="text"
155
+ aria-label="Find in document"
156
+ autocomplete="off"
157
+ spellcheck={false}
158
+ oninput={(e) => search.setQuery(e.currentTarget.value)}
159
+ onkeydown={(e) => {
160
+ if (e.key !== 'Enter' || e.isComposing) return
161
+ e.preventDefault()
162
+ if (e.shiftKey) search.previous()
163
+ else search.next()
164
+ }}
165
+ />
166
+ <span class="ink-find-count" data-ink-find-count=""
167
+ >{findCountText(search.state)}</span
168
+ >
169
+ <button
170
+ type="button"
171
+ data-ink-find-previous=""
172
+ aria-label="Previous match"
173
+ title="Previous match"
174
+ onclick={() => search.previous()}
175
+ >
176
+ <InkIcon icon={ChevronUp} />
177
+ </button>
178
+ <button
179
+ type="button"
180
+ data-ink-find-next=""
181
+ aria-label="Next match"
182
+ title="Next match"
183
+ onclick={() => search.next()}
184
+ >
185
+ <InkIcon icon={ChevronDown} />
186
+ </button>
187
+ <button
188
+ type="button"
189
+ data-ink-find-match-case=""
190
+ aria-label="Match case"
191
+ title="Match case"
192
+ aria-pressed={search.state.matchCase}
193
+ onclick={() => search.setMatchCase(!search.state.matchCase)}
194
+ >
195
+ <InkIcon icon={CaseSensitive} />
196
+ </button>
197
+ <button
198
+ type="button"
199
+ data-ink-find-close=""
200
+ aria-label="Close find"
201
+ title="Close find"
202
+ onclick={() => search.close()}
203
+ >
204
+ <InkIcon icon={X} />
205
+ </button>
206
+ <div
207
+ bind:this={live}
208
+ class="ink-visually-hidden"
209
+ data-ink-find-status=""
210
+ aria-live="polite"
211
+ ></div>
212
+ </div>
213
+ {/if}
@@ -0,0 +1,6 @@
1
+ import type { HTMLAttributes } from 'svelte/elements';
2
+ export type InkFindBarProps = Omit<HTMLAttributes<HTMLDivElement>, 'children'>;
3
+ declare const InkFindBar: import("svelte").Component<InkFindBarProps, {}, "">;
4
+ type InkFindBar = ReturnType<typeof InkFindBar>;
5
+ export default InkFindBar;
6
+ //# sourceMappingURL=InkFindBar.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"InkFindBar.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/InkFindBar.svelte.ts"],"names":[],"mappings":"AAGE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAErD,MAAM,MAAM,eAAe,GAAG,IAAI,CAAC,cAAc,CAAC,cAAc,CAAC,EAAE,UAAU,CAAC,CAC/E;AAmLD,QAAA,MAAM,UAAU,qDAAwC,CAAC;AACzD,KAAK,UAAU,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;AAChD,eAAe,UAAU,CAAC"}
@@ -19,12 +19,10 @@
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'
@@ -37,6 +35,7 @@
37
35
 
38
36
  import InkColorMenu from './InkColorMenu.svelte'
39
37
  import InkIcon from './InkIcon.svelte'
38
+ import { createMenuOpenFocus } from './menu-open-focus.js'
40
39
 
41
40
  let { class: className, children, ...rest }: InkHighlightMenuProps = $props()
42
41
 
@@ -55,15 +54,14 @@
55
54
  let menu = $state<HTMLElement | null>(null)
56
55
 
57
56
  /** 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.
57
+ const openFocus = createMenuOpenFocus()
60
58
  $effect(() => {
61
- if (!open) focusFirst = false
59
+ if (!open) openFocus.reset()
62
60
  })
63
61
  function onTriggerKeydown(e: KeyboardEvent): void {
64
62
  if (e.key !== 'ArrowDown' || e.defaultPrevented || open) return
65
63
  e.preventDefault()
66
- focusFirst = true
64
+ openFocus.first()
67
65
  open = true
68
66
  }
69
67
 
@@ -128,13 +126,8 @@
128
126
  align="start"
129
127
  sideOffset={4}
130
128
  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
- }}
129
+ onOpenAutoFocus={(e) => openFocus.onOpenAutoFocus(e, menu)}
130
+ onfocusin={openFocus.onfocusin}
138
131
  onkeydowncapture={(e: KeyboardEvent) => menu && handleMenuKeys(e, menu)}
139
132
  >
140
133
  <div role="group" aria-label="Kinds" class="ink-highlight-kinds">
@@ -1 +1 @@
1
- {"version":3,"file":"InkHighlightMenu.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/InkHighlightMenu.svelte.ts"],"names":[],"mappings":"AAGE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAA;AACrC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAErD;;;;;GAKG;AACH,MAAM,WAAW,qBAAsB,SAAQ,IAAI,CACjD,cAAc,CAAC,cAAc,CAAC,EAC9B,UAAU,CACX;IACC,uCAAuC;IACvC,QAAQ,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;CAC/B;AAkHH,QAAA,MAAM,gBAAgB,2DAAwC,CAAC;AAC/D,KAAK,gBAAgB,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAC5D,eAAe,gBAAgB,CAAC"}
1
+ {"version":3,"file":"InkHighlightMenu.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/InkHighlightMenu.svelte.ts"],"names":[],"mappings":"AAGE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAA;AACrC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAErD;;;;;GAKG;AACH,MAAM,WAAW,qBAAsB,SAAQ,IAAI,CACjD,cAAc,CAAC,cAAc,CAAC,EAC9B,UAAU,CACX;IACC,uCAAuC;IACvC,QAAQ,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;CAC/B;AA2GH,QAAA,MAAM,gBAAgB,2DAAwC,CAAC;AAC/D,KAAK,gBAAgB,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAC5D,eAAe,gBAAgB,CAAC"}
@@ -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"}