@duffcloudservices/cms 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,76 +1,77 @@
1
- {
2
- "name": "@duffcloudservices/cms",
3
- "version": "0.1.4",
4
- "description": "Vue 3 composables and Vite plugins for DCS CMS integration",
5
- "type": "module",
6
- "exports": {
7
- ".": {
8
- "types": "./dist/index.d.ts",
9
- "import": "./dist/index.js"
10
- },
11
- "./plugins": {
12
- "types": "./dist/plugins/index.d.ts",
13
- "import": "./dist/plugins/index.js"
14
- },
15
- "./editor": {
16
- "types": "./dist/editor/editorBridge.d.ts",
17
- "import": "./dist/editor/editorBridge.js"
18
- },
19
- "./components": {
20
- "import": "./src/components/PreviewRibbon.vue"
21
- }
22
- },
23
- "main": "./dist/index.js",
24
- "types": "./dist/index.d.ts",
25
- "files": [
26
- "dist",
27
- "src/components"
28
- ],
29
- "peerDependencies": {
30
- "vue": "^3.4.0",
31
- "@unhead/vue": "^1.9.0"
32
- },
33
- "dependencies": {
34
- "js-yaml": "^4.1.0"
35
- },
36
- "devDependencies": {
37
- "@types/js-yaml": "^4.0.9",
38
- "@types/node": "^20.11.0",
39
- "@vue/test-utils": "^2.4.0",
40
- "tsup": "^8.0.0",
41
- "typescript": "~5.6.3",
42
- "vite": "^6.3.5",
43
- "vitest": "^3.2.3",
44
- "vue": "^3.5.16",
45
- "@unhead/vue": "^2.0.5"
46
- },
47
- "keywords": [
48
- "vue",
49
- "vitepress",
50
- "cms",
51
- "dcs",
52
- "composables",
53
- "duff-cloud-services"
54
- ],
55
- "author": "Duff Cloud Services",
56
- "license": "MIT",
57
- "repository": {
58
- "type": "git",
59
- "url": "https://github.com/duffn/dcs"
60
- },
61
- "homepage": "https://portal.duffcloudservices.com",
62
- "bugs": {
63
- "url": "https://github.com/duffn/dcs/issues"
64
- },
65
- "engines": {
66
- "node": ">=18.0.0"
67
- },
68
- "scripts": {
69
- "build": "tsup",
70
- "dev": "tsup --watch",
71
- "test": "vitest run",
72
- "test:watch": "vitest",
73
- "type-check": "tsc --noEmit",
74
- "lint": "eslint src --ext .ts"
75
- }
76
- }
1
+ {
2
+ "name": "@duffcloudservices/cms",
3
+ "version": "0.1.6",
4
+ "description": "Vue 3 composables and Vite plugins for DCS CMS integration",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": {
8
+ "types": "./dist/index.d.ts",
9
+ "import": "./dist/index.js"
10
+ },
11
+ "./plugins": {
12
+ "types": "./dist/plugins/index.d.ts",
13
+ "import": "./dist/plugins/index.js"
14
+ },
15
+ "./editor": {
16
+ "types": "./dist/editor/editorBridge.d.ts",
17
+ "import": "./dist/editor/editorBridge.js"
18
+ },
19
+ "./components": {
20
+ "import": "./src/components/PreviewRibbon.vue"
21
+ }
22
+ },
23
+ "main": "./dist/index.js",
24
+ "types": "./dist/index.d.ts",
25
+ "files": [
26
+ "dist",
27
+ "src/components"
28
+ ],
29
+ "scripts": {
30
+ "build": "tsup",
31
+ "dev": "tsup --watch",
32
+ "test": "vitest run",
33
+ "test:watch": "vitest",
34
+ "type-check": "tsc --noEmit",
35
+ "lint": "eslint src --ext .ts",
36
+ "prepublishOnly": "pnpm run build"
37
+ },
38
+ "peerDependencies": {
39
+ "vue": "^3.4.0",
40
+ "@unhead/vue": "^1.9.0"
41
+ },
42
+ "dependencies": {
43
+ "js-yaml": "^4.1.0"
44
+ },
45
+ "devDependencies": {
46
+ "@types/js-yaml": "^4.0.9",
47
+ "@types/node": "^20.11.0",
48
+ "@vue/test-utils": "^2.4.0",
49
+ "tsup": "^8.0.0",
50
+ "typescript": "~5.6.3",
51
+ "vite": "^6.3.5",
52
+ "vitest": "^3.2.3",
53
+ "vue": "^3.5.16",
54
+ "@unhead/vue": "^2.0.5"
55
+ },
56
+ "keywords": [
57
+ "vue",
58
+ "vitepress",
59
+ "cms",
60
+ "dcs",
61
+ "composables",
62
+ "duff-cloud-services"
63
+ ],
64
+ "author": "Duff Cloud Services",
65
+ "license": "MIT",
66
+ "repository": {
67
+ "type": "git",
68
+ "url": "https://github.com/duffn/dcs"
69
+ },
70
+ "homepage": "https://portal.duffcloudservices.com",
71
+ "bugs": {
72
+ "url": "https://github.com/duffn/dcs/issues"
73
+ },
74
+ "engines": {
75
+ "node": ">=18.0.0"
76
+ }
77
+ }
@@ -1,14 +1,25 @@
1
1
  <template>
