@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
@@ -11,7 +11,13 @@
11
11
  PdfAssets,
12
12
  SignatureStore,
13
13
  } from '@inkbar/core'
14
- import type { Tool, ToolId, ZoomValue } from '@inkbar/core/dom'
14
+ import type {
15
+ DecorateAnnotation,
16
+ InkPlugin,
17
+ Tool,
18
+ ToolId,
19
+ ZoomValue,
20
+ } from '@inkbar/core/dom'
15
21
  import type { Snippet } from 'svelte'
16
22
  import type { HTMLAttributes } from 'svelte/elements'
17
23
 
@@ -39,6 +45,19 @@
39
45
  * document opens.
40
46
  */
41
47
  tools?: Tool[] | undefined
48
+ /**
49
+ * Plugins, set up once when the root is created; a later change to this prop is not
50
+ * applied. Their tools join `tools`, their decoration and page layers apply to every
51
+ * document, and their commands are placed with `InkCommand` or `useCommand`.
52
+ */
53
+ plugins?: InkPlugin[] | undefined
54
+ /**
55
+ * Adds `data-*` attributes, classes and a screen-reader description to each annotation's
56
+ * element (see `DecorateAnnotation`). Decorations re-apply whenever the annotations change
57
+ * and when this prop changes, without rebuilding the viewer. Pass a new function when its
58
+ * output would change for any other reason.
59
+ */
60
+ decorateAnnotation?: DecorateAnnotation | undefined
42
61
  /**
43
62
  * The font families text boxes offer, replacing the PDF standard families
44
63
  * (`FONT_FAMILIES`). Fonts beyond those are the PDF writer's to embed.
@@ -79,8 +98,11 @@
79
98
  createSnapshotHistory,
80
99
  InkError as InkErrorClass,
81
100
  openDocument,
101
+ readonly,
102
+ writable,
82
103
  } from '@inkbar/core'
83
104
  import {
105
+ createPluginHost,
84
106
  createToolOptions,
85
107
  cssVariableDefaults,
86
108
  FONT_FAMILIES,
@@ -94,7 +116,8 @@
94
116
  import { setInkContext, type InkContext, type InkHistory } from '../context.js'
95
117
  import { useReadable } from '../use-readable.svelte.js'
96
118
 
97
- import type { ToolController, Viewport } from '@inkbar/core/dom'
119
+ import type { PluginHistory } from '@inkbar/core'
120
+ import type { PluginHost, ToolController, Viewport } from '@inkbar/core/dom'
98
121
 
99
122
  let {
100
123
  src,
@@ -107,6 +130,8 @@
107
130
  tool = $bindable(),
108
131
  onToolChange,
109
132
  tools: hostTools,
133
+ plugins,
134
+ decorateAnnotation,
110
135
  fonts,
111
136
  signatures: signatureStore,
112
137
  signatureFonts,
@@ -138,6 +163,7 @@
138
163
  const IDLE: InkStatus = { state: 'idle' }
139
164
  const NO_ANNOTATIONS: readonly Annotation[] = []
140
165
  const NO_IDS: readonly string[] = []
166
+ const noop = (): void => undefined
141
167
 
142
168
  /** Signed URLs change; the key does not. Bytes and Blobs are their own identity. */
143
169
  function identity(source: DocumentSource | null): unknown {
@@ -152,6 +178,8 @@
152
178
  }
153
179
 
154
180
  let document = $state.raw<InkDocument | null>(null)
181
+ /** The same document outside the reactive graph, for the plugins' history. */
182
+ let documentNow: InkDocument | null = null
155
183
  /** Overrides the document's status while opening, and when opening itself failed. */
156
184
  let override = $state.raw<InkStatus | null>(null)
157
185
  let activated = $state<string | null>(null)
@@ -198,6 +226,8 @@
198
226
  const pw = sourcePassword
199
227
  let cancelled = false
200
228
  let opened: InkDocument | null = null
229
+ /** The snapshot history's hold on a controlled document's store. */
230
+ let detach: () => void = noop
201
231
  untrack(() => snapshotHistory.clear())
202
232
  const source = untrack(() => src)
203
233
  if (source === null) {
@@ -241,7 +271,13 @@
241
271
  const current = untrack(() => annotations)
242
272
  if (doc.annotations.mode === 'controlled' && current !== undefined)
243
273
  doc.annotations.setControlledValue(current)
274
+ documentNow = doc
244
275
  document = doc
276
+ // Controlled edits run the plugins' reactions inside their own snapshot step.
277
+ if (doc.annotations.mode === 'controlled')
278
+ detach = snapshotHistory.attach(doc.annotations)
279
+ // The document is current, so the host reads its history when it hears of it.
280
+ hostDocument.set(doc)
245
281
  override = null
246
282
  setTestHook(ref, doc)
247
283
  const s = doc.status.get()
@@ -264,7 +300,11 @@
264
300
  })()
