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;
|
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[] = [
|
const SLASH_COMMANDS: SlashCommand[] = [
|
||||||
{ type: 'todo', label: '/todo', desc: 'Aufgabe mit Checkbox', icon: <ListTodo className="w-3.5 h-3.5" /> },
|
{ 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'] },
|
{ 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 [showLinkModal, setShowLinkModal] = useState<boolean>(false);
|
||||||
const [showTodoMenu, setShowTodoMenu] = 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 textareaRef = useRef<HTMLTextAreaElement | null>(null);
|
||||||
const debounceTimerRef = useRef<number | null>(null);
|
const debounceTimerRef = useRef<number | null>(null);
|
||||||
const latestBlockRef = useRef<DocumentBlock>(block);
|
const latestBlockRef = useRef<DocumentBlock>(block);
|
||||||
@@ -1087,9 +1112,13 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
|
|||||||
const isEmpty = (!localContent || localContent.trim() === '') && !hasChildren;
|
const isEmpty = (!localContent || localContent.trim() === '') && !hasChildren;
|
||||||
|
|
||||||
|
|
||||||
|
const showBubble = hasHover ? pointerMoving || pointerOnBubble || showTodoMenu : activeFocus;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-outline-block={block.id}
|
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 ${
|
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' : ''
|
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) */}
|
{/* Floating Actions Overlay Bubble (Zoom, Auslagern, Löschen, TODO-Menü, Text-Umwandlung) */}
|
||||||
<div
|
<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 ${
|
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'
|
? 'opacity-100 pointer-events-auto'
|
||||||
: // Hover only with a real pointer: a tap on a phone would make the invisible buttons clickable
|
: // Hidden buttons must not take clicks; keyboard focus inside still shows it
|
||||||
'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'
|
'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 */}
|
{/* Convert back to bullet for headings, callouts, dividers, code and numbered items */}
|
||||||
|
|||||||
Reference in new issue
Block a user