Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 32 additions & 12 deletions frontend/editor/src/proprietary/components/chat/ChatFAB.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export function ChatFAB() {
// Scope the panel's nested MantineProvider to this ref; unscoped it writes
// its color scheme onto <html> and overrides the whole app's theme.
const panelThemeRootRef = useRef<HTMLDivElement>(null);
const triggerButtonRef = useRef<HTMLButtonElement>(null);
const { colorScheme } = useMantineColorScheme();
const panelColorScheme = colorScheme === "dark" ? "dark" : "light";

Expand Down Expand Up @@ -94,6 +95,22 @@ export function ChatFAB() {
return defaultPanelPos(el.offsetWidth, el.offsetHeight);
};

const closePanel = () => {
const activeElement = document.activeElement;
if (
activeElement instanceof HTMLElement &&
activeElement !== document.body
) {
activeElement.blur();
}

setIsOpen(false);

requestAnimationFrame(() => {
triggerButtonRef.current?.focus();
});
};

useLayoutEffect(() => {
// The overlay only mounts once the AI engine is enabled (config can load
// after first render), so re-measure when that flips true rather than only
Expand Down Expand Up @@ -172,6 +189,7 @@ export function ChatFAB() {
>
{/* Trigger button — fades out while panel is open */}
<ChatFABButton
ref={triggerButtonRef}
className={`chat-fab-trigger${isOpen ? " chat-fab-trigger--hidden" : ""}`}
onClick={() => {
// Fallback: ensure a position exists before opening, in case the
Expand Down Expand Up @@ -232,18 +250,20 @@ export function ChatFAB() {
}}
>
<ChatFABWindow open={isOpen} onDoubleClick={handleHeaderDoubleClick}>
<MantineProvider
theme={FAB_PANEL_THEME}
getRootElement={() => panelThemeRootRef.current ?? undefined}
forceColorScheme={panelColorScheme}
>
<div ref={panelThemeRootRef} style={{ display: "contents" }}>
<ChatPanel
onBack={() => setIsOpen(false)}
backLabel={t("chat.fab.close", "Close chat")}
/>
</div>
</MantineProvider>
{isOpen && (
<MantineProvider
theme={FAB_PANEL_THEME}
getRootElement={() => panelThemeRootRef.current ?? undefined}
forceColorScheme={panelColorScheme}
>
<div ref={panelThemeRootRef} style={{ display: "contents" }}>
<ChatPanel
onBack={closePanel}
backLabel={t("chat.fab.close", "Close chat")}
/>
</div>
</MantineProvider>
)}
</ChatFABWindow>
</Rnd>
)}
Expand Down
112 changes: 56 additions & 56 deletions frontend/shared/components/ChatFABButton.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ButtonHTMLAttributes } from "react";
import { forwardRef, type ButtonHTMLAttributes } from "react";
import "@shared/components/ChatFABButton.css";

export interface ChatFABButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
Expand All @@ -8,59 +8,59 @@ export interface ChatFABButtonProps extends ButtonHTMLAttributes<HTMLButtonEleme
showTick?: boolean;
}

export function ChatFABButton({
isLoading = false,
showTick = false,
className,
...rest
}: ChatFABButtonProps) {
const classes = [
"chat-fab-btn",
isLoading ? "chat-fab-btn--loading" : "",
showTick ? "chat-fab-btn--tick" : "",
className ?? "",
]
.filter(Boolean)
.join(" ");
export const ChatFABButton = forwardRef<HTMLButtonElement, ChatFABButtonProps>(
function ChatFABButton(
{ isLoading = false, showTick = false, className, ...rest },
ref,
) {
const classes = [
"chat-fab-btn",
isLoading ? "chat-fab-btn--loading" : "",
showTick ? "chat-fab-btn--tick" : "",
className ?? "",
]
.filter(Boolean)
.join(" ");

return (
<button type="button" className={classes} {...rest}>
<svg
xmlns="http://www.w3.org/2000/svg"
width={28}
height={28}
viewBox="0 0 192 192"
fill="currentColor"
aria-hidden="true"
>
<path
d="M68.48 102.4 L184.73 6.45 L184.73 96.05 L68.48 192 Z"
opacity="0.7"
/>
<path d="M7.26 95.83 L123.37 0 L123.37 89.5 L7.26 185.33 Z" />
</svg>
{isLoading && !showTick && (
<span className="chat-fab-btn__pulse" aria-hidden="true" />
)}
{showTick && (
<span className="chat-fab-btn__tick" aria-hidden="true">
<svg
xmlns="http://www.w3.org/2000/svg"
width={10}
height={10}
viewBox="0 0 10 10"
fill="none"
>
<path
d="M2 5l2 2 4-4"
stroke="#fff"
strokeWidth={1.6}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</span>
)}
</button>
);
}
return (
<button ref={ref} type="button" className={classes} {...rest}>
<svg
xmlns="http://www.w3.org/2000/svg"
width={28}
height={28}
viewBox="0 0 192 192"
fill="currentColor"
aria-hidden="true"
>
<path
d="M68.48 102.4 L184.73 6.45 L184.73 96.05 L68.48 192 Z"
opacity="0.7"
/>
<path d="M7.26 95.83 L123.37 0 L123.37 89.5 L7.26 185.33 Z" />
</svg>
{isLoading && !showTick && (
<span className="chat-fab-btn__pulse" aria-hidden="true" />
)}
{showTick && (
<span className="chat-fab-btn__tick" aria-hidden="true">
<svg
xmlns="http://www.w3.org/2000/svg"
width={10}
height={10}
viewBox="0 0 10 10"
fill="none"
>
<path
d="M2 5l2 2 4-4"
stroke="#fff"
strokeWidth={1.6}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</span>
)}
</button>
);
},
);
Loading