265
301
  return () => {
266
302
  cancelled = true
303
+ // Cleared first, so the plugins close their stores while the document is still alive.
304
+ hostDocument.set(null)
305
+ detach()
267
306
  opened?.destroy()
307
+ documentNow = null
268
308
  document = null
269
309
  setTestHook(ref, undefined)
270
310
  }
@@ -279,12 +319,15 @@
279
319
  })
280
320
 
281
321
  function setTool(id: ToolId): void {
322
+ // Picking the active tool again starts it afresh, as after a one-shot tool's use.
323
+ if (currentTool === id && tools?.active.get() === id) tools.set(id)
282
324
  tool = id
283
325
  onToolChange?.(id)
284
326
  }
285
327
  function setActivated(id: string | null): void {
286
328
  activated = id
287
329
  activatedNow = id
330
+ hostActivated.set(id)
288
331
  if (id) onActivate?.(id)
289
332
  }
290
333
  function reportZoom(z: ZoomValue): void {
@@ -315,8 +358,65 @@
315
358
  else if (document) document.history.batch(fn)
316
359
  else fn()
317
360
  },
361
+ track: (store) => pluginHistory.track(store),
362
+ react: (fn) => pluginHistory.react(fn),
318
363
  }
319
364
 
365
+ /**
366
+ * The history the plugins reach, picked by the open document's mode when they call. The host
367
+ * reads it as soon as a document opens, so it goes by `documentNow`, which is set by then.
368
+ */
369
+ const activeHistory = (): PluginHistory | undefined => {
370
+ const doc = documentNow
371
+ if (!doc) return undefined
372
+ return doc.annotations.mode === 'controlled' ? snapshotHistory : doc.history
373
+ }
374
+ const pluginHistory: PluginHistory = {
375
+ batch: (fn) => {
376
+ const h = activeHistory()
377
+ if (h) h.batch(fn)
378
+ else fn()
379
+ },
380
+ track: (store) => activeHistory()?.track(store) ?? noop,
381
+ react: (fn) => activeHistory()?.react(fn) ?? noop,
382
+ }
383
+
384
+ /*
385
+ * One plugin host for the root's life, built in an effect so that no `setup` runs on the
386
+ * server, where effects never run and nothing would clean it up. Until then `pluginHost` is
387
+ * null and InkCommand renders its command disabled. The open effect above runs first, but
388
+ * the document it opens arrives after an await, so the host is in place by then. It follows
389
+ * these writables rather than the root's state: the open effect sets `hostDocument` once
390
+ * `documentNow` is current.
391
+ */
392
+ const hostDocument = writable<InkDocument | null>(null)
393
+ const hostActivated = writable<string | null>(null)
394
+ const viewportEl = writable<HTMLElement | null>(null)
395
+ let pluginHost = $state.raw<PluginHost | null>(null)
396
+ $effect(() => {
397
+ const host = untrack(() =>
398
+ createPluginHost({
399
+ // Read once, as documented on the prop.
400
+ plugins: plugins ?? [],
401
+ document: readonly(hostDocument),
402
+ history: () => pluginHistory,
403
+ activated: readonly(hostActivated),
404
+ // As in `createViewer`: an id not in the open document is ignored.
405
+ setActivated: (id) => {
406
+ if (id === null || documentNow?.annotations.get(id)) setActivated(id)
407
+ },
408
+ root: readonly(viewportEl),
409
+ decorate: () => decorateAnnotation,
410
+ viewport: () => viewport,
411
+ })
412
+ )
413
+ pluginHost = host
414
+ // Destroyed after InkViewport's engine and its plugin layers, as in `createViewer`: Svelte
415
+ // creates a component's effects after its template and tears them down in creation order,
416
+ // so the children's teardowns run first.
417
+ return () => host.destroy()
418
+ })
419
+
320
420
  /** The document's tool style; it batches through `inkHistory`, whichever mode is on. */