2
2
  <Teleport to="body">
3
- <div v-if="isVisible" class="dcs-ribbon-container">
3
+ <div
4
+ v-if="isVisible"
5
+ class="dcs-ribbon-container"
6
+ :class="{ 'dcs-ribbon-container--flipping': isFlipping }"
7
+ :style="containerStyle"
8
+ >
4
9
  <svg
5
10
  ref="ribbonSvgRef"
6
11
  :viewBox="`0 0 ${SVG_SIZE} ${SVG_SIZE}`"
7
12
  class="dcs-ribbon-svg"
8
13
  >
9
14
  <defs>
10
- <path id="dcs-ribbon-text-path" :d="centerPath" />
11
- <linearGradient id="dcs-ribbon-grad" x1="0%" y1="100%" x2="100%" y2="0%">
15
+ <path id="dcs-ribbon-text-path" :d="textPath" />
16
+ <linearGradient
17
+ id="dcs-ribbon-grad"
18
+ :x1="side === 'left' ? '0%' : '100%'"
19
+ y1="100%"
20
+ :x2="side === 'left' ? '100%' : '0%'"
21
+ y2="0%"
22
+ >
12
23
  <stop offset="0%" stop-color="#1a1a2e" />
13
24
  <stop offset="40%" stop-color="#16213e" />
14
25
  <stop offset="100%" stop-color="#0f3460" />
@@ -19,7 +30,11 @@
19
30
  </defs>
20
31
 
21
32
  <!-- Drop shadow (offset copy of band) -->
22
- <path :d="bandPath" fill="rgba(0,0,0,0.2)" transform="translate(2,3)" />
33
+ <path
34
+ :d="bandPath"
35
+ fill="rgba(0,0,0,0.2)"
36
+ :transform="side === 'left' ? 'translate(2,3)' : 'translate(-2,3)'"
37
+ />
23
38
 
24
39
  <!-- Main ribbon band -->
25
40
  <path
@@ -110,6 +125,9 @@ const STI = 4
110
125
  /** The preview domain where the ribbon should be displayed */
111
126
  const PREVIEW_DOMAIN = 'preview.duffcloudservices.com'
112
127
 
128
+ /** localStorage key for persisting which corner the ribbon is docked to */
129
+ const STORAGE_KEY = 'dcs-preview-ribbon-side'
130
+
113
131
  /* ------------------------------------------------------------------ */
114
132
  /* Visibility gate */
115
133
  /* ------------------------------------------------------------------ */
@@ -134,9 +152,40 @@ function shouldShow(): boolean {
134
152
  return true
135
153
  }
136
154
 
