@duffcloudservices/cms 0.1.6 → 0.1.7
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 +1 -1
- package/src/components/PreviewRibbon.vue +194 -193
package/package.json
CHANGED
|
@@ -1,13 +1,7 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<Teleport to="body">
|
|
3
|
-
<div
|
|
4
|
-
v-if="isVisible"
|
|
5
|
-
class="dcs-ribbon-container"
|
|
6
|
-
:class="{ 'dcs-ribbon-container--flipping': isFlipping }"
|
|
7
|
-
:style="containerStyle"
|
|
8
|
-
>
|
|
3
|
+
<div v-if="isVisible" :class="['dcs-ribbon-container', `dcs-ribbon-container--${side}`]">
|
|
9
4
|
<svg
|
|
10
|
-
ref="ribbonSvgRef"
|
|
11
5
|
:viewBox="`0 0 ${SVG_SIZE} ${SVG_SIZE}`"
|
|
12
6
|
class="dcs-ribbon-svg"
|
|
13
7
|
>
|
|
@@ -42,8 +36,7 @@
|
|
|
42
36
|
fill="url(#dcs-ribbon-grad)"
|
|
43
37
|
stroke="rgba(255,255,255,0.08)"
|
|
44
38
|
stroke-width="1"
|
|
45
|
-
|
|
46
|
-
@pointerdown.stop.prevent="onRibbonPointerDown"
|
|
39
|
+
class="dcs-ribbon-band"
|
|
47
40
|
/>
|
|
48
41
|
|
|
49
42
|
<!-- Decorative stitched borders -->
|
|
@@ -72,6 +65,20 @@
|
|
|
72
65
|
</textPath>
|
|
73
66
|
</text>
|
|
74
67
|
</svg>
|
|
68
|
+
|
|
69
|
+
<!--
|
|
70
|
+
Touch overlay: transparent HTML <div> sitting on top of the SVG.
|
|
71
|
+
Android Chrome ignores touch-action: none on SVG <path> elements
|
|
72
|
+
because the compositor only checks HTML elements in the hit chain.
|
|
73
|
+
This HTML div with clip-path creates a reliable touch-action: none
|
|
74
|
+
hit area over the ribbon band that works on all mobile browsers.
|
|
75
|
+
-->
|
|
76
|
+
<div
|
|
77
|
+
ref="touchOverlayRef"
|
|
78
|
+
:class="['dcs-ribbon-touch', { 'dcs-ribbon-touch--dragging': isDragging }]"
|
|
79
|
+
:style="touchClipStyle"
|
|
80
|
+
@pointerdown.stop.prevent="onPointerDown"
|
|
81
|
+
/>
|
|
75
82
|
</div>
|
|
76
83
|
</Teleport>
|
|
77
84
|
</template>
|
|
@@ -169,24 +176,6 @@ function saveSide(s: 'left' | 'right') {
|
|
|
169
176
|
try { localStorage.setItem(STORAGE_KEY, s) } catch { /* noop */ }
|
|
170
177
|
}
|
|
171
178
|
|
|
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
|
-
}
|
|
189
|
-
|
|
190
179
|
/* ------------------------------------------------------------------ */
|
|
191
180
|
/* Version display */
|
|
192
181
|
/* ------------------------------------------------------------------ */
|
|
@@ -212,10 +201,6 @@ onMounted(async () => {
|
|
|
212
201
|
if (!isVisible.value) return
|
|
213
202
|
|
|
214
203
|
side.value = loadSide()
|
|
215
|
-
updateViewport()
|
|
216
|
-
posX.value = getRestX()
|
|
217
|
-
|
|
218
|
-
window.addEventListener('resize', onResize)
|
|
219
204
|
|
|
220
205
|
// Resolve version
|
|
221
206
|
if (resolvedVersion.value) return
|
|
@@ -253,239 +238,232 @@ onMounted(async () => {
|
|
|
253
238
|
})
|
|
254
239
|
|
|
255
240
|
/* ------------------------------------------------------------------ */
|
|
256
|
-
/*
|
|
241
|
+
/* Elastic stretch state (control-point displacement) */
|
|
257
242
|
/* ------------------------------------------------------------------ */
|
|
258
|
-
const
|
|
259
|
-
const
|
|
243
|
+
const stretchX = ref(0)
|
|
244
|
+
const stretchY = ref(0)
|
|
260
245
|
const isDragging = ref(false)
|
|
261
|
-
const isFlipping = ref(false)
|
|
262
246
|
|
|
263
|
-
/**
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
247
|
+
/**
|
|
248
|
+
* Absolute SVG-space control point for the band center-line.
|
|
249
|
+
* Left side default: (CP0, CP0) = (90, 90)
|
|
250
|
+
* Right side default: (SVG_SIZE - CP0, CP0) = (170, 90)
|
|
251
|
+
* stretchX/Y displace the control point in screen-mapped SVG units.
|
|
252
|
+
*/
|
|
253
|
+
const cx = computed(() =>
|
|
254
|
+
side.value === 'left'
|
|
255
|
+
? CP0 + stretchX.value
|
|
256
|
+
: SVG_SIZE - CP0 + stretchX.value,
|
|
257
|
+
)
|
|
258
|
+
const cy = computed(() => CP0 + stretchY.value)
|
|
268
259
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
260
|
+
/**
|
|
261
|
+
* Perpendicular sign: for left-side ribbon the outward perpendicular
|
|
262
|
+
* direction is (+1, +1); for right-side it is (-1, +1).
|
|
263
|
+
* This determines how CPO is applied to get outer/inner band edges.
|
|
264
|
+
*/
|
|
265
|
+
const ps = computed(() => (side.value === 'left' ? 1 : -1))
|
|
272
266
|
|
|
273
267
|
/* ------------------------------------------------------------------ */
|
|
274
|
-
/* SVG path computation (side-aware,
|
|
268
|
+
/* SVG path computation (side-aware, with elastic control-point) */
|
|
275
269
|
/* ------------------------------------------------------------------ */
|
|
276
270
|
|
|
277
271
|
/**
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
* Right side: (SVG_SIZE, y0) → (SVG_SIZE-x1, 0) via (SVG_SIZE-cx, cy)
|
|
272
|
+
* Text center-line. On the right side the path direction is reversed
|
|
273
|
+
* so that textPath renders left-to-right (readable).
|
|
281
274
|
*/
|
|
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`
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
/** Text center-line path. Reversed on right side so text reads naturally. */
|
|
290
275
|
const textPath = computed(() => {
|
|
291
276
|
if (side.value === 'left') {
|
|
292
|
-
return `M 0,${EP} Q ${
|
|
277
|
+
return `M 0,${EP} Q ${cx.value},${cy.value} ${EP},0`
|
|
293
278
|
}
|
|
294
|
-
|
|
295
|
-
return `M ${SVG_SIZE - EP},0 Q ${SVG_SIZE - CP0},${CP0} ${SVG_SIZE},${EP}`
|
|
279
|
+
return `M ${SVG_SIZE - EP},0 Q ${cx.value},${cy.value} ${SVG_SIZE},${EP}`
|
|
296
280
|
})
|
|
297
281
|
|
|
298
|
-
const outerStitchPath = computed(() =>
|
|
299
|
-
mirrorQ(
|
|
300
|
-
EP + HW - STI,
|
|
301
|
-
CP0 + CPO - 3,
|
|
302
|
-
CP0 + CPO - 3,
|
|
303
|
-
EP + HW - STI,
|
|
304
|
-
),
|
|
305
|
-
)
|
|
306
|
-
|
|
307
|
-
const innerStitchPath = computed(() =>
|
|
308
|
-
mirrorQ(
|
|
309
|
-
EP - HW + STI,
|
|
310
|
-
CP0 - CPO + 3,
|
|
311
|
-
CP0 - CPO + 3,
|
|
312
|
-
EP - HW + STI,
|
|
313
|
-
),
|
|
314
|
-
)
|
|
315
|
-
|
|
316
282
|
const bandPath = computed(() => {
|
|
317
|
-
const
|
|
318
|
-
|
|
319
|
-
const
|
|
320
|
-
const
|
|
321
|
-
|
|
322
|
-
const
|
|
323
|
-
const
|
|
324
|
-
const iCy = CP0 - CPO
|
|
283
|
+
const p = ps.value
|
|
284
|
+
// Outer edge control point (further from corner)
|
|
285
|
+
const ocx = cx.value + p * CPO
|
|
286
|
+
const ocy = cy.value + CPO
|
|
287
|
+
// Inner edge control point (closer to corner)
|
|
288
|
+
const icx = cx.value - p * CPO
|
|
289
|
+
const icy = cy.value - CPO
|
|
325
290
|
|
|
326
291
|
if (side.value === 'left') {
|
|
327
292
|
return [
|
|
328
|
-
`M 0,${
|
|
329
|
-
`L ${
|
|
330
|
-
`Q ${
|
|
293
|
+
`M 0,${EP + HW} Q ${ocx},${ocy} ${EP + HW},0`,
|
|
294
|
+
`L ${EP - HW},0`,
|
|
295
|
+
`Q ${icx},${icy} 0,${EP - HW}`,
|
|
331
296
|
`Z`,
|
|
332
297
|
].join(' ')
|
|
333
298
|
}
|
|
334
299
|
|
|
335
300
|
return [
|
|
336
|
-
`M ${SVG_SIZE},${
|
|
337
|
-
`L ${SVG_SIZE -
|
|
338
|
-
`Q ${
|
|
301
|
+
`M ${SVG_SIZE},${EP + HW} Q ${ocx},${ocy} ${SVG_SIZE - EP - HW},0`,
|
|
302
|
+
`L ${SVG_SIZE - EP + HW},0`,
|
|
303
|
+
`Q ${icx},${icy} ${SVG_SIZE},${EP - HW}`,
|
|
339
304
|
`Z`,
|
|
340
305
|
].join(' ')
|
|
341
306
|
})
|
|
342
307
|
|
|
308
|
+
const outerStitchPath = computed(() => {
|
|
309
|
+
const p = ps.value
|
|
310
|
+
const scx = cx.value + p * (CPO - 3)
|
|
311
|
+
const scy = cy.value + (CPO - 3)
|
|
312
|
+
const d = EP + HW - STI
|
|
313
|
+
|
|
314
|
+
if (side.value === 'left') {
|
|
315
|
+
return `M 0,${d} Q ${scx},${scy} ${d},0`
|
|
316
|
+
}
|
|
317
|
+
return `M ${SVG_SIZE},${d} Q ${scx},${scy} ${SVG_SIZE - d},0`
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
const innerStitchPath = computed(() => {
|
|
321
|
+
const p = ps.value
|
|
322
|
+
const scx = cx.value - p * (CPO - 3)
|
|
323
|
+
const scy = cy.value - (CPO - 3)
|
|
324
|
+
const d = EP - HW + STI
|
|
325
|
+
|
|
326
|
+
if (side.value === 'left') {
|
|
327
|
+
return `M 0,${d} Q ${scx},${scy} ${d},0`
|
|
328
|
+
}
|
|
329
|
+
return `M ${SVG_SIZE},${d} Q ${scx},${scy} ${SVG_SIZE - d},0`
|
|
330
|
+
})
|
|
331
|
+
|
|
343
332
|
/* ------------------------------------------------------------------ */
|
|
344
|
-
/*
|
|
333
|
+
/* Touch overlay clip-path */
|
|
345
334
|
/* ------------------------------------------------------------------ */
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Clip-path polygon that creates an invisible hit area over the ribbon
|
|
338
|
+
* band, wider than the visible band for comfortable touch targeting.
|
|
339
|
+
* This is on an HTML <div>, so touch-action: none is guaranteed to work
|
|
340
|
+
* on Android Chrome (unlike SVG <path> elements).
|
|
341
|
+
*/
|
|
342
|
+
const touchClipStyle = computed(() => {
|
|
343
|
+
if (side.value === 'left') {
|
|
344
|
+
// Quadrilateral covering the band diagonal: bottom-left → top-right
|
|
345
|
+
return { clipPath: 'polygon(0% 100%, 100% 0%, 65% 0%, 0% 65%)' }
|
|
346
|
+
}
|
|
347
|
+
// Right side: mirrored
|
|
348
|
+
return { clipPath: 'polygon(100% 100%, 0% 0%, 35% 0%, 100% 65%)' }
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
/* ------------------------------------------------------------------ */
|
|
352
|
+
/* Drag: elastic control-point stretch + flip on midpoint crossing */
|
|
353
|
+
/* ------------------------------------------------------------------ */
|
|
354
|
+
const touchOverlayRef = ref<HTMLDivElement | null>(null)
|
|
355
|
+
let dragStart: { x: number; y: number } | null = null
|
|
350
356
|
let springRaf = 0
|
|
351
|
-
let flipTimeout: ReturnType<typeof setTimeout> | null = null
|
|
352
357
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
358
|
+
/**
|
|
359
|
+
* Get the rendered pixel size of the SVG element.
|
|
360
|
+
* Used to convert screen-space drag deltas to SVG-space units.
|
|
361
|
+
*/
|
|
362
|
+
function getSvgPixelSize(): number {
|
|
363
|
+
return window.innerWidth <= 640 ? 200 : 260
|
|
364
|
+
}
|
|
356
365
|
|
|
366
|
+
function onPointerDown(e: PointerEvent) {
|
|
357
367
|
// Cancel any in-progress spring animation
|
|
358
368
|
if (springRaf) {
|
|
359
369
|
cancelAnimationFrame(springRaf)
|
|
360
370
|
springRaf = 0
|
|
361
371
|
}
|
|
362
372
|
|
|
363
|
-
// Capture the pointer on the
|
|
364
|
-
//
|
|
365
|
-
//
|
|
366
|
-
|
|
373
|
+
// Capture the pointer on the HTML overlay div.
|
|
374
|
+
// Unlike SVG <path> elements, HTML elements reliably support
|
|
375
|
+
// setPointerCapture on Android Chrome, ensuring all subsequent
|
|
376
|
+
// pointer events are delivered even when the finger moves outside.
|
|
377
|
+
const target = e.currentTarget as HTMLElement
|
|
367
378
|
target.setPointerCapture(e.pointerId)
|
|
368
|
-
capturedTarget = target
|
|
369
|
-
lastPointerId = e.pointerId
|
|
370
379
|
|
|
371
380
|
isDragging.value = true
|
|
372
|
-
dragStart = { x: e.clientX, y: e.clientY
|
|
381
|
+
dragStart = { x: e.clientX, y: e.clientY }
|
|
382
|
+
|
|
373
383
|
globalThis.addEventListener('pointermove', onPointerMove)
|
|
374
384
|
globalThis.addEventListener('pointerup', onPointerUp)
|
|
375
|
-
globalThis.addEventListener('pointercancel',
|
|
385
|
+
globalThis.addEventListener('pointercancel', onPointerUp)
|
|
376
386
|
}
|
|
377
387
|
|
|
378
388
|
function onPointerMove(e: PointerEvent) {
|
|
379
389
|
if (!dragStart) return
|
|
380
390
|
|
|
381
|
-
|
|
382
|
-
const
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
//
|
|
387
|
-
const
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
if (side.value === '
|
|
391
|
-
triggerFlip('
|
|
392
|
-
} else if (side.value === 'right' && ribbonCenterX < midpoint) {
|
|
393
|
-
triggerFlip('left')
|
|
391
|
+
// Convert screen-space drag delta to SVG-space units
|
|
392
|
+
const scale = SVG_SIZE / getSvgPixelSize()
|
|
393
|
+
stretchX.value = (e.clientX - dragStart.x) * scale
|
|
394
|
+
stretchY.value = (e.clientY - dragStart.y) * scale
|
|
395
|
+
|
|
396
|
+
// Flip when the pointer crosses the viewport midpoint
|
|
397
|
+
const mid = window.innerWidth / 2
|
|
398
|
+
if (side.value === 'left' && e.clientX > mid) {
|
|
399
|
+
triggerFlip('right', e)
|
|
400
|
+
} else if (side.value === 'right' && e.clientX < mid) {
|
|
401
|
+
triggerFlip('left', e)
|
|
394
402
|
}
|
|
395
403
|
}
|
|
396
404
|
|
|
397
|
-
function onPointerUp(e
|
|
398
|
-
|
|
405
|
+
function onPointerUp(e: PointerEvent) {
|
|
406
|
+
if (touchOverlayRef.value) {
|
|
407
|
+
try { touchOverlayRef.value.releasePointerCapture(e.pointerId) } catch { /* already released */ }
|
|
408
|
+
}
|
|
399
409
|
isDragging.value = false
|
|
400
410
|
dragStart = null
|
|
401
411
|
removeListeners()
|
|
402
|
-
|
|
403
|
-
// Spring back to rest position
|
|
404
412
|
animateSnapBack()
|
|
405
413
|
}
|
|
406
414
|
|
|
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
415
|
function removeListeners() {
|
|
421
416
|
globalThis.removeEventListener('pointermove', onPointerMove)
|
|
422
417
|
globalThis.removeEventListener('pointerup', onPointerUp)
|
|
423
|
-
globalThis.removeEventListener('pointercancel',
|
|
418
|
+
globalThis.removeEventListener('pointercancel', onPointerUp)
|
|
424
419
|
}
|
|
425
420
|
|
|
426
421
|
/**
|
|
427
|
-
*
|
|
428
|
-
*
|
|
429
|
-
*
|
|
422
|
+
* Snap the ribbon to the opposite corner when the pointer crosses
|
|
423
|
+
* the viewport midpoint. Resets the elastic stretch, switches side,
|
|
424
|
+
* and persists the user's preference.
|
|
430
425
|
*/
|
|
431
|
-
function triggerFlip(newSide: 'left' | 'right') {
|
|
426
|
+
function triggerFlip(newSide: 'left' | 'right', e: PointerEvent) {
|
|
427
|
+
// Reset stretch to default shape
|
|
428
|
+
stretchX.value = 0
|
|
429
|
+
stretchY.value = 0
|
|
430
|
+
|
|
432
431
|
// End drag
|
|
433
432
|
isDragging.value = false
|
|
434
433
|
dragStart = null
|
|
435
434
|
removeListeners()
|
|
436
435
|
|
|
437
436
|
// Release pointer capture
|
|
438
|
-
if (
|
|
439
|
-
try {
|
|
437
|
+
if (touchOverlayRef.value) {
|
|
438
|
+
try { touchOverlayRef.value.releasePointerCapture(e.pointerId) } catch { /* noop */ }
|
|
440
439
|
}
|
|
441
|
-
capturedTarget = null
|
|
442
|
-
lastPointerId = null
|
|
443
440
|
|
|
444
|
-
//
|
|
441
|
+
// Switch to the new side
|
|
445
442
|
side.value = newSide
|
|
446
443
|
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
444
|
}
|
|
462
445
|
|
|
463
|
-
/** Spring-physics animation to snap the
|
|
446
|
+
/** Spring-physics animation to snap the control point back to default */
|
|
464
447
|
function animateSnapBack() {
|
|
465
|
-
const targetX = getRestX()
|
|
466
|
-
const targetY = 0
|
|
467
448
|
const stiffness = 0.12
|
|
468
449
|
const damping = 0.72
|
|
469
450
|
let vx = 0
|
|
470
451
|
let vy = 0
|
|
471
452
|
|
|
472
453
|
function step() {
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
vy = (vy - stiffness * dyToTarget) * damping
|
|
478
|
-
posX.value += vx
|
|
479
|
-
posY.value += vy
|
|
454
|
+
vx = (vx - stiffness * stretchX.value) * damping
|
|
455
|
+
vy = (vy - stiffness * stretchY.value) * damping
|
|
456
|
+
stretchX.value += vx
|
|
457
|
+
stretchY.value += vy
|
|
480
458
|
|
|
481
459
|
if (
|
|
482
|
-
Math.abs(
|
|
483
|
-
Math.abs(
|
|
460
|
+
Math.abs(stretchX.value) < 0.3 &&
|
|
461
|
+
Math.abs(stretchY.value) < 0.3 &&
|
|
484
462
|
Math.abs(vx) < 0.3 &&
|
|
485
463
|
Math.abs(vy) < 0.3
|
|
486
464
|
) {
|
|
487
|
-
|
|
488
|
-
|
|
465
|
+
stretchX.value = 0
|
|
466
|
+
stretchY.value = 0
|
|
489
467
|
springRaf = 0
|
|
490
468
|
return
|
|
491
469
|
}
|
|
@@ -501,31 +479,31 @@ function animateSnapBack() {
|
|
|
501
479
|
/* ------------------------------------------------------------------ */
|
|
502
480
|
onBeforeUnmount(() => {
|
|
503
481
|
if (springRaf) cancelAnimationFrame(springRaf)
|
|
504
|
-
|
|
505
|
-
globalThis.removeEventListener('pointermove', onPointerMove)
|
|
506
|
-
globalThis.removeEventListener('pointerup', onPointerUp)
|
|
507
|
-
globalThis.removeEventListener('pointercancel', onPointerCancel)
|
|
508
|
-
window.removeEventListener('resize', onResize)
|
|
482
|
+
removeListeners()
|
|
509
483
|
})
|
|
510
484
|
</script>
|
|
511
485
|
|
|
512
486
|
<style scoped>
|
|
513
487
|
/* ------------------------------------------------------------------ */
|
|
514
|
-
/* Container — fixed
|
|
488
|
+
/* Container — fixed in corner, non-blocking */
|
|
515
489
|
/* ------------------------------------------------------------------ */
|
|
516
490
|
.dcs-ribbon-container {
|
|
517
491
|
position: fixed;
|
|
518
492
|
top: 0;
|
|
519
|
-
left: 0;
|
|
520
493
|
z-index: 2147483647;
|
|
521
494
|
pointer-events: none;
|
|
522
495
|
user-select: none;
|
|
523
496
|
-webkit-user-select: none;
|
|
524
497
|
}
|
|
525
498
|
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
499
|
+
.dcs-ribbon-container--left {
|
|
500
|
+
left: 0;
|
|
501
|
+
right: auto;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
.dcs-ribbon-container--right {
|
|
505
|
+
right: 0;
|
|
506
|
+
left: auto;
|
|
529
507
|
}
|
|
530
508
|
|
|
531
509
|
/* ------------------------------------------------------------------ */
|
|
@@ -540,16 +518,10 @@ onBeforeUnmount(() => {
|
|
|
540
518
|
}
|
|
541
519
|
|
|
542
520
|
/* ------------------------------------------------------------------ */
|
|
543
|
-
/* Band (
|
|
521
|
+
/* Band (visual only — touch handled by overlay div) */
|
|
544
522
|
/* ------------------------------------------------------------------ */
|
|
545
523
|
.dcs-ribbon-band {
|
|
546
|
-
pointer-events:
|
|
547
|
-
cursor: grab;
|
|
548
|
-
touch-action: none;
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
.dcs-ribbon-band--dragging {
|
|
552
|
-
cursor: grabbing;
|
|
524
|
+
pointer-events: none;
|
|
553
525
|
}
|
|
554
526
|
|
|
555
527
|
/* ------------------------------------------------------------------ */
|
|
@@ -584,12 +556,41 @@ onBeforeUnmount(() => {
|
|
|
584
556
|
display: none;
|
|
585
557
|
}
|
|
586
558
|
|
|
559
|
+
/* ------------------------------------------------------------------ */
|
|
560
|
+
/* Touch overlay — transparent HTML div for reliable mobile touch */
|
|
561
|
+
/* */
|
|
562
|
+
/* Android Chrome ignores touch-action on SVG <path> elements because */
|
|
563
|
+
/* the compositor only walks HTML elements. This HTML <div> with */
|
|
564
|
+
/* clip-path creates a shaped, invisible hit area where touch-action */
|
|
565
|
+
/* is guaranteed to work. */
|
|
566
|
+
/* ------------------------------------------------------------------ */
|
|
567
|
+
.dcs-ribbon-touch {
|
|
568
|
+
position: absolute;
|
|
569
|
+
top: 0;
|
|
570
|
+
left: 0;
|
|
571
|
+
width: 260px;
|
|
572
|
+
height: 260px;
|
|
573
|
+
touch-action: none;
|
|
574
|
+
pointer-events: auto;
|
|
575
|
+
cursor: grab;
|
|
576
|
+
-webkit-tap-highlight-color: transparent;
|
|
577
|
+
/* clip-path set via :style binding for left/right side */
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
.dcs-ribbon-touch--dragging {
|
|
581
|
+
cursor: grabbing;
|
|
582
|
+
}
|
|
583
|
+
|
|
587
584
|
/* Compact label for narrow screens */
|
|
588
585
|
@media (max-width: 640px) {
|
|
589
586
|
.dcs-ribbon-svg {
|
|
590
587
|
width: 200px;
|
|
591
588
|
height: 200px;
|
|
592
589
|
}
|
|
590
|
+
.dcs-ribbon-touch {
|
|
591
|
+
width: 200px;
|
|
592
|
+
height: 200px;
|
|
593
|
+
}
|
|
593
594
|
.dcs-ribbon-text--full {
|
|
594
595
|
display: none;
|
|
595
596
|
}
|