321
421
  const styleTracker = $derived.by(() => {
322
422
  const doc = document
@@ -325,7 +425,8 @@
325
425
  trackToolStyle({
326
426
  store: doc.annotations,
327
427
  history: { batch: (fn) => inkHistory.batch(fn) },
328
- tools: hostTools,
428
+ // The plugins' tools too, so a plugin tool's style group applies.
429
+ tools: [...(hostTools ?? []), ...(pluginHost?.tools ?? [])],
329
430
  tool: currentTool,
330
431
  })
331
432
  )
@@ -351,6 +452,10 @@
351
452
  get selection() {
352
453
  return selection.current
353
454
  },
455
+ get pluginHost() {
456
+ return pluginHost
457
+ },
458
+ viewportEl,
354
459
  get viewport() {
355
460
  return viewport
356
461
  },
@@ -364,6 +469,9 @@
364
469
  get hostTools() {
365
470
  return hostTools
366
471
  },
472
+ get decorateAnnotation() {
473
+ return decorateAnnotation
474
+ },
367
475
  get fonts() {
368
476
  return fonts ?? FONT_FAMILIES
369
477
  },
@@ -1,6 +1,6 @@
1
1
  import type { ChildSnippet } from './child-snippet.js';
2
2
  import type { Annotation, AnnotationChange, AnnotationDefaults, DocumentSource, InkDocument, InkError, InkStatus, PdfAssets, SignatureStore } from '@inkbar/core';
3
- import type { Tool, ToolId, ZoomValue } from '@inkbar/core/dom';
3
+ import type { DecorateAnnotation, InkPlugin, Tool, ToolId, ZoomValue } from '@inkbar/core/dom';
4
4
  import type { Snippet } from 'svelte';
5
5
  import type { HTMLAttributes } from 'svelte/elements';
6
6
  export interface InkRootProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children' | 'onchange' | 'onerror'> {
@@ -24,6 +24,19 @@ export interface InkRootProps extends Omit<HTMLAttributes<HTMLDivElement>, 'chil
24
24
  * document opens.
25
25
  */
26
26
  tools?: Tool[] | undefined;
27
+ /**
28
+ * Plugins, set up once when the root is created; a later change to this prop is not
29
+ * applied. Their tools join `tools`, their decoration and page layers apply to every
30
+ * document, and their commands are placed with `InkCommand` or `useCommand`.
31
+ */
32
+ plugins?: InkPlugin[] | undefined;
33
+ /**
34
+ * Adds `data-*` attributes, classes and a screen-reader description to each annotation's
35
+ * element (see `DecorateAnnotation`). Decorations re-apply whenever the annotations change
36
+ * and when this prop changes, without rebuilding the viewer. Pass a new function when its
37
+ * output would change for any other reason.
38
+ */
39
+ decorateAnnotation?: DecorateAnnotation | undefined;
27
40
  /**
28
41
  * The font families text boxes offer, replacing the PDF standard families
29
42
  * (`FONT_FAMILIES`). Fonts beyond those are the PDF writer's to embed.
@@ -1 +1 @@
1
- {"version":3,"file":"InkRoot.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/InkRoot.svelte.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"InkRoot.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/InkRoot.svelte.ts"],"names":[],"mappings":"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"}
@@ -18,9 +18,7 @@
18
18
 
19
19
  <script lang="ts">
20
20
  import {
21
- focusInitialMenuItem,
22
21
  handleMenuKeys,
23
- menuItems,
24
22
  SHAPE_COLUMNS,
25
23
  SHAPE_ICONS,
26
24
  SHAPE_KINDS,
@@ -35,12 +33,15 @@
35
33
  import { useToolOptions } from '../hooks/useToolOptions.svelte.js'
36
34
 
37
35
  import InkIcon from './InkIcon.svelte'
36
+ import { createMenuOpenFocus } from './menu-open-focus.js'
38
37
 
39
38
  let { class: className, children, ...rest }: InkShapeMenuProps = $props()
40
39
 
41
40
  const current = useTool()
42
41
  const shape = useToolOptions<ShapeOptions>('shape')
43
42
  let open = $state(false)
43
+ /** The split button: the panel lines up under it rather than the chevron. */
44
+ let group = $state<HTMLElement | null>(null)
44
45
 
45
46
  const model = $derived(shapeMenuModel(shape.options, SHAPE_KINDS))
46
47
  const active = $derived(current.tool === 'shape')
@@ -48,15 +49,14 @@
48
49
  let menu = $state<HTMLElement | null>(null)
49
50
 
50
51
  /** ArrowDown opens on the first item; a click opens on the checked one. */
51
- let focusFirst = false
52
- // Bits runs the open-focus callback more than once, so the flag outlives it; reset on close.
52
+ const openFocus = createMenuOpenFocus()
53
53
  $effect(() => {
54
- if (!open) focusFirst = false
54
+ if (!open) openFocus.reset()
55
55
  })
56
56
  function onTriggerKeydown(e: KeyboardEvent): void {
57
57
  if (e.key !== 'ArrowDown' || e.defaultPrevented || open) return
58
58
  e.preventDefault()
59
- focusFirst = true
59
+ openFocus.first()
60
60
  open = true
61
61
  }
62
62
 
@@ -68,6 +68,7 @@
68
68
  </script>
69
69
 
70
70
  <div
71
+ bind:this={group}
71
72
  role="group"
72
73
  aria-label="Shape"
73
74
  class={['ink-split-button ink-shape-menu', className]}
@@ -115,17 +116,13 @@
115
116
  class="ink-select-content ink-menu-content"
116
117
  data-ink-select-content=""
117
118
  data-ink-menu="shape"
119
+ customAnchor={group}
118
120
  side="bottom"
119
121
  align="start"
120
122
  sideOffset={4}
121
123
  collisionPadding={8}
122
- onOpenAutoFocus={(e) => {
123
- e.preventDefault()
124
- if (menu) {
125
- if (focusFirst) menuItems(menu)[0]?.focus({ preventScroll: true })
126
- else focusInitialMenuItem(menu)
127
- }
128
- }}
124
+ onOpenAutoFocus={(e) => openFocus.onOpenAutoFocus(e, menu)}
125
+ onfocusin={openFocus.onfocusin}
129
126
  onkeydown={(e) => menu && handleMenuKeys(e, menu)}
130
127
  >
131
128
  <div
@@ -1 +1 @@
1
- {"version":3,"file":"InkShapeMenu.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/InkShapeMenu.svelte.ts"],"names":[],"mappings":"AAGE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAA;AACrC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAErD;;;;GAIG;AACH,MAAM,WAAW,iBAAkB,SAAQ,IAAI,CAC7C,cAAc,CAAC,cAAc,CAAC,EAC9B,UAAU,CACX;IACC,uCAAuC;IACvC,QAAQ,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;CAC/B;AAoGH,QAAA,MAAM,YAAY,uDAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
1
+ {"version":3,"file":"InkShapeMenu.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/InkShapeMenu.svelte.ts"],"names":[],"mappings":"AAGE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAA;AACrC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAErD;;;;GAIG;AACH,MAAM,WAAW,iBAAkB,SAAQ,IAAI,CAC7C,cAAc,CAAC,cAAc,CAAC,EAC9B,UAAU,CACX;IACC,uCAAuC;IACvC,QAAQ,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;CAC/B;AA+FH,QAAA,MAAM,YAAY,uDAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
@@ -21,9 +21,7 @@
21
21
 
22
22
  <script lang="ts">
23
23
  import {
24
- focusInitialMenuItem,
25
24
  handleMenuKeys,
26
- menuItems,
27
25
  SIGNATURE_LABELS,
28
26
  signatureMenuModel,
29
27
  signaturePreview,
@@ -39,6 +37,7 @@
39
37
 
40
38
  import InkIcon from './InkIcon.svelte'
41
39
  import InkSignatureDialog from './InkSignatureDialog.svelte'
40
+ import { createMenuOpenFocus } from './menu-open-focus.js'
42
41
 
43
42
  import type { SignatureOptions, SignatureTemplate } from '@inkbar/core'
44
43
 
@@ -58,15 +57,16 @@
58
57
  const model = $derived(signatureMenuModel(templates.current, signature.options))
59
58
  const active = $derived(current.tool === 'signature')
60
59
  let open = $state(false)
60
+ /** The split button: the panel lines up under it rather than the chevron. */
61
+ let group = $state<HTMLElement | null>(null)
61
62
  let creating = $state(false)
62
63
  let menu = $state<HTMLElement | null>(null)
63
64
  let main = $state<HTMLElement | null>(null)
64
65
 
65
66
  /** ArrowDown opens on the first item; a click opens on the checked one. */
66
- let focusFirst = false
67
- // Bits runs the open-focus callback more than once, so the flag outlives it; reset on close.
67
+ const openFocus = createMenuOpenFocus()
68
68
  $effect(() => {
69
- if (!open) focusFirst = false
69
+ if (!open) openFocus.reset()
70
70
  })
71
71
  /** Set while the menu closes for the dialog, which takes focus instead of the trigger. */
72
72
  let toDialog = false
@@ -74,7 +74,7 @@
74
74
  function onTriggerKeydown(e: KeyboardEvent): void {
75
75
  if (e.key !== 'ArrowDown' || e.defaultPrevented || open) return
76
76
  e.preventDefault()
77
- focusFirst = true
77
+ openFocus.first()
78
78
  open = true
79
79
  }
80
80
 
@@ -115,6 +115,7 @@
115
115
  </script>
116
116
 
117
117
  <div
118
+ bind:this={group}
118
119
  role="group"
119
120
  aria-label={SIGNATURE_LABELS.sign}
120
121
  class={['ink-split-button ink-signature-menu', className]}
@@ -160,17 +161,13 @@
160
161
  class="ink-select-content ink-menu-content"
161
162
  data-ink-select-content=""
162
163
  data-ink-menu="signature"
164
+ customAnchor={group}
163
165
  side="bottom"
164
166
  align="start"
165
167
  sideOffset={4}
166
168
  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
- }}
169
+ onOpenAutoFocus={(e) => openFocus.onOpenAutoFocus(e, menu)}
170
+ onfocusin={openFocus.onfocusin}
174
171
  onCloseAutoFocus={(e) => {
175
172
  if (toDialog) e.preventDefault()
176
173
  toDialog = false
@@ -1 +1 @@
1
- {"version":3,"file":"InkSignatureMenu.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/InkSignatureMenu.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;IAC9B,mFAAmF;IACnF,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,CAAA;CACtC;AAuLH,QAAA,MAAM,gBAAgB,2DAAwC,CAAC;AAC/D,KAAK,gBAAgB,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAC5D,eAAe,gBAAgB,CAAC"}
1
+ {"version":3,"file":"InkSignatureMenu.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/InkSignatureMenu.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;IAC9B,mFAAmF;IACnF,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,CAAA;CACtC;AAkLH,QAAA,MAAM,gBAAgB,2DAAwC,CAAC;AAC/D,KAAK,gBAAgB,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAC5D,eAAe,gBAAgB,CAAC"}
@@ -137,6 +137,13 @@
137
137
  }
138
138
  </script>
139
139
 
140
+ <svelte:document
141
+ onpointerdowncapture={(e) =>
142
+ open && focusReturn.onPressOutside(e, '[data-ink-thread-popover]', close)}
143
+ onpointerup={focusReturn.onPressEnd}
144
+ onpointercancel={focusReturn.onPressEnd}
145
+ />
146
+
140
147
  <Popover.Root bind:open={() => open, onOpenChange}>
141
148
  <Popover.Portal>
142
149
  {#if note && anchor}
@@ -1 +1 @@
1
- {"version":3,"file":"InkThreadPopover.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/InkThreadPopover.svelte.ts"],"names":[],"mappings":"AAGE,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,kBAAkB,CAAA;AAC9D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAA;AAErC,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,EAAE,OAAO,CAAC,CAAC,sBAAsB,CAAC,CAAC,GAAG,SAAS,CAAA;CACzD;AA6LH,QAAA,MAAM,gBAAgB,2DAAwC,CAAC;AAC/D,KAAK,gBAAgB,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAC5D,eAAe,gBAAgB,CAAC"}
1
+ {"version":3,"file":"InkThreadPopover.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/InkThreadPopover.svelte.ts"],"names":[],"mappings":"AAGE,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,kBAAkB,CAAA;AAC9D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAA;AAErC,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,EAAE,OAAO,CAAC,CAAC,sBAAsB,CAAC,CAAC,GAAG,SAAS,CAAA;CACzD;AAgMH,QAAA,MAAM,gBAAgB,2DAAwC,CAAC;AAC/D,KAAK,gBAAgB,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAC5D,eAAe,gBAAgB,CAAC"}
@@ -27,6 +27,7 @@
27
27
  } from '@inkbar/icons'
