markstream-svelte 2.0.12 → 2.0.13

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.
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import type { SvelteRenderableNode, SvelteRenderContext } from './shared/node-helpers'
3
+ import { createPanGesture } from 'markstream-core'
3
4
  import { onDestroy, onMount, tick, untrack } from 'svelte'
4
5
  import { useSafeI18n } from '../i18n/useSafeI18n'
5
6
  import { getInfographic } from '../optional/infographic'
@@ -65,6 +66,9 @@
65
66
  let modalOpen = $state(false)
66
67
  let modalMarkup = $state('')
67
68
  let zoom = $state(1)
69
+ let translateX = $state(0)
70
+ let translateY = $state(0)
71
+ let isDragging = $state(false)
68
72
  let copyTimer: ReturnType<typeof setTimeout> | null = null
69
73
 
70
74
  let source = $derived(getString((node as any)?.code))
@@ -82,10 +86,18 @@
82
86
  let previewStyle = $derived([
83
87
  `min-height: ${previewHeight}px`,
84
88
  maxHeight && maxHeight !== 'none' ? `max-height: ${maxHeight}` : '',
89
+ `cursor: ${isDragging ? 'grabbing' : 'grab'}`,
85
90
  ].filter(Boolean).join('; '))
86
- let transformStyle = $derived(`transform: scale(${zoom}); transform-origin: center center;`)
91
+ let transformStyle = $derived(`transform: translate(${translateX}px, ${translateY}px) scale(${zoom}); transform-origin: center center;`)
87
92
  let shouldRender = $derived(!(resolvedLoading && !source.trim()))
88
93
 
94
+ // The diagram is scaled to fit the preview box, so at fit zoom nothing is
95
+ // off-screen and panning only shifts it inside the box. Touch therefore keeps
96
+ // scrolling the page at that zoom (a chat is mostly scrolling) and only claims
97
+ // the gesture once the diagram is zoomed in and actually overflows the box.
98
+ // The fullscreen modal always claims it.
99
+ let panSurfaceClaimsTouch = $derived(modalOpen || zoom > 1)
100
+
89
101
  onMount(() => {
90
102
  mounted = true
91
103
  queueInfographicRender(true)
@@ -107,6 +119,8 @@
107
119
  renderToken += 1
108
120
  destroyInstance()
109
121
  lastCompletedRenderSignature = ''
122
+ // A drag in progress must not keep its window listeners after unmount.
123
+ panGesture.stop()
110
124
  if (copyTimer)
111
125
  clearTimeout(copyTimer)
112
126
  })
@@ -343,6 +357,39 @@
343
357
  zoom = Math.max(0.5, Math.round((zoom - 0.1) * 10) / 10)
344
358
  }
345
359
 