137
- onMounted(() => {
138
- isVisible.value = shouldShow()
139
- })
155
+ /* ------------------------------------------------------------------ */
156
+ /* Side state (which corner the ribbon is docked to, persisted) */
157
+ /* ------------------------------------------------------------------ */
158
+ const side = ref<'left' | 'right'>('left')
159
+
160
+ function loadSide(): 'left' | 'right' {
161
+ try {
162
+ const s = localStorage.getItem(STORAGE_KEY)
163
+ if (s === 'left' || s === 'right') return s
164
+ } catch { /* noop */ }
165
+ return 'left'
166
+ }
167
+
168
+ function saveSide(s: 'left' | 'right') {
169
+ try { localStorage.setItem(STORAGE_KEY, s) } catch { /* noop */ }
170
+ }
171
+
172
+ /* ------------------------------------------------------------------ */
173
+ /* Viewport tracking */
174
+ /* ------------------------------------------------------------------ */
175
+ const viewportWidth = ref(1920)
176
+ const svgSizePx = ref(260)
177
+
178
+ function updateViewport() {
179
+ viewportWidth.value = window.innerWidth
180
+ svgSizePx.value = window.innerWidth <= 640 ? 200 : 260
181
+ }
182
+
183
+ function onResize() {
184
+ updateViewport()
185
+ if (!isDragging.value && !isFlipping.value) {
186
+ posX.value = getRestX()
187
+ }
188
+ }
140
189
 
141
190
  /* ------------------------------------------------------------------ */
142
191
  /* Version display */
@@ -147,11 +196,30 @@ const displayVersion = computed(() =>
147
196
  resolvedVersion.value ? ` v${resolvedVersion.value}` : '',
148
197
  )
149
198
 