28
28
  import { Toolbar } from 'bits-ui'
29
29
 
30
+ import { useInkContext } from '../context.js'
30
31
  import { useTool } from '../hooks/useTool.svelte.js'
31
32
 
32
33
  import { asBitsProps } from './child-snippet.js'
@@ -56,11 +57,18 @@
56
57
  ...rest
57
58
  }: InkToolButtonProps = $props()
58
59
 
60
+ const ctx = useInkContext('InkToolButton')
59
61
  const current = useTool()
60
62
  const active = $derived(current.tool === tool)
61
- // A host tool has no built-in name or icon: `label` and `children` supply them.
62
- const name = $derived(label ?? TOOL_LABELS[tool] ?? tool)
63
- const icon = $derived(ICONS[tool])
63
+ /** A host or plugin tool by this id, for the name and icon core has none for. */
64
+ const own = $derived(
65
+ [...(ctx.hostTools ?? []), ...(ctx.pluginHost?.tools ?? [])].find(
66
+ (t) => t.id === tool
67
+ )
68
+ )
69
+ // `label` and `children` win; then core's name and icon; then the tool's own.
70
+ const name = $derived(label ?? TOOL_LABELS[tool] ?? own?.label ?? tool)
71
+ const icon = $derived(ICONS[tool] ?? own?.icon)
64
72
  </script>
