feat(workspace): bullet action bubble only while the mouse moves
With a real pointer the bubble (zoom, extract, delete, to-do menu, type) shows while the mouse moves over the row and for a second after; it stays while the mouse rests on it, its to-do menu is open or a button in it has keyboard focus. A resting mouse, also while typing, no longer leaves it over the text. Touch devices keep showing it at the focused bullet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
426b548fc7
commit
2f872c0590
1 file changed
+34
-3
@@ -67,6 +67,9 @@ function slashRank(c: SlashCommand, q: string): number {
|
||||
return 4;
|
||||
}
|
||||
|
||||
// How long the action bubble stays after the last mouse move over a bullet
|
||||
const BUBBLE_IDLE_MS = 1000;
|
||||
|
||||
const SLASH_COMMANDS: SlashCommand[] = [
|
||||
{ type: 'todo', label: '/todo', desc: 'Aufgabe mit Checkbox', icon: <ListTodo className="w-3.5 h-3.5" /> },
|
||||
{ type: 'numbered', label: '/number', desc: 'Nummerierte Liste (1., a., i.)', icon: <ListOrdered className="w-3.5 h-3.5" />, aliases: ['nummer', 'liste', 'ol'] },
|
||||
@@ -209,6 +212,28 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
|
||||
const [showLinkModal, setShowLinkModal] = useState<boolean>(false);
|
||||
const [showTodoMenu, setShowTodoMenu] = useState<boolean>(false);
|
||||
|
||||
// Action bubble with a real pointer: shown only while the mouse moves over the row (until a moment after
|
||||
// the last move), while it rests on the bubble or a menu of it is open; a resting mouse must not leave it
|
||||
// over the text. Touch devices keep showing it at the focused bullet.
|
||||
const hasHover = useMemo(() => typeof window !== 'undefined' && !!window.matchMedia?.('(hover: hover)').matches, []);
|
||||
const [pointerMoving, setPointerMoving] = useState(false);
|
||||
const [pointerOnBubble, setPointerOnBubble] = useState(false);
|
||||
const pointerIdleTimerRef = useRef<number | null>(null);
|
||||
const handleRowMouseMove = () => {
|
||||
if (!hasHover) return;
|
||||
setPointerMoving(true);
|
||||
if (pointerIdleTimerRef.current !== null) window.clearTimeout(pointerIdleTimerRef.current);
|
||||
pointerIdleTimerRef.current = window.setTimeout(() => setPointerMoving(false), BUBBLE_IDLE_MS);
|
||||
};
|
||||
const handleRowMouseLeave = () => {
|
||||
if (pointerIdleTimerRef.current !== null) window.clearTimeout(pointerIdleTimerRef.current);
|
||||
pointerIdleTimerRef.current = null;
|
||||
setPointerMoving(false);
|
||||
};
|
||||
useEffect(() => () => {
|
||||
if (pointerIdleTimerRef.current !== null) window.clearTimeout(pointerIdleTimerRef.current);
|
||||
}, []);
|
||||
|
||||
const textareaRef = useRef<HTMLTextAreaElement | null>(null);
|
||||
const debounceTimerRef = useRef<number | null>(null);
|
||||
const latestBlockRef = useRef<DocumentBlock>(block);
|
||||
@@ -1087,9 +1112,13 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
|
||||
const isEmpty = (!localContent || localContent.trim() === '') && !hasChildren;
|
||||
|
||||
|
||||
const showBubble = hasHover ? pointerMoving || pointerOnBubble || showTodoMenu : activeFocus;
|
||||
|
||||
return (
|
||||
<div
|
||||
data-outline-block={block.id}
|
||||
onMouseMove={handleRowMouseMove}
|
||||
onMouseLeave={handleRowMouseLeave}
|
||||
className={`group relative flex items-start gap-1 py-1 px-0.5 sm:px-1.5 rounded-card transition-all duration-300 ${
|
||||
inRangeSelection ? 'bg-accent/15' : ''
|
||||
} ${
|
||||
@@ -1595,11 +1624,13 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
|
||||
|
||||
{/* Floating Actions Overlay Bubble (Zoom, Auslagern, Löschen, TODO-Menü, Text-Umwandlung) */}
|
||||
<div
|
||||
onMouseEnter={() => setPointerOnBubble(true)}
|
||||
onMouseLeave={() => setPointerOnBubble(false)}
|
||||
className={`absolute right-1 sm:right-2 top-0.5 sm:top-1 z-20 bg-panel/90 backdrop-blur-sm border border-border shadow-xs rounded-pill px-1 sm:px-1.5 py-0.5 flex items-center gap-0.5 sm:gap-1 transition-opacity ${
|
||||
activeFocus
|
||||
showBubble
|
||||
? 'opacity-100 pointer-events-auto'
|
||||
: // Hover only with a real pointer: a tap on a phone would make the invisible buttons clickable
|
||||
'opacity-0 [@media(hover:hover)]:group-hover:opacity-100 focus-within:opacity-100 pointer-events-none [@media(hover:hover)]:group-hover:pointer-events-auto focus-within:pointer-events-auto'
|
||||
: // Hidden buttons must not take clicks; keyboard focus inside still shows it
|
||||
'opacity-0 pointer-events-none focus-within:opacity-100 focus-within:pointer-events-auto'
|
||||
}`}
|
||||
>
|
||||
{/* Convert back to bullet for headings, callouts, dividers, code and numbered items */}
|
||||
|
||||
Reference in new issue
Block a user