360
+ function resetZoom() {
361
+ zoom = 1
362
+ translateX = 0
363
+ translateY = 0
364
+ }
365
+
366
+ // Drag functionality. The gesture lifecycle (window listeners, pointer identity,
367
+ // lost releases) lives in markstream-core so every package pans identically.
368
+ const panGesture = createPanGesture({
369
+ getTranslate: () => ({ x: translateX, y: translateY }),
370
+ setTranslate: (next) => {
371
+ translateX = next.x
372
+ translateY = next.y
373
+ },
374
+ canStart: (event) => {
375
+ // Nothing to pan until the diagram exists. Without this the pending/error
376
+ // source panel the block renders in the preview would be claimed by the
377
+ // gesture, and `preventDefault` would take text selection away from it.
378
+ if (!hasPreview)
379
+ return false
380
+
381
+ // Touch pans only where the surface claims the gesture. Elsewhere the page
382
+ // keeps the swipe for scrolling, and panning along with it would shift the
383
+ // diagram by a few pixels before the browser takes the gesture over.
384
+ return event.pointerType !== 'touch' || panSurfaceClaimsTouch
385
+ },
386
+ onActiveChange: active => (isDragging = active),
387
+ })
388
+
389
+ function startDrag(event: PointerEvent) {
390
+ panGesture.start(event)
391
+ }
392
+
346
393
  function showButtonTooltip(event: MouseEvent | FocusEvent, text: string) {
347
394
  const target = event.currentTarget as HTMLElement | null
348
395
  if (!target || (target instanceof HTMLButtonElement && target.disabled))
@@ -453,10 +500,11 @@
453
500
  <button type="button" class="markstream-svelte-enhanced-block__action markstream-svelte-enhanced-block__action--icon infographic-action-btn" aria-label={t('common.zoomOut') || 'Zoom out'} onblur={() => hideTooltip()} onclick={zoomOut} onfocus={(event) => showButtonTooltip(event, t('common.zoomOut') || 'Zoom out')} onmouseleave={() => hideTooltip()} onmouseenter={(event) => showButtonTooltip(event, t('common.zoomOut') || 'Zoom out')}>
454
501
  <svg aria-hidden="true" role="img" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="m21 21l-4.35-4.35M8 11h6"/></g></svg>
455
502
  </button>
456
- <button type="button" class="markstream-svelte-enhanced-block__action infographic-action-btn infographic-zoom-reset" onblur={() => hideTooltip()} onclick={() => (zoom = 1)} onfocus={(event) => showButtonTooltip(event, t('common.resetZoom') || 'Reset zoom')} onmouseleave={() => hideTooltip()} onmouseenter={(event) => showButtonTooltip(event, t('common.resetZoom') || 'Reset zoom')}>{Math.round(zoom * 100)}%</button>
503
+ <button type="button" class="markstream-svelte-enhanced-block__action infographic-action-btn infographic-zoom-reset" onblur={() => hideTooltip()} onclick={resetZoom} onfocus={(event) => showButtonTooltip(event, t('common.resetZoom') || 'Reset zoom')} onmouseleave={() => hideTooltip()} onmouseenter={(event) => showButtonTooltip(event, t('common.resetZoom') || 'Reset zoom')}>{Math.round(zoom * 100)}%</button>
457
504
  </div>
458
505
  {/if}
459
- <div class="infographic-render" style={previewStyle}>
506
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
507
+ <div class="infographic-render" class:markstream-svelte-pan-touch={panSurfaceClaimsTouch} style={previewStyle} onpointerdown={startDrag}>
460
508
  {#if !hasPreview && !renderError}
461
509
  <pre class="infographic-source-code infographic-pending-source"><code>{source}</code></pre>
462
510
  {/if}
@@ -480,13 +528,14 @@
480
528
  <button type="button" class="markstream-svelte-enhanced-block__action markstream-svelte-enhanced-block__action--icon infographic-action-btn" aria-label={t('common.zoomOut') || 'Zoom out'} onclick={zoomOut}>
481
529
  <svg aria-hidden="true" role="img" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="m21 21l-4.35-4.35M8 11h6"/></g></svg>
482
530
  </button>
483
- <button type="button" class="markstream-svelte-enhanced-block__action infographic-action-btn infographic-zoom-reset" aria-label={t('common.resetZoom') || 'Reset zoom'} onclick={() => (zoom = 1)}>{Math.round(zoom * 100)}%</button>
531
+ <button type="button" class="markstream-svelte-enhanced-block__action infographic-action-btn infographic-zoom-reset" aria-label={t('common.resetZoom') || 'Reset zoom'} onclick={resetZoom}>{Math.round(zoom * 100)}%</button>
484
532
  <button type="button" class="markstream-svelte-enhanced-block__action markstream-svelte-enhanced-block__action--icon infographic-action-btn" aria-label={t('common.close') || 'Close'} onclick={closeModal}>
485
533
  <svg aria-hidden="true" role="img" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18 6L6 18M6 6l12 12"/></svg>
486
534
  </button>
487
535
  </div>
488
- <div class="mermaid-modal-body">
489
- <div class="mermaid-modal-content infographic-modal-content" style={`transform: scale(${zoom});`}>{@html modalMarkup}</div>
536
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
537
+ <div class="mermaid-modal-body" class:markstream-svelte-pan-touch={panSurfaceClaimsTouch} style={`cursor: ${isDragging ? 'grabbing' : 'grab'}`} onpointerdown={startDrag}>
538
+ <div class="mermaid-modal-content infographic-modal-content" style={`transform: translate(${translateX}px, ${translateY}px) scale(${zoom});`}>{@html modalMarkup}</div>
490
539
  </div>
491
540
  </div>
492
541
  </div>
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import type { SvelteRenderableNode, SvelteRenderContext } from './shared/node-helpers'
3
+ import { createPanGesture } from 'markstream-core'
3
4
  import { onMount, tick, untrack } from 'svelte'
4
5
  import { toSafeMermaidSvgMarkup } from 'stream-markdown-parser'
5
6
  import { useSafeI18n } from '../i18n/useSafeI18n'
@@ -77,6 +78,9 @@
77
78
  let showSource = $state(false)
78
79
  let modalOpen = $state(false)
79
80
  let zoom = $state(1)
81
+ let translateX = $state(0)
82
+ let translateY = $state(0)
83
+ let isDragging = $state(false)
80
84
  let blockHost: HTMLElement | null = $state(null)
81
85
  let previewHost: HTMLElement | null = $state(null)
82
86
  let modalHost: HTMLElement | null = $state(null)
@@ -100,11 +104,19 @@
100
104
  let previewStyle = $derived([
101
105
  `min-height: ${previewHeight}px`,
102
106
  maxHeight && maxHeight !== 'none' ? `max-height: ${maxHeight}` : '',
103
- `transform: scale(${zoom})`,
107
+ `transform: translate(${translateX}px, ${translateY}px) scale(${zoom})`,
108
+ `cursor: ${isDragging ? 'grabbing' : 'grab'}`,
104
109
  ].filter(Boolean).join('; '))
105
110
  let shouldRender = $derived(!(resolvedLoading && !source.trim()))
106
111
  let canUsePreview = $derived(Boolean(svgMarkup && !showSource))
107
112
 
113
+ // The diagram is scaled to fit the preview box, so at fit zoom nothing is
114
+ // off-screen and panning only shifts it inside the box. Touch therefore keeps
115
+ // scrolling the page at that zoom (a chat is mostly scrolling) and only claims
116
+ // the gesture once the diagram is zoomed in and actually overflows the box.
117
+ // The fullscreen modal always claims it.
118
+ let panSurfaceClaimsTouch = $derived(modalOpen || zoom > 1)
119
+
108
120
  onMount(() => {
109
121
  mounted = true
110
122
  scheduleRender(true)
@@ -113,6 +125,8 @@
113
125
  mounted = false
114
126
  renderToken += 1
115
127
  clearRenderTimer()
128
+ // A drag in progress must not keep its window listeners after unmount.
129
+ panGesture.stop()
116
130
  if (copyTimer)
117
131
  clearTimeout(copyTimer)
118
132
  }
@@ -460,6 +474,39 @@
460
474
  zoom = Math.max(0.5, Math.round((zoom - 0.1) * 10) / 10)
461
475
  }
462
476
 
477
+ function resetZoom() {
478
+ zoom = 1
479
+ translateX = 0
480
+ translateY = 0
481
+ }
482
+
483
+ // Drag functionality. The gesture lifecycle (window listeners, pointer identity,
484
+ // lost releases) lives in markstream-core so every package pans identically.
485
+ const panGesture = createPanGesture({
486
+ getTranslate: () => ({ x: translateX, y: translateY }),
487
+ setTranslate: (next) => {
488
+ translateX = next.x
489
+ translateY = next.y
490
+ },
491
+ canStart: (event) => {
492
+ // Nothing to pan until the diagram exists. Without this the pending/error
493
+ // source panel the block renders in the preview would be claimed by the
494
+ // gesture, and `preventDefault` would take text selection away from it.
495
+ if (!svgMarkup)
496
+ return false
497
+
498
+ // Touch pans only where the surface claims the gesture. Elsewhere the page
499
+ // keeps the swipe for scrolling, and panning along with it would shift the
500
+ // diagram by a few pixels before the browser takes the gesture over.
501
+ return event.pointerType !== 'touch' || panSurfaceClaimsTouch
502
+ },
503
+ onActiveChange: active => (isDragging = active),
504
+ })
505
+
506
+ function startDrag(event: PointerEvent) {
507
+ panGesture.start(event)
508
+ }
509
+
463
510
  function showButtonTooltip(event: MouseEvent | FocusEvent, text: string, placement: TooltipPlacement = 'top') {
464
511
  const target = event.currentTarget as HTMLElement | null
465
512
  if (!target || (target instanceof HTMLButtonElement && target.disabled))
@@ -568,10 +615,11 @@
568
615
  <button type="button" class="mermaid-btn mermaid-action-btn mermaid-btn--icon" aria-label={t('common.zoomOut')} onblur={() => hideTooltip()} onclick={zoomOut} onfocus={(event) => showButtonTooltip(event, t('common.zoomOut') || 'Zoom out')} onmouseleave={() => hideTooltip()} onmouseenter={(event) => showButtonTooltip(event, t('common.zoomOut') || 'Zoom out')}>
569
616
  <svg aria-hidden="true" role="img" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="m21 21l-4.35-4.35M8 11h6"/></g></svg>
570
617
  </button>
571
- <button type="button" class="mermaid-btn mermaid-action-btn mermaid-zoom-reset" aria-label={t('common.resetZoom')} onblur={() => hideTooltip()} onclick={() => (zoom = 1)} onfocus={(event) => showButtonTooltip(event, t('common.resetZoom') || 'Reset zoom')} onmouseleave={() => hideTooltip()} onmouseenter={(event) => showButtonTooltip(event, t('common.resetZoom') || 'Reset zoom')}>{Math.round(zoom * 100)}%</button>
618
+ <button type="button" class="mermaid-btn mermaid-action-btn mermaid-zoom-reset" aria-label={t('common.resetZoom')} onblur={() => hideTooltip()} onclick={resetZoom} onfocus={(event) => showButtonTooltip(event, t('common.resetZoom') || 'Reset zoom')} onmouseleave={() => hideTooltip()} onmouseenter={(event) => showButtonTooltip(event, t('common.resetZoom') || 'Reset zoom')}>{Math.round(zoom * 100)}%</button>
572
619
  </div>
573
620
  {/if}
574
- <div bind:this={previewHost} class="mermaid-preview markstream-svelte-mermaid" style={previewStyle}>
621
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
622
+ <div bind:this={previewHost} class="mermaid-preview markstream-svelte-mermaid" class:markstream-svelte-pan-touch={panSurfaceClaimsTouch} style={previewStyle} onpointerdown={startDrag}>
575
623
  {#if svgMarkup}
576
624
  {@html svgMarkup}
577
625
  {:else if renderError}
@@ -598,13 +646,14 @@
598
646
  <button type="button" class="mermaid-btn mermaid-action-btn mermaid-btn--icon" aria-label={t('common.zoomOut')} onclick={zoomOut}>
599
647
  <svg aria-hidden="true" role="img" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="m21 21l-4.35-4.35M8 11h6"/></g></svg>
600
648
  </button>
601
- <button type="button" class="mermaid-btn mermaid-action-btn mermaid-zoom-reset" aria-label={t('common.resetZoom')} onclick={() => (zoom = 1)}>{Math.round(zoom * 100)}%</button>
649
+ <button type="button" class="mermaid-btn mermaid-action-btn mermaid-zoom-reset" aria-label={t('common.resetZoom')} onclick={resetZoom}>{Math.round(zoom * 100)}%</button>
602
650
  <button type="button" class="mermaid-btn mermaid-action-btn mermaid-btn--icon" aria-label={t('common.close')} onclick={closeModal}>
603
651
  <svg aria-hidden="true" role="img" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18 6L6 18M6 6l12 12"/></svg>
604
652
  </button>
605
653
  </div>
606
- <div class="mermaid-modal-body">
607
- <div bind:this={modalHost} class="mermaid-modal-content markstream-svelte-mermaid" style={`transform: scale(${zoom});`}>{@html svgMarkup}</div>
654
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
655
+ <div class="mermaid-modal-body" class:markstream-svelte-pan-touch={panSurfaceClaimsTouch} style={`cursor: ${isDragging ? 'grabbing' : 'grab'}`} onpointerdown={startDrag}>
656
+ <div bind:this={modalHost} class="mermaid-modal-content markstream-svelte-mermaid" style={`transform: translate(${translateX}px, ${translateY}px) scale(${zoom});`}>{@html svgMarkup}</div>
608
657
  </div>
609
658
  </div>
610
659
  </div>
package/dist/index.css CHANGED
@@ -1099,9 +1099,14 @@ body > div[id^="dmermaid-"] {
1099
1099
  color: var(--code-fg);
1100
1100
  }
1101
1101
 
1102
+ /* The pan transform is applied to `.mermaid-preview` itself, and a transformed
1103
+ element paints above the header it overlaps; the body is what clips the panned
1104
+ diagram back into the preview area. clip (not auto/hidden) means it is not a
1105
+ scroll container at all, so a touch swipe keeps scrolling the page. */
1102
1106
  .markstream-svelte .mermaid-body {
1103
1107
  position: relative;
1104
1108
  padding: 0;
1109
+ overflow: clip;
1105
1110
  }
1106
1111
 
1107
1112
  .markstream-svelte .markstream-svelte-zoom-controls {
@@ -1128,7 +1133,11 @@ body > div[id^="dmermaid-"] {
1128
1133
  .markstream-svelte .mermaid-preview {
1129
1134
  width: 100%;
1130
1135
  min-height: var(--ms-size-diagram-min-height);
1131
- overflow: auto;
1136
+ /* clip, not auto: an overflow:auto pan surface is a "user-scrollable" box,
1137
+ so a touch swipe latches onto it and, when nothing can scroll there, the
1138
+ page does not scroll either. clip is never a scroll container and clips
1139
+ the same (the diagram is scaled to fit). */
1140
+ overflow: clip;
1132
1141
  display: flex;
1133
1142
  align-items: center;
1134
1143
  justify-content: center;
@@ -1141,6 +1150,14 @@ body > div[id^="dmermaid-"] {
1141
1150
  height: auto;
1142
1151
  }
1143
1152
 
1153
+ /* The pan surface takes the touch gesture instead of letting the page scroller
1154
+ claim it, which would fire pointercancel and stop the drag mid-gesture. The
1155
+ class is only bound while panning can reveal something (zoomed in, or in the
1156
+ fullscreen modal), so at fit zoom a swipe keeps scrolling the page. */
1157
+ .markstream-svelte .markstream-svelte-pan-touch {
1158
+ touch-action: none;
1159
+ }
1160
+
1144
1161
  .markstream-svelte .mermaid-source {
1145
1162
  margin: 0;
1146
1163
  padding: var(--ms-inset-panel-body);
@@ -1401,6 +1418,10 @@ body > div[id^="dmermaid-"] {
1401
1418
  .markstream-svelte .infographic-block-body {
1402
1419
  padding: 0;
1403
1420
  background: var(--diagram-bg);
1421
+ /* The pan transform is applied to .infographic-render itself, so the body is
1422
+ what clips the panned diagram back into the preview area. clip (not the
1423
+ base __body's auto) so it is not a scroll container a swipe could latch on. */
1424
+ overflow: clip;
1404
1425
  }
1405
1426
 
1406
1427
  .markstream-svelte .infographic-source {
@@ -2162,7 +2183,9 @@ body > div[id^="dmermaid-"] {
2162
2183
  position: relative;
2163
2184
  width: 100%;
2164
2185
  min-height: var(--ms-size-diagram-min-height);
2165
- overflow: auto;
2186
+ /* clip, not auto — same reason as .mermaid-preview: a scrollable pan surface
2187
+ would latch touch swipes and stop the page from scrolling. */
2188
+ overflow: clip;
2166
2189
  text-align: center;
2167
2190
  }
2168
2191
 
package/dist/index.px.css CHANGED
@@ -1099,9 +1099,14 @@ body > div[id^="dmermaid-"] {
1099
1099
  color: var(--code-fg);
1100
1100
  }
1101
1101
 
1102
+ /* The pan transform is applied to `.mermaid-preview` itself, and a transformed
1103
+ element paints above the header it overlaps; the body is what clips the panned
1104
+ diagram back into the preview area. clip (not auto/hidden) means it is not a
1105
+ scroll container at all, so a touch swipe keeps scrolling the page. */
1102
1106
  .markstream-svelte .mermaid-body {
1103
1107
  position: relative;
1104
1108
  padding: 0;
1109
+ overflow: clip;
1105
1110
  }
1106
1111
 
1107
1112
  .markstream-svelte .markstream-svelte-zoom-controls {
@@ -1128,7 +1133,11 @@ body > div[id^="dmermaid-"] {
1128
1133
  .markstream-svelte .mermaid-preview {
1129
1134
  width: 100%;
1130
1135
  min-height: var(--ms-size-diagram-min-height);
1131
- overflow: auto;
1136
+ /* clip, not auto: an overflow:auto pan surface is a "user-scrollable" box,
1137
+ so a touch swipe latches onto it and, when nothing can scroll there, the
1138
+ page does not scroll either. clip is never a scroll container and clips
1139
+ the same (the diagram is scaled to fit). */
1140
+ overflow: clip;
1132
1141
  display: flex;
1133
1142
  align-items: center;
1134
1143
  justify-content: center;
@@ -1141,6 +1150,14 @@ body > div[id^="dmermaid-"] {
1141
1150
  height: auto;
1142
1151
  }
1143
1152
 
1153
+ /* The pan surface takes the touch gesture instead of letting the page scroller
1154
+ claim it, which would fire pointercancel and stop the drag mid-gesture. The
1155
+ class is only bound while panning can reveal something (zoomed in, or in the
1156
+ fullscreen modal), so at fit zoom a swipe keeps scrolling the page. */
1157
+ .markstream-svelte .markstream-svelte-pan-touch {
1158
+ touch-action: none;
1159
+ }
1160
+
1144
1161
  .markstream-svelte .mermaid-source {
1145
1162
  margin: 0;
1146
1163
  padding: var(--ms-inset-panel-body);
@@ -1401,6 +1418,10 @@ body > div[id^="dmermaid-"] {
1401
1418
  .markstream-svelte .infographic-block-body {
1402
1419
  padding: 0;
1403
1420
  background: var(--diagram-bg);
1421
+ /* The pan transform is applied to .infographic-render itself, so the body is
1422
+ what clips the panned diagram back into the preview area. clip (not the
1423
+ base __body's auto) so it is not a scroll container a swipe could latch on. */
1424
+ overflow: clip;
1404
1425
  }
1405
1426
 
1406
1427
  .markstream-svelte .infographic-source {
@@ -2162,7 +2183,9 @@ body > div[id^="dmermaid-"] {
2162
2183
  position: relative;
2163
2184
  width: 100%;
2164
2185
  min-height: var(--ms-size-diagram-min-height);
2165
- overflow: auto;
2186
+ /* clip, not auto — same reason as .mermaid-preview: a scrollable pan surface
2187
+ would latch touch swipes and stop the page from scrolling. */
2188
+ overflow: clip;
2166
2189
  text-align: center;
2167
2190
  }
2168
2191
 
@@ -1099,9 +1099,14 @@ body > div[id^="dmermaid-"] {
1099
1099
  color: var(--code-fg);
1100
1100
  }
1101
1101
 
1102
+ /* The pan transform is applied to `.mermaid-preview` itself, and a transformed
1103
+ element paints above the header it overlaps; the body is what clips the panned
1104
+ diagram back into the preview area. clip (not auto/hidden) means it is not a
1105
+ scroll container at all, so a touch swipe keeps scrolling the page. */
1102
1106
  .markstream-svelte .mermaid-body {
1103
1107
  position: relative;
1104
1108
  padding: 0;
1109
+ overflow: clip;
1105
1110
  }
1106
1111
 
1107
1112
  .markstream-svelte .markstream-svelte-zoom-controls {
@@ -1128,7 +1133,11 @@ body > div[id^="dmermaid-"] {
1128
1133
  .markstream-svelte .mermaid-preview {
1129
1134
  width: 100%;
1130
1135
  min-height: var(--ms-size-diagram-min-height);
1131
- overflow: auto;
1136
+ /* clip, not auto: an overflow:auto pan surface is a "user-scrollable" box,
1137
+ so a touch swipe latches onto it and, when nothing can scroll there, the
1138
+ page does not scroll either. clip is never a scroll container and clips
1139
+ the same (the diagram is scaled to fit). */
1140
+ overflow: clip;
1132
1141
  display: flex;
1133
1142
  align-items: center;
1134
1143
  justify-content: center;
@@ -1141,6 +1150,14 @@ body > div[id^="dmermaid-"] {
1141
1150
  height: auto;
1142
1151
  }
1143
1152
 
1153
+ /* The pan surface takes the touch gesture instead of letting the page scroller
1154
+ claim it, which would fire pointercancel and stop the drag mid-gesture. The
1155
+ class is only bound while panning can reveal something (zoomed in, or in the
1156
+ fullscreen modal), so at fit zoom a swipe keeps scrolling the page. */
1157
+ .markstream-svelte .markstream-svelte-pan-touch {
1158
+ touch-action: none;
1159
+ }
1160
+
1144
1161
  .markstream-svelte .mermaid-source {
1145
1162
  margin: 0;
1146
1163
  padding: var(--ms-inset-panel-body);
@@ -1401,6 +1418,10 @@ body > div[id^="dmermaid-"] {
1401
1418
  .markstream-svelte .infographic-block-body {
1402
1419
  padding: 0;
1403
1420
  background: var(--diagram-bg);
1421
+ /* The pan transform is applied to .infographic-render itself, so the body is
1422
+ what clips the panned diagram back into the preview area. clip (not the
1423
+ base __body's auto) so it is not a scroll container a swipe could latch on. */
1424
+ overflow: clip;
1404
1425
  }
1405
1426
 
1406
1427
  .markstream-svelte .infographic-source {
@@ -2162,7 +2183,9 @@ body > div[id^="dmermaid-"] {
2162
2183
  position: relative;
2163
2184
  width: 100%;
2164
2185
  min-height: var(--ms-size-diagram-min-height);
2165
- overflow: auto;
2186
+ /* clip, not auto — same reason as .mermaid-preview: a scrollable pan surface
2187
+ would latch touch swipes and stop the page from scrolling. */
2188
+ overflow: clip;
2166
2189
  text-align: center;
2167
2190
  }
2168
2191
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "markstream-svelte",
3
3
  "type": "module",
4
- "version": "2.0.12",
4
+ "version": "2.0.13",
5
5
  "description": "Svelte 5 and SvelteKit streaming Markdown renderer for AI chat, LLM token streams, SSE/WebSocket output, incomplete Markdown, Mermaid, KaTeX, stream-diffs powered code blocks, and custom Svelte components.",
6
6
  "author": "Simon He",
7
7
  "license": "MIT",
@@ -100,8 +100,8 @@
100
100
  },
101
101
  "dependencies": {
102
102
  "@floating-ui/dom": "^1.8.0",
103
- "markstream-core": "2.0.12",
104
- "stream-markdown-parser": "1.2.16"
103
+ "stream-markdown-parser": "1.2.16",
104
+ "markstream-core": "2.0.13"
105
105
  },
106
106
  "devDependencies": {
107
107
  "@sveltejs/package": "^2.5.8",