65
73
 
66
74
  <Toolbar.Button
@@ -1 +1 @@
1
- {"version":3,"file":"InkToolButton.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/InkToolButton.svelte.ts"],"names":[],"mappings":"AAGE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AACtD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AAC9C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAA;AACrC,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAA;AAE3D,MAAM,WAAW,kBAAmB,SAAQ,IAAI,CAAC,oBAAoB,EAAE,UAAU,CAAC;IAChF,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAC1B,QAAQ,CAAC,EAAE,OAAO,CAAC,CAAC;QAAE,MAAM,EAAE,OAAO,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC,GAAG,SAAS,CAAA;IACpE,KAAK,CAAC,EAAE,YAAY,GAAG,SAAS,CAAA;CACjC;AA4EH,QAAA,MAAM,aAAa,wDAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
1
+ {"version":3,"file":"InkToolButton.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/InkToolButton.svelte.ts"],"names":[],"mappings":"AAGE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AACtD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AAC9C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAA;AACrC,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAA;AAE3D,MAAM,WAAW,kBAAmB,SAAQ,IAAI,CAAC,oBAAoB,EAAE,UAAU,CAAC;IAChF,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAC1B,QAAQ,CAAC,EAAE,OAAO,CAAC,CAAC;QAAE,MAAM,EAAE,OAAO,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC,GAAG,SAAS,CAAA;IACpE,KAAK,CAAC,EAAE,YAAY,GAAG,SAAS,CAAA;CACjC;AAqFH,QAAA,MAAM,aAAa,wDAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
@@ -43,7 +43,7 @@
43
43
  import InkPage from './InkPage.svelte'
