fix(touch): pointer event parity for slash commands, collab overlay, canvas, and wallet charts

Convert remaining mouse-only event listeners to Pointer Events so touch
and pen inputs work identically to mouse across rApps and canvas:

- rnotes slash-command: mousedown/mouseenter → pointerdown/pointerenter
- collab overlay: mousemove → pointermove for cursor broadcast, click → pointerdown for collab-id tracking
- canvas.html: toolbar label reveal gated behind @media (hover: hover), :active fallbacks on 12 button selectors, JS mouseenter/mouseleave → pointerenter/pointerleave
- wallet-viz D3 charts: all mouseover/mousemove/mouseout → pointerenter/pointermove/pointerleave on transaction paths, balance area, Sankey links, and reset button

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-03-31 11:06:44 -07:00
parent 690e4dedb4
commit 5bc64a5b4e
4 changed files with 43 additions and 39 deletions

View File

@ -172,12 +172,12 @@ export function createSlashCommandPlugin(editor: Editor, shadowRoot: ShadowRoot)
// Click handlers
menuEl.querySelectorAll('.slash-menu-item').forEach((el) => {
el.addEventListener('mousedown', (e) => {
el.addEventListener('pointerdown', (e) => {
e.preventDefault();
const idx = parseInt((el as HTMLElement).dataset.index || '0');
executeItem(idx);
});
el.addEventListener('mouseenter', () => {
el.addEventListener('pointerenter', () => {
selectedIndex = parseInt((el as HTMLElement).dataset.index || '0');
updateMenuContent();
});

View File

@ -70,8 +70,8 @@ function addResetButton(parent: HTMLElement, svg: any, zoom: any): void {
border: "1px solid rgba(255,255,255,0.15)", borderRadius: "6px",
cursor: "pointer", backdropFilter: "blur(4px)",
});
btn.addEventListener("mouseenter", () => { btn.style.background = "rgba(255,255,255,0.15)"; });
btn.addEventListener("mouseleave", () => { btn.style.background = "rgba(255,255,255,0.08)"; });
btn.addEventListener("pointerenter", () => { btn.style.background = "rgba(255,255,255,0.15)"; });
btn.addEventListener("pointerleave", () => { btn.style.background = "rgba(255,255,255,0.08)"; });
btn.addEventListener("click", () => {
svg.transition().duration(500).call(zoom.transform, d3.zoomIdentity);
});
@ -293,9 +293,9 @@ export function renderTimeline(
contentGroup.append("path").attr("d", path.toString()).attr("fill", `url(#${id}-inflow)`)
.attr("opacity", 0.85).style("cursor", "pointer").style("transition", "opacity 0.2s, filter 0.2s")
.on("mouseover", (event: any) => showTxTooltip(event, tx, prevBalance))
.on("mousemove", (event: any) => moveTip(event))
.on("mouseout", () => { tooltip.style.display = "none"; });
.on("pointerenter", (event: any) => showTxTooltip(event, tx, prevBalance))
.on("pointermove", (event: any) => moveTip(event))
.on("pointerleave", () => { tooltip.style.display = "none"; });
} else {
const startY = riverBottomBefore;
const endY = startY + flowHeight;
@ -318,9 +318,9 @@ export function renderTimeline(
contentGroup.append("path").attr("d", path.toString()).attr("fill", `url(#${id}-outflow)`)
.attr("opacity", 0.85).style("cursor", "pointer").style("transition", "opacity 0.2s, filter 0.2s")
.on("mouseover", (event: any) => showTxTooltip(event, tx, prevBalance))
.on("mousemove", (event: any) => moveTip(event))
.on("mouseout", () => { tooltip.style.display = "none"; });
.on("pointerenter", (event: any) => showTxTooltip(event, tx, prevBalance))
.on("pointermove", (event: any) => moveTip(event))
.on("pointerleave", () => { tooltip.style.display = "none"; });
}
});
@ -330,7 +330,7 @@ export function renderTimeline(
.attr("x", scale.range()[0] - 50).attr("y", centerY - 100)
.attr("width", scale.range()[1] - scale.range()[0] + 100).attr("height", 200)
.attr("fill", "transparent").style("cursor", "crosshair")
.on("mousemove", function(event: any) {
.on("pointermove", function(event: any) {
const [mouseX] = d3.pointer(event);
const hoveredDate = scale.invert(mouseX);
let balAtPoint = 0;
@ -357,7 +357,7 @@ export function renderTimeline(
tooltip.style.display = "block";
moveTip(event);
})
.on("mouseout", function() {
.on("pointerleave", function() {
riverHoverGroup.selectAll(".bal-ind").remove();
tooltip.style.display = "none";
});
@ -627,8 +627,8 @@ export function renderSankey(
.attr("stroke-opacity", 0.4)
.attr("stroke-width", (d: any) => Math.max(1, d.width))
.style("transition", "stroke-opacity 0.2s")
.on("mouseover", function(this: any) { d3.select(this).attr("stroke-opacity", 0.7); })
.on("mouseout", function(this: any) { d3.select(this).attr("stroke-opacity", 0.4); })
.on("pointerenter", function(this: any) { d3.select(this).attr("stroke-opacity", 0.7); })
.on("pointerleave", function(this: any) { d3.select(this).attr("stroke-opacity", 0.4); })
.append("title")
.text((d: any) => `${d.source.name} -> ${d.target.name}\n${d.value.toLocaleString()} ${d.token}`);

View File

@ -392,14 +392,14 @@ export class RStackCollabOverlay extends HTMLElement {
});
}
// ── Mouse tracking (15Hz throttle) ──
// ── Pointer tracking (15Hz throttle) — covers mouse, touch, and pen ──
#mouseHandler = (e: MouseEvent) => {
#pointerHandler = (e: PointerEvent) => {
this.#lastCursor = { x: e.clientX, y: e.clientY };
};
#startMouseTracking() {
document.addEventListener('mousemove', this.#mouseHandler, { passive: true });
document.addEventListener('pointermove', this.#pointerHandler, { passive: true });
// Broadcast at 15Hz
this.#mouseMoveTimer = setInterval(() => {
this.#broadcastPresence(this.#lastCursor, undefined);
@ -407,7 +407,7 @@ export class RStackCollabOverlay extends HTMLElement {
}
#stopMouseTracking() {
document.removeEventListener('mousemove', this.#mouseHandler);
document.removeEventListener('pointermove', this.#pointerHandler);
if (this.#mouseMoveTimer) clearInterval(this.#mouseMoveTimer);
this.#mouseMoveTimer = null;
}
@ -429,18 +429,18 @@ export class RStackCollabOverlay extends HTMLElement {
#startFocusTracking() {
document.addEventListener('focusin', this.#focusHandler, { passive: true });
document.addEventListener('click', this.#clickHandler, { passive: true });
document.addEventListener('pointerdown', this.#clickHandler, { passive: true });
document.addEventListener('focusout', this.#blurHandler, { passive: true });
}
#stopFocusTracking() {
document.removeEventListener('focusin', this.#focusHandler);
document.removeEventListener('click', this.#clickHandler);
document.removeEventListener('pointerdown', this.#clickHandler);
document.removeEventListener('focusout', this.#blurHandler);
}
// Also track clicks on data-collab-id (many elements aren't focusable)
#clickHandler = (e: MouseEvent) => {
// Also track taps/clicks on data-collab-id (many elements aren't focusable)
#clickHandler = (e: PointerEvent) => {
const real = (e.composedPath()[0] as HTMLElement);
const target = real?.closest?.('[data-collab-id]');
if (target) {

View File

@ -83,7 +83,7 @@
justify-content: center;
}
.toolbar-group-toggle:hover {
.toolbar-group-toggle:hover, .toolbar-group-toggle:active {
background: var(--rs-toolbar-btn-hover);
}
@ -136,7 +136,7 @@
touch-action: manipulation;
}
.toolbar-dropdown button:hover {
.toolbar-dropdown button:hover, .toolbar-dropdown button:active {
background: var(--rs-toolbar-btn-bg);
}
@ -281,7 +281,7 @@
touch-action: manipulation;
}
#toolbar > button:hover {
#toolbar > button:hover, #toolbar > button:active {
background: var(--rs-toolbar-btn-hover);
}
@ -321,11 +321,15 @@
position: relative;
}
/* Show label outside toolbar on hover or when group is open */
.toolbar-group:hover > .toolbar-group-toggle .tg-label,
/* Show label outside toolbar on hover (only on hover-capable devices) or when group is open */
.toolbar-group.open > .toolbar-group-toggle .tg-label {
display: block;
}
@media (hover: hover) {
.toolbar-group:hover > .toolbar-group-toggle .tg-label {
display: block;
}
}
/* Collapse/expand toggle — at bottom of toolbar */
#toolbar-collapse {
@ -347,7 +351,7 @@
justify-content: center;
}
#toolbar-collapse:hover {
#toolbar-collapse:hover, #toolbar-collapse:active {
opacity: 1;
background: var(--rs-toolbar-btn-bg);
color: var(--rs-text-primary);
@ -401,7 +405,7 @@
position: relative;
}
#bottom-toolbar .tool-btn:hover {
#bottom-toolbar .tool-btn:hover, #bottom-toolbar .tool-btn:active {
background: var(--rs-toolbar-btn-hover);
}
@ -449,7 +453,7 @@
opacity: 0.7;
}
#recent-tools .recent-tool-btn:hover {
#recent-tools .recent-tool-btn:hover, #recent-tools .recent-tool-btn:active {
background: var(--rs-toolbar-btn-hover);
opacity: 1;
}
@ -506,7 +510,7 @@
transition: background 0.15s;
}
#tool-plus-menu button:hover {
#tool-plus-menu button:hover, #tool-plus-menu button:active {
background: var(--rs-toolbar-btn-hover);
}
@ -563,7 +567,7 @@
transition: background 0.15s;
}
#arrow-style-menu button:hover {
#arrow-style-menu button:hover, #arrow-style-menu button:active {
background: var(--rs-toolbar-btn-hover);
}
@ -642,7 +646,7 @@
touch-action: manipulation;
}
.corner-btn:hover {
.corner-btn:hover, .corner-btn:active {
background: var(--rs-toolbar-btn-hover);
}
@ -886,7 +890,7 @@
cursor: pointer;
}
#shape-context-menu button:hover {
#shape-context-menu button:hover, #shape-context-menu button:active {
background: var(--rs-bg-hover);
}
@ -894,7 +898,7 @@
color: var(--rs-error);
}
#shape-context-menu button.danger:hover {
#shape-context-menu button.danger:hover, #shape-context-menu button.danger:active {
background: rgba(239, 68, 68, 0.1);
}
@ -935,7 +939,7 @@
cursor: pointer;
}
.submenu button:hover {
.submenu button:hover, .submenu button:active {
background: var(--rs-bg-hover);
}
@ -5949,10 +5953,10 @@ Use real coordinates, YYYY-MM-DD dates, ISO currency codes. Ask clarifying quest
menu.innerHTML = items;
document.body.appendChild(menu);
// Hover effect
// Hover/press effect
menu.querySelectorAll("button").forEach(b => {
b.addEventListener("mouseenter", () => b.style.background = "#f1f5f9");
b.addEventListener("mouseleave", () => b.style.background = "none");
b.addEventListener("pointerenter", () => b.style.background = "#f1f5f9");
b.addEventListener("pointerleave", () => b.style.background = "none");
});
const closeMenu = (ev) => {