150
- // Try to read the version from VITE_SITE_VERSION env var at build time
199
+ // Sync prop changes
200
+ watch(
201
+ () => props.version,
202
+ (v) => {
203
+ if (v !== null && v !== undefined) resolvedVersion.value = v
204
+ },
205
+ )
206
+
207
+ /* ------------------------------------------------------------------ */
208
+ /* Lifecycle */
209
+ /* ------------------------------------------------------------------ */
151
210
  onMounted(async () => {
211
+ isVisible.value = shouldShow()
212
+ if (!isVisible.value) return
213
+
214
+ side.value = loadSide()
215
+ updateViewport()
216
+ posX.value = getRestX()
217
+
218
+ window.addEventListener('resize', onResize)
219
+
220
+ // Resolve version
152
221
  if (resolvedVersion.value) return
153
222
 
154
- // Check Vite env first
155
223
  try {
156
224
  const envVersion = (import.meta.env as Record<string, string | undefined>)
157
225
  .VITE_SITE_VERSION
@@ -163,8 +231,6 @@ onMounted(async () => {
163
231
  /* noop */
164
232
  }
165
233
 
166
- // Fallback: fetch from API (reuse useSiteVersion logic inline to avoid
167
- // coupling lifecycle hooks – the ribbon may mount before Pinia is ready)
168
234
  try {
169
235
  const slug =
170
236
  (import.meta.env as Record<string, string | undefined>).VITE_SITE_SLUG ??
@@ -186,50 +252,63 @@ onMounted(async () => {
186
252
  }
187
253
  })
188
254
 
189
- // Sync prop changes
190
- watch(
191
- () => props.version,
192
- (v) => {
193
- if (v !== null && v !== undefined) resolvedVersion.value = v
194
- },
195
- )
196
-
197
255
  /* ------------------------------------------------------------------ */
198
- /* Elastic stretch state (control-point displacement) */
256
+ /* Position state */
199
257
  /* ------------------------------------------------------------------ */
200
- const stretchX = ref(0)
201
- const stretchY = ref(0)
258
+ const posX = ref(0)
259
+ const posY = ref(0)
202
260
  const isDragging = ref(false)
261
+ const isFlipping = ref(false)
203
262
 
204
- /** Effective control-point position (default + stretch offset) */
205
- const cpX = computed(() => CP0 + stretchX.value)
206
- const cpY = computed(() => CP0 + stretchY.value)
263
+ /** Rest X position for the current side */
264
+ function getRestX(): number {
265
+ if (side.value === 'left') return 0
266
+ return viewportWidth.value - svgSizePx.value
267
+ }
268
+
269
+ const containerStyle = computed(() => ({
270
+ transform: `translate(${posX.value}px, ${posY.value}px)`,
271
+ }))
207
272
 
208
273
  /* ------------------------------------------------------------------ */
209
- /* SVG path computation */
274
+ /* SVG path computation (side-aware, X-mirrored for right) */
210
275
  /* ------------------------------------------------------------------ */
211
276
 
212
- /** Quadratic bezier from left-edge (0, y0) to top-edge (x1, 0) */
213
- function q(y0: number, cx: number, cy: number, x1: number): string {
214
- return `M 0,${y0} Q ${cx},${cy} ${x1},0`
277
+ /**
278
+ * Quadratic bezier curve for stitch / helper paths.
279
+ * Left side: (0, y0) (x1, 0) via (cx, cy)
280
+ * Right side: (SVG_SIZE, y0) → (SVG_SIZE-x1, 0) via (SVG_SIZE-cx, cy)
281
+ */
282
+ function mirrorQ(y0: number, cx: number, cy: number, x1: number): string {
283
+ if (side.value === 'left') {
284
+ return `M 0,${y0} Q ${cx},${cy} ${x1},0`
285
+ }
286
+ return `M ${SVG_SIZE},${y0} Q ${SVG_SIZE - cx},${cy} ${SVG_SIZE - x1},0`
215
287
  }
216
288
 
217
- const centerPath = computed(() => q(EP, cpX.value, cpY.value, EP))
289
+ /** Text center-line path. Reversed on right side so text reads naturally. */
290
+ const textPath = computed(() => {
291
+ if (side.value === 'left') {
292
+ return `M 0,${EP} Q ${CP0},${CP0} ${EP},0`
293
+ }
294
+ // Right side: path goes from top-edge → right-edge for left-to-right text
295
+ return `M ${SVG_SIZE - EP},0 Q ${SVG_SIZE - CP0},${CP0} ${SVG_SIZE},${EP}`
296
+ })
218
297
 
219
298
  const outerStitchPath = computed(() =>
220
- q(
299
+ mirrorQ(
221
300
  EP + HW - STI,
222
- cpX.value + CPO - 3,
223
- cpY.value + CPO - 3,
301
+ CP0 + CPO - 3,
302
+ CP0 + CPO - 3,
224
303
  EP + HW - STI,
225
304
  ),
226
305
  )
227
306
 
228
307
  const innerStitchPath = computed(() =>
229
- q(
308
+ mirrorQ(
230
309
  EP - HW + STI,
231
- cpX.value - CPO + 3,
232
- cpY.value - CPO + 3,
310
+ CP0 - CPO + 3,
311
+ CP0 - CPO + 3,
233
312
  EP - HW + STI,
234
313
  ),
235
314
  )
@@ -237,34 +316,43 @@ const innerStitchPath = computed(() =>
237
316
  const bandPath = computed(() => {
238
317
  const oY = EP + HW
239
318
  const oX = EP + HW
240
- const oCx = cpX.value + CPO
241
- const oCy = cpY.value + CPO
319
+ const oCx = CP0 + CPO
320
+ const oCy = CP0 + CPO
242
321
  const iY = EP - HW
243
322
  const iX = EP - HW
244
- const iCx = cpX.value - CPO
245
- const iCy = cpY.value - CPO
323
+ const iCx = CP0 - CPO
324
+ const iCy = CP0 - CPO
325
+
326
+ if (side.value === 'left') {
327
+ return [
328
+ `M 0,${oY} Q ${oCx},${oCy} ${oX},0`,
329
+ `L ${iX},0`,
330
+ `Q ${iCx},${iCy} 0,${iY}`,
331
+ `Z`,
332
+ ].join(' ')
333
+ }
246
334
 
247
335
  return [
248
- // Outer curve (left-edge top-edge)
249
- `M 0,${oY} Q ${oCx},${oCy} ${oX},0`,
250
- // Cap at top-edge inner curve start
251
- `L ${iX},0`,
252
- // Inner curve (top-edge → left-edge, reversed direction)
253
- `Q ${iCx},${iCy} 0,${iY}`,
336
+ `M ${SVG_SIZE},${oY} Q ${SVG_SIZE - oCx},${oCy} ${SVG_SIZE - oX},0`,
337
+ `L ${SVG_SIZE - iX},0`,
338
+ `Q ${SVG_SIZE - iCx},${iCy} ${SVG_SIZE},${iY}`,
254
339
  `Z`,
255
340
  ].join(' ')
256
341
  })
257
342
 
258
343
  /* ------------------------------------------------------------------ */
259
- /* Drag: elastic ribbon stretch */
344
+ /* Drag: translate ribbon + flip on midpoint crossing */
260
345
  /* ------------------------------------------------------------------ */
261
346
  const ribbonSvgRef = ref<SVGSVGElement | null>(null)
262
- let dragState: { startX: number; startY: number } | null = null
347
+ let dragStart: { x: number; y: number; posX: number; posY: number } | null = null
348
+ let capturedTarget: Element | null = null
349
+ let lastPointerId: number | null = null
263
350
  let springRaf = 0
351
+ let flipTimeout: ReturnType<typeof setTimeout> | null = null
264
352
 
265
353
  function onRibbonPointerDown(e: PointerEvent) {
266
- const el = ribbonSvgRef.value
267
- if (!el) return
354
+ // Don't allow interaction during flip animation
355
+ if (isFlipping.value) return
268
356
 
269
357
  // Cancel any in-progress spring animation
270
358
  if (springRaf) {
@@ -272,48 +360,132 @@ function onRibbonPointerDown(e: PointerEvent) {
272
360
  springRaf = 0
273
361
  }
274
362
 
275
- el.setPointerCapture(e.pointerId)
363
+ // Capture the pointer on the element that received the event (the band path).
364
+ // This ensures all subsequent pointer events are delivered to this element,
365
+ // preventing the browser from cancelling the pointer sequence for scrolling.
366
+ const target = e.currentTarget as Element
367
+ target.setPointerCapture(e.pointerId)
368
+ capturedTarget = target
369
+ lastPointerId = e.pointerId
370
+
276
371
  isDragging.value = true
277
- dragState = { startX: e.clientX, startY: e.clientY }
372
+ dragStart = { x: e.clientX, y: e.clientY, posX: posX.value, posY: posY.value }
278
373
  globalThis.addEventListener('pointermove', onPointerMove)
279
374
  globalThis.addEventListener('pointerup', onPointerUp)
375
+ globalThis.addEventListener('pointercancel', onPointerCancel)
280
376
  }
281
377
 
282
378
  function onPointerMove(e: PointerEvent) {
283
- if (!dragState) return
284
- stretchX.value = e.clientX - dragState.startX
285
- stretchY.value = e.clientY - dragState.startY
379
+ if (!dragStart) return
380
+
381
+ const dx = e.clientX - dragStart.x
382
+ const dy = e.clientY - dragStart.y
383
+ posX.value = dragStart.posX + dx
384
+ posY.value = dragStart.posY + dy
385
+
386
+ // Check if ribbon center has crossed viewport midpoint → trigger flip
387
+ const ribbonCenterX = posX.value + svgSizePx.value / 2
388
+ const midpoint = viewportWidth.value / 2
389
+
390
+ if (side.value === 'left' && ribbonCenterX > midpoint) {
391
+ triggerFlip('right')
392
+ } else if (side.value === 'right' && ribbonCenterX < midpoint) {
393
+ triggerFlip('left')
394
+ }
286
395
  }
287
396
 
288
- function onPointerUp() {
397
+ function onPointerUp(e?: PointerEvent) {
398
+ releasePointer(e)
289
399
  isDragging.value = false
290
- dragState = null
400
+ dragStart = null
401
+ removeListeners()
402
+
403
+ // Spring back to rest position
404
+ animateSnapBack()
405
+ }
406
+
407
+ /** Handle browser cancelling the pointer (e.g. when native scroll takes over) */
408
+ function onPointerCancel(e: PointerEvent) {
409
+ onPointerUp(e)
410
+ }
411
+
412
+ function releasePointer(e?: PointerEvent) {
413
+ if (capturedTarget && e) {
414
+ try { capturedTarget.releasePointerCapture(e.pointerId) } catch { /* already released */ }
415
+ }
416
+ capturedTarget = null
417
+ lastPointerId = null
418
+ }
419
+
420
+ function removeListeners() {
291
421
  globalThis.removeEventListener('pointermove', onPointerMove)
292
422
  globalThis.removeEventListener('pointerup', onPointerUp)
293
- animateSnapBack()
423
+ globalThis.removeEventListener('pointercancel', onPointerCancel)
294
424
  }
295
425
 
296
- /** Spring-physics animation to snap the control point back to default */
426
+ /**
427
+ * Animate the ribbon to the opposite corner.
428
+ * Ends the drag, toggles SVG paths, and uses a CSS transition
429
+ * to smoothly slide the container to its new rest position.
430
+ */
431
+ function triggerFlip(newSide: 'left' | 'right') {
432
+ // End drag
433
+ isDragging.value = false
434
+ dragStart = null
435
+ removeListeners()
436
+
437
+ // Release pointer capture
438
+ if (capturedTarget && lastPointerId !== null) {
439
+ try { capturedTarget.releasePointerCapture(lastPointerId) } catch { /* noop */ }
440
+ }
441
+ capturedTarget = null
442
+ lastPointerId = null
443
+
444
+ // Toggle side (paths flip instantly, gradient direction updates)
445
+ side.value = newSide
446
+ saveSide(newSide)
447
+
448
+ // Enable CSS transition, then set target position.
449
+ // The requestAnimationFrame ensures the browser has laid out the
450
+ // current position before the transition target is applied.
451
+ isFlipping.value = true
452
+ requestAnimationFrame(() => {
453
+ posX.value = getRestX()
454
+ posY.value = 0
455
+
456
+ flipTimeout = setTimeout(() => {
457
+ isFlipping.value = false
458
+ flipTimeout = null
459
+ }, 550)
460
+ })
461
+ }
462
+
463
+ /** Spring-physics animation to snap the ribbon back to its rest position */
297
464
  function animateSnapBack() {
465
+ const targetX = getRestX()
466
+ const targetY = 0
298
467
  const stiffness = 0.12
299
468
  const damping = 0.72
300
469
  let vx = 0
301
470
  let vy = 0
302
471
 
303
472
  function step() {
304
- vx = (vx - stiffness * stretchX.value) * damping
305
- vy = (vy - stiffness * stretchY.value) * damping
306
- stretchX.value += vx
307
- stretchY.value += vy
473
+ const dxToTarget = posX.value - targetX
474
+ const dyToTarget = posY.value - targetY
475
+
476
+ vx = (vx - stiffness * dxToTarget) * damping
477
+ vy = (vy - stiffness * dyToTarget) * damping
478
+ posX.value += vx
479
+ posY.value += vy
308
480
 
309
481
  if (
310
- Math.abs(stretchX.value) < 0.3 &&
311
- Math.abs(stretchY.value) < 0.3 &&
482
+ Math.abs(dxToTarget) < 0.3 &&
483
+ Math.abs(dyToTarget) < 0.3 &&
312
484
  Math.abs(vx) < 0.3 &&
313
485
  Math.abs(vy) < 0.3
314
486
  ) {
315
- stretchX.value = 0
316
- stretchY.value = 0
487
+ posX.value = targetX
488
+ posY.value = targetY
317
489
  springRaf = 0
318
490
  return
319
491
  }
@@ -329,14 +501,17 @@ function animateSnapBack() {
329
501
  /* ------------------------------------------------------------------ */
330
502
  onBeforeUnmount(() => {
331
503
  if (springRaf) cancelAnimationFrame(springRaf)
504
+ if (flipTimeout) clearTimeout(flipTimeout)
332
505
  globalThis.removeEventListener('pointermove', onPointerMove)
333
506
  globalThis.removeEventListener('pointerup', onPointerUp)
507
+ globalThis.removeEventListener('pointercancel', onPointerCancel)
508
+ window.removeEventListener('resize', onResize)
334
509
  })
335
510
  </script>
336
511
 
337
512
  <style scoped>
338
513
  /* ------------------------------------------------------------------ */
339
- /* Container — fixed top-left, non-blocking */
514
+ /* Container — fixed top-left, non-blocking, positioned via transform */
340
515
  /* ------------------------------------------------------------------ */
341
516
  .dcs-ribbon-container {
342
517
  position: fixed;
@@ -348,6 +523,11 @@ onBeforeUnmount(() => {
348
523
  -webkit-user-select: none;
349
524
  }
350
525
 
526
+ /* Smooth slide transition when snapping to the opposite corner */
527
+ .dcs-ribbon-container--flipping {
528
+ transition: transform 0.5s cubic-bezier(0.22, 1, 0.36, 1);
529
+ }
530
+
351
531
  /* ------------------------------------------------------------------ */
352
532
  /* SVG canvas */
353
533
  /* ------------------------------------------------------------------ */
@@ -365,6 +545,7 @@ onBeforeUnmount(() => {
365
545
  .dcs-ribbon-band {
366
546
  pointer-events: auto;
367
547
  cursor: grab;
548
+ touch-action: none;
368
549
  }
369
550
 
370
551
  .dcs-ribbon-band--dragging {