44
44
 
45
45
  import type { InkDocument, InkStatus } from '@inkbar/core'
46
- import type { PageMount, ViewerEngine, ZoomValue } from '@inkbar/core/dom'
46
+ import type { PageMount, ToolId, ViewerEngine, ZoomValue } from '@inkbar/core/dom'
47
47
 
48
48
  let {
49
49
  mode = 'continuous',
@@ -126,6 +126,11 @@
126
126
  buffer,
127
127
  tool: ctx.tool,
128
128
  ...(ctx.hostTools ? { tools: ctx.hostTools } : {}),
129
+ // The engine adds the plugins' tools and layers, and decorates through the host, which
130
+ // reads `decorateAnnotation` first.
131
+ ...(ctx.pluginHost ? { plugins: ctx.pluginHost } : {}),
132
+ // Read through, so the effect below can re-apply a new function without a rebuild.
133
+ decorateAnnotation: (a) => ctx.decorateAnnotation?.(a),
129
134
  ...(ctx.toolStyle ? { toolStyle: ctx.toolStyle } : {}),
130
135
  toolHistory: { batch: (fn) => ctx.history.batch(fn) },
131
136
  toolOptions: ctx.toolOptions,
@@ -155,7 +160,15 @@
155
160
  lastReportedPage = p
156
161
  ctx.reportPage(p)
157
162
  }
158
- stops = [vp.zoom.subscribe(reportZoom), vp.currentPage.subscribe(reportPage)]
163
+ // A tool that switches itself (a one-shot tool once used) is reported as a pick.
164
+ const followTool = (id: ToolId): void => {
165
+ if (id !== ctx.tool) ctx.setTool(id)
166
+ }
167
+ stops = [
168
+ vp.zoom.subscribe(reportZoom),
169
+ vp.currentPage.subscribe(reportPage),
170
+ tc.active.subscribe(followTool),
171
+ ]
159
172
  // Whatever the build restored is reported once; nothing is when it matched.
160
173
  reportZoom(vp.zoom.get())
161
174
  reportPage(vp.currentPage.get())
@@ -181,6 +194,16 @@
181
194
  })
182
195
  }
183
196
 
197
+ // The plugins' `root`. Before the build below, so it is set when the engine mounts.
198
+ $effect(() => {
199
+ const el = scroll
200
+ const { viewportEl } = ctx
201
+ viewportEl.set(el)
202
+ return () => {
203
+ if (viewportEl.get() === el) viewportEl.set(null)
204
+ }
205
+ })
206
+
184
207
  // Build for a new document or status; tear down on the next change and on unmount. Only the
185
208
  // values read before `untrack` are dependencies, each through a derived, which notifies only
186
209
  // when the value changes: the status is a new object on every engine report.
@@ -229,6 +252,13 @@
229
252
  })
230
253
  })
231
254
 
255
+ // A new decorateAnnotation re-applies to the mounted annotation elements at once.
256
+ const decorate = $derived(ctx.decorateAnnotation)
257
+ $effect(() => {
258
+ void decorate
259
+ untrack(() => engine?.redecorate())
260
+ })
261
+
232
262
  // The setters below run untracked: core relays out inside them and the engine's page
233
263
  // callbacks read and write `pages`. Tracked, each effect would depend on the list it changes
234
264
  // and run again, without end when the host holds a zoom the viewport clamps.
