UI - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: перенос Voice Tasker в нижний dock

This commit is contained in:
DCCONSTRUCTIONS 2026-04-27 21:19:36 +03:00
parent 6da9818826
commit efa357c260
3 changed files with 62 additions and 17 deletions

View File

@ -69,6 +69,7 @@ export const AppHeader = observer(function AppHeader(props: AppHeaderProps) {
)}
>
<ExtendedAppHeader header={header} />
<div className="nodedc-bottom-dock-voice-slot" data-nodedc-voice-task-dock-slot />
</Row>
{mobileHeader && mobileHeader}
</div>

View File

@ -6,6 +6,7 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import type { ElementType, MouseEvent, ReactNode } from "react";
import { createPortal } from "react-dom";
import { useParams } from "next/navigation";
import useSWR from "swr";
import {
@ -760,6 +761,7 @@ export function VoiceTaskerGlobalControl({ workspaceSlug }: Props) {
const [commitResult, setCommitResult] = useState<TVoiceTaskCommitResult | null>(null);
const [hasDraftChanges, setHasDraftChanges] = useState(false);
const [selectedTargetIssue, setSelectedTargetIssue] = useState<TVoiceTaskTargetOption | null>(null);
const [dockSlot, setDockSlot] = useState<Element | null>(null);
const mediaRecorderRef = useRef<MediaRecorder | null>(null);
const discardedRecorderRef = useRef<MediaRecorder | null>(null);
@ -787,6 +789,22 @@ export function VoiceTaskerGlobalControl({ workspaceSlug }: Props) {
return UNAVAILABLE_LABELS[preflight.reason ?? "not_configured"];
}, [preflight]);
useEffect(() => {
if (typeof document === "undefined") return;
const updateDockSlot = () => {
setDockSlot(document.querySelector("[data-nodedc-voice-task-dock-slot]"));
};
updateDockSlot();
const observer = new MutationObserver(updateDockSlot);
observer.observe(document.body, { childList: true, subtree: true });
return () => {
observer.disconnect();
};
}, []);
const clearTimer = useCallback(() => {
if (timerRef.current) {
window.clearInterval(timerRef.current);
@ -1171,23 +1189,21 @@ export function VoiceTaskerGlobalControl({ workspaceSlug }: Props) {
return (
<>
<div className="pointer-events-none fixed right-4 bottom-[calc(var(--nodedc-bottom-dock-offset,0px)+1rem)] z-[29]">
<Tooltip tooltipContent={tooltipContent} position="left">
<button
type="button"
className={cn(
"pointer-events-auto flex size-11 items-center justify-center border-0 bg-transparent p-0 shadow-none transition outline-none",
isAvailable
? "text-[rgb(var(--nodedc-accent-rgb))] hover:text-[rgb(var(--nodedc-card-active-rgb))]"
: "cursor-not-allowed text-tertiary"
)}
disabled={!isAvailable}
onClick={openVoiceTasker}
>
<Mic className="size-7" />
</button>
</Tooltip>
</div>
{isAvailable && dockSlot
? createPortal(
<Tooltip tooltipContent={tooltipContent} position="top">
<button
type="button"
className="nodedc-bottom-dock-voice-button"
onClick={openVoiceTasker}
aria-label="Voice Tasker"
>
<Mic className="size-4" />
</button>
</Tooltip>,
dockSlot
)
: null}
<ModalCore
isOpen={isOpen}

View File

@ -359,6 +359,34 @@
backdrop-filter: blur(34px);
}
.nodedc-bottom-dock-voice-slot {
display: flex;
flex: 0 0 auto;
align-items: center;
justify-content: center;
min-width: 0;
}
.nodedc-bottom-dock-voice-button {
display: grid;
height: 2rem;
width: 2rem;
place-items: center;
border: 0 !important;
border-radius: 999px;
background: transparent;
color: rgb(var(--nodedc-accent-rgb));
outline: none !important;
box-shadow: none !important;
transition:
color 160ms ease,
transform 160ms ease;
}
.nodedc-bottom-dock-voice-button:hover {
color: rgb(var(--nodedc-card-active-rgb));
}
.nodedc-bottom-dock-aware-padding {
padding-bottom: calc(var(--nodedc-quick-add-reserve, 2.5rem) + 0.5rem);
}