hlassiege/darylpublic Fork
⑂ 22449ed
Commits
⬇ Clone ▾
git clone https://git.rickub.com/hlassiege/daryl.git
git clone ssh://git@rickub.com/hlassiege/daryl.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Download ZIP

feat(derush): navigate the timeline like the editing one

Alt+wheel zooms on the playhead, which stays centered; Ctrl+wheel moves
the playhead with the timeline following it; the wheel alone scrolls the
page. A jump off-screen while paused recenters the playhead, scrubbing
the ruler snaps to cuts (Alt: free), and trim edges show [ and ] cursors.
hlassiege committed 2026-10-12T10:43:46+02:00 Browse files
22449ed parent: 8566bea
modified web/src/i18n/locales/en/derush.ts +2 -2
@@ -138,7 +138,7 @@ export default {
138138 splitKey: "Split the piece under the playhead (Ctrl + B or S)",
139139 undo: "Undo (Ctrl + Z)",
140140 redo: "Redo (Ctrl + Y)",
141- zoomIn: "Zoom in (+)",
141+ zoomIn: "Zoom in (+) · Alt+scroll",
142142 zoomOut: "Zoom out (−)",
143143 fit: "Fit",
144144 },
@@ -146,5 +146,5 @@ export default {
146146 trim: "Drag to trim or extend (Alt: no snapping)",
147147 nested: "Nested timeline “{name}” · double-click to open it",
148148 },
149- shortcuts: "Drag a piece to move it · drag its edges to trim · B blade, A select · Delete remove · S split here · ← → frame (Shift: second) · ↑ ↓ prev. / next piece · I / O range · Ctrl + Z undo · Ctrl + wheel zoom",
149+ shortcuts: "Drag a piece to move it · drag its edges to trim · B blade, A select · Delete remove · S split here · ← → frame (Shift: second) · ↑ ↓ prev. / next piece · I / O range · Ctrl + Z undo · Ctrl + wheel move playhead · Alt + wheel zoom",
150150 };
@@ -138,7 +138,7 @@ export default {
138 splitKey: "Split the piece under the playhead (Ctrl + B or S)",138 splitKey: "Split the piece under the playhead (Ctrl + B or S)",
139 undo: "Undo (Ctrl + Z)",139 undo: "Undo (Ctrl + Z)",
140 redo: "Redo (Ctrl + Y)",140 redo: "Redo (Ctrl + Y)",
141- zoomIn: "Zoom in (+)",141+ zoomIn: "Zoom in (+) · Alt+scroll",
142 zoomOut: "Zoom out (−)",142 zoomOut: "Zoom out (−)",
143 fit: "Fit",143 fit: "Fit",
144 },144 },
@@ -146,5 +146,5 @@ export default {
146 trim: "Drag to trim or extend (Alt: no snapping)",146 trim: "Drag to trim or extend (Alt: no snapping)",
147 nested: "Nested timeline “{name}” · double-click to open it",147 nested: "Nested timeline “{name}” · double-click to open it",
148 },148 },
149- shortcuts: "Drag a piece to move it · drag its edges to trim · B blade, A select · Delete remove · S split here · ← → frame (Shift: second) · ↑ ↓ prev. / next piece · I / O range · Ctrl + Z undo · Ctrl + wheel zoom",149+ shortcuts: "Drag a piece to move it · drag its edges to trim · B blade, A select · Delete remove · S split here · ← → frame (Shift: second) · ↑ ↓ prev. / next piece · I / O range · Ctrl + Z undo · Ctrl + wheel move playhead · Alt + wheel zoom",
150 };150 };
modified web/src/i18n/locales/fr/derush.ts +2 -2
@@ -138,7 +138,7 @@ export default {
138138 splitKey: "Couper le bout sous la tête de lecture (Ctrl + B ou S)",
139139 undo: "Annuler (Ctrl + Z)",
140140 redo: "Refaire (Ctrl + Y)",
141- zoomIn: "Zoomer (+)",
141+ zoomIn: "Zoomer (+) · Alt+molette",
142142 zoomOut: "Dézoomer (−)",
143143 fit: "Ajuster",
144144 },
@@ -146,5 +146,5 @@ export default {
146146 trim: "Tire pour rogner ou rallonger (Alt : sans aimantation)",
147147 nested: "Timeline imbriquée « {name} » · double-clic pour l’ouvrir",
148148 },
149- shortcuts: "Glisse un bout pour le déplacer · tire ses bords pour le rogner · B lame, A sélection · Suppr supprimer · S couper ici · ← → image (Maj : seconde) · ↑ ↓ bout préc. / suiv. · I / O plage · Ctrl + Z annuler · Ctrl + molette zoom",
149+ shortcuts: "Glisse un bout pour le déplacer · tire ses bords pour le rogner · B lame, A sélection · Suppr supprimer · S couper ici · ← → image (Maj : seconde) · ↑ ↓ bout préc. / suiv. · I / O plage · Ctrl + Z annuler · Ctrl + molette déplacer la tête · Alt + molette zoom",
150150 };
@@ -138,7 +138,7 @@ export default {
138 splitKey: "Couper le bout sous la tête de lecture (Ctrl + B ou S)",138 splitKey: "Couper le bout sous la tête de lecture (Ctrl + B ou S)",
139 undo: "Annuler (Ctrl + Z)",139 undo: "Annuler (Ctrl + Z)",
140 redo: "Refaire (Ctrl + Y)",140 redo: "Refaire (Ctrl + Y)",
141- zoomIn: "Zoomer (+)",141+ zoomIn: "Zoomer (+) · Alt+molette",
142 zoomOut: "Dézoomer (−)",142 zoomOut: "Dézoomer (−)",
143 fit: "Ajuster",143 fit: "Ajuster",
144 },144 },
@@ -146,5 +146,5 @@ export default {
146 trim: "Tire pour rogner ou rallonger (Alt : sans aimantation)",146 trim: "Tire pour rogner ou rallonger (Alt : sans aimantation)",
147 nested: "Timeline imbriquée « {name} » · double-clic pour l’ouvrir",147 nested: "Timeline imbriquée « {name} » · double-clic pour l’ouvrir",
148 },148 },
149- shortcuts: "Glisse un bout pour le déplacer · tire ses bords pour le rogner · B lame, A sélection · Suppr supprimer · S couper ici · ← → image (Maj : seconde) · ↑ ↓ bout préc. / suiv. · I / O plage · Ctrl + Z annuler · Ctrl + molette zoom",149+ shortcuts: "Glisse un bout pour le déplacer · tire ses bords pour le rogner · B lame, A sélection · Suppr supprimer · S couper ici · ← → image (Maj : seconde) · ↑ ↓ bout préc. / suiv. · I / O plage · Ctrl + Z annuler · Ctrl + molette déplacer la tête · Alt + molette zoom",
150 };150 };
modified web/src/project/steps/derush/Timeline.vue +32 -19
@@ -3,7 +3,9 @@
33 * La timeline du dérush, façon logiciel de montage : des bouts de rushes bout à bout (V1) et leur son (A1).
44 * On attrape un bout pour le déplacer, on tire ses bords pour le rogner ou le rallonger (aimanté aux
55 * phrases et aux mots, Alt pour s'en passer), la lame le coupe en deux. Glisser sur A1 sélectionne une plage.
6- * Ctrl + molette zoome autour de la souris, la molette fait défiler. Dans chaque bout, une bande marque
6+ * Alt + molette zoome sur la tête de lecture, Ctrl + molette la déplace (elle reste au milieu de l'écran, comme dans
7+ * Filmora ; la molette seule fait défiler la page). Tirée sur la règle, la tête s'aimante aux coupes (Alt : sans aimant).
8+ * Dans chaque bout, une bande marque
79 * la parole (les trous sont des silences, les pointillés du son non transcrit) ; zoomé, le texte des phrases s'affiche.
810 * Une timeline imbriquée est un seul bloc (double-clic : on l'ouvre). On y dépose rushes et timelines de la médiathèque.
911 */
@@ -59,9 +61,10 @@ function setZoom(next: number, anchor: number, anchorPx: number) {
5961 if (scroller.value) scroller.value.scrollLeft = Math.max(0, anchor * pps.value - anchorPx);
6062 });
6163 }
64+// comme dans Filmora : on zoome sur la tête de lecture, qui reste au milieu de l'écran (près d'un bout de la
65+// timeline, elle s'en écarte le temps qu'il faut)
6266 function zoomBy(factor: number) {
63- const at = props.player.t.value;
64- setZoom(pps.value * factor, at, Math.max(0, Math.min(viewW.value, x(at) - scrollX.value)));
67+ setZoom(pps.value * factor, props.player.t.value, viewW.value / 2);
6568 }
6669 function fit() {
6770 setZoom(minPps.value, 0, 0);
@@ -69,14 +72,16 @@ function fit() {
6972 defineExpose({ zoomBy, fit });
7073
7174 function onWheel(e: WheelEvent) {
72- if (e.ctrlKey || e.metaKey) {
75+ // la molette seule fait défiler la page
76+ if (e.altKey) {
7377 e.preventDefault();
74- const rect = scroller.value!.getBoundingClientRect();
75- const px = e.clientX - rect.left;
76- setZoom(pps.value * Math.pow(1.0015, -e.deltaY), (scrollX.value + px) / pps.value, px);
77- } else if (!e.shiftKey && Math.abs(e.deltaY) > Math.abs(e.deltaX)) {
78+ zoomBy(Math.pow(1.0015, -e.deltaY));
79+ } else if ((e.ctrlKey || e.metaKey) && Math.abs(e.deltaY) > Math.abs(e.deltaX)) {
80+ // on se déplace avec la tête de lecture, qui reste au milieu de l'écran
7881 e.preventDefault();
79- scroller.value!.scrollLeft += e.deltaY;
82+ const t = Math.max(0, Math.min(props.player.t.value + e.deltaY / pps.value, total.value));
83+ props.player.seek(t);
84+ scroller.value!.scrollLeft = Math.max(0, x(t) - viewW.value / 2);
8085 }
8186 }
8287
@@ -89,13 +94,13 @@ onMounted(() => {
8994 onUnmounted(() => ro.disconnect());
9095 watch(total, (n, old) => { if (!old && n) nextTick(fit); else if (pps.value < minPps.value) pps.value = minPps.value; });
9196
92-// la tête de lecture reste visible : pendant la lecture on tourne la page, après un saut on la ramène
97+// la tête reste visible : en lecture on tourne la page quand elle sort de l'écran ; un saut (flèches, bout
98+// suivant, ±1 s…) hors de l'écran la ramène au centre
9399 watch(headX, (px) => {
94100 const el = scroller.value;
95101 if (!el || drag) return;
96- if (px > scrollX.value + viewW.value - 30 || px < scrollX.value) {
97- el.scrollLeft = Math.max(0, props.player.playing.value ? px - 60 : px - viewW.value / 3);
98- }
102+ if (px <= scrollX.value + viewW.value - 30 && px >= scrollX.value) return;
103+ el.scrollLeft = Math.max(0, props.player.playing.value ? px - 60 : px - viewW.value / 2);
99104 });
100105
101106 // --- règle
@@ -178,7 +183,7 @@ const timeAt = (e: PointerEvent | MouseEvent) => {
178183 };
179184
180185 type Drag =
181- | { kind: "scrub" }
186+ | { kind: "scrub"; pts: number[] }
182187 | { kind: "range"; a: number; moved: boolean }
183188 | { kind: "clip"; id: string; d0: number; moved: boolean; additive: boolean }
184189 | { kind: "trim"; i: number; side: "in" | "out"; edge: number; d0: number; to: number; pts: number[]; min: number; max: number };
@@ -193,8 +198,9 @@ function capture(e: PointerEvent) {
193198 function downRuler(e: PointerEvent) {
194199 if (e.button !== 0) return;
195200 e.stopPropagation();
196- drag = { kind: "scrub" };
197- props.player.seek(timeAt(e));
201+ // la tête tirée sur la règle s'aimante aux coupes (sans sa propre position, où elle resterait collée)
202+ drag = { kind: "scrub", pts: [0, total.value, ...starts.value].filter((p) => p !== props.player.t.value) };
203+ props.player.seek(e.altKey ? timeAt(e) : snap(timeAt(e), drag.pts));
198204 capture(e);
199205 }
200206
@@ -249,7 +255,7 @@ function onMove(e: PointerEvent) {
249255 if (!drag) return;
250256 const d = timeAt(e);
251257 if (drag.kind === "scrub") {
252- props.player.seek(d);
258+ props.player.seek(e.altKey ? d : snap(d, drag.pts));
253259 } else if (drag.kind === "range") {
254260 if (!drag.moved && Math.abs(d - drag.a) * pps.value < 4) return;
255261 drag.moved = true;
@@ -419,8 +425,15 @@ const shortName = (n: string) => n.replace(/\.[^.]+$/, "");
419425 .mark.voice { background: transparent; border-bottom: 4px dotted rgba(255, 255, 255, .7); }
420426 .mark em { display: block; font-style: normal; font-size: 10px; line-height: 1.25; color: #fff; padding: 2px 4px; white-space: normal; max-height: 26px; overflow: hidden; }
421427 .edge { position: absolute; top: 0; bottom: 0; width: 8px; cursor: ew-resize; z-index: 4; }
422-.edge.l { left: 0; }
423-.edge.r { right: 0; }
428+/* comme dans Filmora : [ rogne le début du bout sous la souris, ] sa fin (entre deux bouts collés, on voit lequel) */
429+.edge.l {
430+ left: 0;
431+ cursor: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none' stroke-linecap='round' stroke-linejoin='round'><path d='M16 4H12V20H16M8 12H2M4 10L2 12L4 14' stroke='white' stroke-width='4'/><path d='M16 4H12V20H16M8 12H2M4 10L2 12L4 14' stroke='black' stroke-width='2'/></svg>") 12 12, w-resize;
432+}
433+.edge.r {
434+ right: 0;
435+ cursor: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none' stroke-linecap='round' stroke-linejoin='round'><path d='M8 4H12V20H8M16 12H22M20 10L22 12L20 14' stroke='white' stroke-width='4'/><path d='M8 4H12V20H8M16 12H22M20 10L22 12L20 14' stroke='black' stroke-width='2'/></svg>") 12 12, e-resize;
436+}
424437 .edge:hover { background: rgba(229, 72, 77, .55); }
425438 .blade .edge { display: none; }
426439 .drop-at { position: absolute; width: 3px; margin-left: -1.5px; background: #3B82C4; border-radius: 2px; z-index: 7; pointer-events: none; }
@@ -3,7 +3,9 @@
3 * La timeline du dérush, façon logiciel de montage : des bouts de rushes bout à bout (V1) et leur son (A1).3 * La timeline du dérush, façon logiciel de montage : des bouts de rushes bout à bout (V1) et leur son (A1).
4 * On attrape un bout pour le déplacer, on tire ses bords pour le rogner ou le rallonger (aimanté aux4 * On attrape un bout pour le déplacer, on tire ses bords pour le rogner ou le rallonger (aimanté aux
5 * phrases et aux mots, Alt pour s'en passer), la lame le coupe en deux. Glisser sur A1 sélectionne une plage.5 * phrases et aux mots, Alt pour s'en passer), la lame le coupe en deux. Glisser sur A1 sélectionne une plage.
6- * Ctrl + molette zoome autour de la souris, la molette fait défiler. Dans chaque bout, une bande marque6+ * Alt + molette zoome sur la tête de lecture, Ctrl + molette la déplace (elle reste au milieu de l'écran, comme dans
7+ * Filmora ; la molette seule fait défiler la page). Tirée sur la règle, la tête s'aimante aux coupes (Alt : sans aimant).
8+ * Dans chaque bout, une bande marque
7 * la parole (les trous sont des silences, les pointillés du son non transcrit) ; zoomé, le texte des phrases s'affiche.9 * la parole (les trous sont des silences, les pointillés du son non transcrit) ; zoomé, le texte des phrases s'affiche.
8 * Une timeline imbriquée est un seul bloc (double-clic : on l'ouvre). On y dépose rushes et timelines de la médiathèque.10 * Une timeline imbriquée est un seul bloc (double-clic : on l'ouvre). On y dépose rushes et timelines de la médiathèque.
9 */11 */
@@ -59,9 +61,10 @@ function setZoom(next: number, anchor: number, anchorPx: number) {
59 if (scroller.value) scroller.value.scrollLeft = Math.max(0, anchor * pps.value - anchorPx);61 if (scroller.value) scroller.value.scrollLeft = Math.max(0, anchor * pps.value - anchorPx);
60 });62 });
61 }63 }
64+// comme dans Filmora : on zoome sur la tête de lecture, qui reste au milieu de l'écran (près d'un bout de la
65+// timeline, elle s'en écarte le temps qu'il faut)
62 function zoomBy(factor: number) {66 function zoomBy(factor: number) {
63- const at = props.player.t.value;67+ setZoom(pps.value * factor, props.player.t.value, viewW.value / 2);
64- setZoom(pps.value * factor, at, Math.max(0, Math.min(viewW.value, x(at) - scrollX.value)));
65 }68 }
66 function fit() {69 function fit() {
67 setZoom(minPps.value, 0, 0);70 setZoom(minPps.value, 0, 0);
@@ -69,14 +72,16 @@ function fit() {
69 defineExpose({ zoomBy, fit });72 defineExpose({ zoomBy, fit });
70 73
71 function onWheel(e: WheelEvent) {74 function onWheel(e: WheelEvent) {
72- if (e.ctrlKey || e.metaKey) {75+ // la molette seule fait défiler la page
76+ if (e.altKey) {
73 e.preventDefault();77 e.preventDefault();
74- const rect = scroller.value!.getBoundingClientRect();78+ zoomBy(Math.pow(1.0015, -e.deltaY));
75- const px = e.clientX - rect.left;79+ } else if ((e.ctrlKey || e.metaKey) && Math.abs(e.deltaY) > Math.abs(e.deltaX)) {
76- setZoom(pps.value * Math.pow(1.0015, -e.deltaY), (scrollX.value + px) / pps.value, px);80+ // on se déplace avec la tête de lecture, qui reste au milieu de l'écran
77- } else if (!e.shiftKey && Math.abs(e.deltaY) > Math.abs(e.deltaX)) {
78 e.preventDefault();81 e.preventDefault();
79- scroller.value!.scrollLeft += e.deltaY;82+ const t = Math.max(0, Math.min(props.player.t.value + e.deltaY / pps.value, total.value));
83+ props.player.seek(t);
84+ scroller.value!.scrollLeft = Math.max(0, x(t) - viewW.value / 2);
80 }85 }
81 }86 }
82 87
@@ -89,13 +94,13 @@ onMounted(() => {
89 onUnmounted(() => ro.disconnect());94 onUnmounted(() => ro.disconnect());
90 watch(total, (n, old) => { if (!old && n) nextTick(fit); else if (pps.value < minPps.value) pps.value = minPps.value; });95 watch(total, (n, old) => { if (!old && n) nextTick(fit); else if (pps.value < minPps.value) pps.value = minPps.value; });
91 96
92-// la tête de lecture reste visible : pendant la lecture on tourne la page, après un saut on la ramène97+// la tête reste visible : en lecture on tourne la page quand elle sort de l'écran ; un saut (flèches, bout
98+// suivant, ±1 s…) hors de l'écran la ramène au centre
93 watch(headX, (px) => {99 watch(headX, (px) => {
94 const el = scroller.value;100 const el = scroller.value;
95 if (!el || drag) return;101 if (!el || drag) return;
96- if (px > scrollX.value + viewW.value - 30 || px < scrollX.value) {102+ if (px <= scrollX.value + viewW.value - 30 && px >= scrollX.value) return;
97- el.scrollLeft = Math.max(0, props.player.playing.value ? px - 60 : px - viewW.value / 3);103+ el.scrollLeft = Math.max(0, props.player.playing.value ? px - 60 : px - viewW.value / 2);
98- }
99 });104 });
100 105
101 // --- règle106 // --- règle
@@ -178,7 +183,7 @@ const timeAt = (e: PointerEvent | MouseEvent) => {
178 };183 };
179 184
180 type Drag =185 type Drag =
181- | { kind: "scrub" }186+ | { kind: "scrub"; pts: number[] }
182 | { kind: "range"; a: number; moved: boolean }187 | { kind: "range"; a: number; moved: boolean }
183 | { kind: "clip"; id: string; d0: number; moved: boolean; additive: boolean }188 | { kind: "clip"; id: string; d0: number; moved: boolean; additive: boolean }
184 | { kind: "trim"; i: number; side: "in" | "out"; edge: number; d0: number; to: number; pts: number[]; min: number; max: number };189 | { kind: "trim"; i: number; side: "in" | "out"; edge: number; d0: number; to: number; pts: number[]; min: number; max: number };
@@ -193,8 +198,9 @@ function capture(e: PointerEvent) {
193 function downRuler(e: PointerEvent) {198 function downRuler(e: PointerEvent) {
194 if (e.button !== 0) return;199 if (e.button !== 0) return;
195 e.stopPropagation();200 e.stopPropagation();
196- drag = { kind: "scrub" };201+ // la tête tirée sur la règle s'aimante aux coupes (sans sa propre position, où elle resterait collée)
197- props.player.seek(timeAt(e));202+ drag = { kind: "scrub", pts: [0, total.value, ...starts.value].filter((p) => p !== props.player.t.value) };
203+ props.player.seek(e.altKey ? timeAt(e) : snap(timeAt(e), drag.pts));
198 capture(e);204 capture(e);
199 }205 }
200 206
@@ -249,7 +255,7 @@ function onMove(e: PointerEvent) {
249 if (!drag) return;255 if (!drag) return;
250 const d = timeAt(e);256 const d = timeAt(e);
251 if (drag.kind === "scrub") {257 if (drag.kind === "scrub") {
252- props.player.seek(d);258+ props.player.seek(e.altKey ? d : snap(d, drag.pts));
253 } else if (drag.kind === "range") {259 } else if (drag.kind === "range") {
254 if (!drag.moved && Math.abs(d - drag.a) * pps.value < 4) return;260 if (!drag.moved && Math.abs(d - drag.a) * pps.value < 4) return;
255 drag.moved = true;261 drag.moved = true;
@@ -419,8 +425,15 @@ const shortName = (n: string) => n.replace(/\.[^.]+$/, "");
419 .mark.voice { background: transparent; border-bottom: 4px dotted rgba(255, 255, 255, .7); }425 .mark.voice { background: transparent; border-bottom: 4px dotted rgba(255, 255, 255, .7); }
420 .mark em { display: block; font-style: normal; font-size: 10px; line-height: 1.25; color: #fff; padding: 2px 4px; white-space: normal; max-height: 26px; overflow: hidden; }426 .mark em { display: block; font-style: normal; font-size: 10px; line-height: 1.25; color: #fff; padding: 2px 4px; white-space: normal; max-height: 26px; overflow: hidden; }
421 .edge { position: absolute; top: 0; bottom: 0; width: 8px; cursor: ew-resize; z-index: 4; }427 .edge { position: absolute; top: 0; bottom: 0; width: 8px; cursor: ew-resize; z-index: 4; }
422-.edge.l { left: 0; }428+/* comme dans Filmora : [ rogne le début du bout sous la souris, ] sa fin (entre deux bouts collés, on voit lequel) */
423-.edge.r { right: 0; }429+.edge.l {
430+ left: 0;
431+ cursor: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none' stroke-linecap='round' stroke-linejoin='round'><path d='M16 4H12V20H16M8 12H2M4 10L2 12L4 14' stroke='white' stroke-width='4'/><path d='M16 4H12V20H16M8 12H2M4 10L2 12L4 14' stroke='black' stroke-width='2'/></svg>") 12 12, w-resize;
432+}
433+.edge.r {
434+ right: 0;
435+ cursor: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none' stroke-linecap='round' stroke-linejoin='round'><path d='M8 4H12V20H8M16 12H22M20 10L22 12L20 14' stroke='white' stroke-width='4'/><path d='M8 4H12V20H8M16 12H22M20 10L22 12L20 14' stroke='black' stroke-width='2'/></svg>") 12 12, e-resize;
436+}
424 .edge:hover { background: rgba(229, 72, 77, .55); }437 .edge:hover { background: rgba(229, 72, 77, .55); }
425 .blade .edge { display: none; }438 .blade .edge { display: none; }
426 .drop-at { position: absolute; width: 3px; margin-left: -1.5px; background: #3B82C4; border-radius: 2px; z-index: 7; pointer-events: none; }439 .drop-at { position: absolute; width: 3px; margin-left: -1.5px; background: #3B82C4; border-radius: 2px; z-index: 7; pointer-events: none; }