@@ -1 +1 @@
1
- {"version":3,"file":"InkViewport.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/InkViewport.svelte.ts"],"names":[],"mappings":"AAGE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAA;AAC5C,OAAO,KAAK,EAAE,mBAAmB,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAA;AACrF,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAA;AACrC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAErD,MAAM,WAAW,gBAAiB,SAAQ,IAAI,CAC5C,cAAc,CAAC,cAAc,CAAC,EAC9B,UAAU,CACX;IACC,IAAI,CAAC,EAAE,UAAU,CAAA;IACjB,QAAQ,CAAC,EAAE,YAAY,CAAA;IACvB,iCAAiC;IACjC,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,0DAA0D;IAC1D,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,OAAO,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;IAC7B,KAAK,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;IAC3B,KAAK,CAAC,EAAE,OAAO,CAAC,CAAC;QAAE,KAAK,EAAE,QAAQ,CAAA;KAAE,CAAC,CAAC,GAAG,SAAS,CAAA;IAClD,UAAU,CAAC,EAAE,OAAO,CAAC,CAAC,mBAAmB,CAAC,CAAC,GAAG,SAAS,CAAA;IACvD,wFAAwF;IACxF,SAAS,CAAC,EAAE,OAAO,CAAC,CAAC;QAAE,KAAK,EAAE,QAAQ,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC,GAAG,SAAS,CAAA;CACrE;AA8RH,QAAA,MAAM,WAAW,sDAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}
1
+ {"version":3,"file":"InkViewport.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/InkViewport.svelte.ts"],"names":[],"mappings":"AAGE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAA;AAC5C,OAAO,KAAK,EAAE,mBAAmB,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAA;AACrF,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAA;AACrC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAErD,MAAM,WAAW,gBAAiB,SAAQ,IAAI,CAC5C,cAAc,CAAC,cAAc,CAAC,EAC9B,UAAU,CACX;IACC,IAAI,CAAC,EAAE,UAAU,CAAA;IACjB,QAAQ,CAAC,EAAE,YAAY,CAAA;IACvB,iCAAiC;IACjC,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,0DAA0D;IAC1D,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,OAAO,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;IAC7B,KAAK,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;IAC3B,KAAK,CAAC,EAAE,OAAO,CAAC,CAAC;QAAE,KAAK,EAAE,QAAQ,CAAA;KAAE,CAAC,CAAC,GAAG,SAAS,CAAA;IAClD,UAAU,CAAC,EAAE,OAAO,CAAC,CAAC,mBAAmB,CAAC,CAAC,GAAG,SAAS,CAAA;IACvD,wFAAwF;IACxF,SAAS,CAAC,EAAE,OAAO,CAAC,CAAC;QAAE,KAAK,EAAE,QAAQ,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC,GAAG,SAAS,CAAA;CACrE;AA4TH,QAAA,MAAM,WAAW,sDAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}
@@ -0,0 +1,12 @@
1
+ export interface MenuOpenFocus {
2
+ /** Makes the next open start on the first item (ArrowDown on the trigger). */
3
+ first: () => void;
4
+ /** For the content's `onOpenAutoFocus`. */
5
+ onOpenAutoFocus: (e: Event, menu: HTMLElement | null) => void;
6
+ /** For the content's `onfocusin`: remembers where the user is. */
7
+ onfocusin: (e: FocusEvent) => void;
8
+ /** Call when the menu closes. */
9
+ reset: () => void;
10
+ }
11
+ export declare function createMenuOpenFocus(): MenuOpenFocus;
12
+ //# sourceMappingURL=menu-open-focus.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"menu-open-focus.d.ts","sourceRoot":"","sources":["../../src/lib/components/menu-open-focus.ts"],"names":[],"mappings":"AAQA,MAAM,WAAW,aAAa;IAC5B,8EAA8E;IAC9E,KAAK,EAAE,MAAM,IAAI,CAAA;IACjB,2CAA2C;IAC3C,eAAe,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,WAAW,GAAG,IAAI,KAAK,IAAI,CAAA;IAC7D,kEAAkE;IAClE,SAAS,EAAE,CAAC,CAAC,EAAE,UAAU,KAAK,IAAI,CAAA;IAClC,iCAAiC;IACjC,KAAK,EAAE,MAAM,IAAI,CAAA;CAClB;AAED,wBAAgB,mBAAmB,IAAI,aAAa,CAsBnD"}
@@ -0,0 +1,35 @@
1
+ /*
2
+ * Where a toolbar menu puts focus as it opens: the first item when ArrowDown opened it, else the
3
+ * checked one. Bits can remount a popover's focus scope just after it opens, which sends focus
4
+ * to the trigger and runs the open-focus callback again; by then the user may have moved on, so
5
+ * a repeat run goes back to the item they last focused.
6
+ */
7
+ import { focusInitialMenuItem, menuItems } from '@inkbar/core/dom';
8
+ export function createMenuOpenFocus() {
9
+ let first = false;
10
+ let last = null;
11
+ return {
12
+ first: () => {
13
+ first = true;
14
+ },
15
+ onOpenAutoFocus: (e, menu) => {
16
+ e.preventDefault();
17
+ if (!menu)
18
+ return;
19
+ if (last && menu.contains(last))
20
+ last.focus({ preventScroll: true });
21
+ else if (first)
22
+ menuItems(menu)[0]?.focus({ preventScroll: true });
23
+ else
24
+ focusInitialMenuItem(menu);
25
+ },
26
+ onfocusin: (e) => {
27
+ if (e.target instanceof HTMLElement)
28
+ last = e.target;
29
+ },
30
+ reset: () => {
31
+ first = false;
32
+ last = null;
33
+ },
34
+ };
35
+ }
@@ -5,6 +5,18 @@ export interface PopoverFocusReturn {
5
5
  beforeRemove: FocusReturn['beforeRemove'];
6
6
  /** Bits UI's `onOpenAutoFocus` handler. Leaves the default alone: focus moves into the popover. */
7
7
  onOpenAutoFocus: (event: Event) => void;
8
+ /**
9
+ * A document-level pointer press, captured. `inside` selects the popover's content. A press on
10
+ * the viewer's own scroll element, outside the popover, that will not activate an annotation
11
+ * closes it now, through `close`, before the press moves focus. Bits UI closes it a beat
12
+ * later, and its focus trap pulls focus back into the popover in between, which ends a text
13
+ * selection the press has just begun. Nothing is focused when it closes this way: the press
14
+ * places focus. Every other press (a toolbar, a host's menu portaled out of the popover) is
15
+ * left to Bits UI, whose layer stack knows what belongs to the popover.
16
+ */
17
+ onPressOutside: (event: PointerEvent, inside: string, close: () => void) => void;
18
+ /** A pointer release or cancel, captured. */
19
+ onPressEnd: () => void;
8
20
  /** Bits UI's `onCloseAutoFocus` handler. Prevents the default; the planner places focus. */
9
21
  onCloseAutoFocus: (event: Event, anchor: HTMLElement | null) => void;
10
22
  }
@@ -21,8 +33,10 @@ export declare function keepOpenOnAnnotation(event: PointerEvent): void;
21
33
  * close reaches the planner one microtask late, and only when no open was reported after it.
22
34
  *
23
35
  * This relies on Bits UI reporting a remount as a close and then an open within one synchronous
24
- * call (observed in bits-ui 2.19.5, `FocusScope.mount`). If that changes, the browser case 'a
25
- * card opened by selection alone takes focus without passing it through the annotation' fails.
36
+ * call (observed in bits-ui 2.19.5, `FocusScope.mount`). The unit case 'a close followed by an
37
+ * open in the same tick moves no focus and uses up the removal plan' pins the planner's side, and
38
+ * the browser case 'a popover opened without passing focus through the annotation takes focus'
39
+ * checks that an open moves focus straight into the popover.
26
40
  *
27
41
  * A close that is superseded this way moves no focus, but it still uses up whatever
28
42
  * `beforeRemove` planned for it, so the plan cannot leak into a later close. A close that is
@@ -1 +1 @@
1
- {"version":3,"file":"popover-focus.d.ts","sourceRoot":"","sources":["../../src/lib/components/popover-focus.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,WAAW,EACjB,MAAM,kBAAkB,CAAA;AAIzB,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AAE/C,iFAAiF;AACjF,MAAM,WAAW,kBAAkB;IACjC,YAAY,EAAE,WAAW,CAAC,cAAc,CAAC,CAAA;IACzC,mGAAmG;IACnG,eAAe,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAA;IACvC,4FAA4F;IAC5F,gBAAgB,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,GAAG,IAAI,KAAK,IAAI,CAAA;CACrE;AAED,8GAA8G;AAC9G,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,YAAY,GAAG,IAAI,CAE9D;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,UAAU,GAAG,kBAAkB,CAkClE"}
1
+ {"version":3,"file":"popover-focus.d.ts","sourceRoot":"","sources":["../../src/lib/components/popover-focus.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,WAAW,EACjB,MAAM,kBAAkB,CAAA;AAKzB,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AAE/C,iFAAiF;AACjF,MAAM,WAAW,kBAAkB;IACjC,YAAY,EAAE,WAAW,CAAC,cAAc,CAAC,CAAA;IACzC,mGAAmG;IACnG,eAAe,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAA;IACvC;;;;;;;;OAQG;IACH,cAAc,EAAE,CAAC,KAAK,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,IAAI,KAAK,IAAI,CAAA;IAChF,6CAA6C;IAC7C,UAAU,EAAE,MAAM,IAAI,CAAA;IACtB,4FAA4F;IAC5F,gBAAgB,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,GAAG,IAAI,KAAK,IAAI,CAAA;CACrE;AAED,8GAA8G;AAC9G,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,YAAY,GAAG,IAAI,CAE9D;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,UAAU,GAAG,kBAAkB,CAiDlE"}