The floating chat window covered page content; you could only collapse it manually. Now it auto-collapses to its header (visual collapse only — ChatThread stays mounted so an in-flight stream isn't interrupted) when you interact with the page, and expands again from the header. - document mousedown listener in the CAPTURE phase, armed only when windowOpen && !minimized; collapses on a pointer-down outside the window. Guards: ignore clicks inside the window and inside any Mantine [data-portal] (the chat-list kebab menu + delete-confirm modal render in portals). - Header click expands: startDrag distinguishes click vs drag by a 4px threshold (minimizedRef avoids a stale closure); an expand-click doesn't persist geometry. - Reset minimized=false when the window opens (no sticky collapsed state). - a11y: when minimized, the title is the keyboard expand affordance (role=button, tabIndex, aria-label Expand, Enter/Space) — kept off the dragBar container so no role=button wraps the Minimize/Close buttons. - Pure helpers shouldCollapseOnOutsidePointer + isHeaderClick with vitest tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
166 lines
4.3 KiB
CSS
166 lines
4.3 KiB
CSS
/* Floating AI chat window shell. Ported from the GitmostAgent.jsx design.
|
|
Dynamic values (left/top/width/height) stay inline on the element; only the
|
|
static chrome lives here. */
|
|
|
|
.window {
|
|
/* position: fixed + left/top/width/height are applied inline so the window
|
|
floats over the whole viewport. z-index sits above page content and the
|
|
app shell but BELOW Mantine overlays (modals=200, menus=300,
|
|
notifications=400) so the rename input, kebab menu and delete-confirm
|
|
modal still render above the window. */
|
|
position: fixed;
|
|
z-index: 105;
|
|
background: light-dark(#fff, var(--mantine-color-dark-7));
|
|
border: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
|
|
/* Match the rest of the UI: use the Mantine md radius token instead of an
|
|
oversized hard-coded value so the window corners blend with inner cards. */
|
|
border-radius: var(--mantine-radius-md);
|
|
box-shadow:
|
|
0 10px 24px rgba(0, 0, 0, 0.13),
|
|
0 30px 64px rgba(0, 0, 0, 0.17);
|
|
overflow: hidden;
|
|
resize: both;
|
|
display: flex;
|
|
flex-direction: column;
|
|
min-width: 300px;
|
|
min-height: 400px;
|
|
max-width: 900px;
|
|
max-height: 1100px;
|
|
font-size: 11px;
|
|
color: light-dark(var(--mantine-color-black), var(--mantine-color-dark-0));
|
|
}
|
|
|
|
/* Hide the native resizer; we draw our own affordance icon in the corner. */
|
|
.window::-webkit-resizer {
|
|
background: transparent;
|
|
}
|
|
|
|
/* When minimized the window collapses to the header only: auto height, no
|
|
resize. Width/height inline values are overridden. */
|
|
.minimized {
|
|
height: auto !important;
|
|
min-height: 0 !important;
|
|
resize: none;
|
|
}
|
|
|
|
/* Body wrapper (history + chat thread). Always rendered so ChatThread stays
|
|
mounted; hidden (not unmounted) when minimized so an in-flight stream keeps
|
|
running and the window collapses to just the header. */
|
|
.content {
|
|
flex: 1;
|
|
min-height: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.minimized .content {
|
|
display: none;
|
|
}
|
|
|
|
/* In the collapsed state the header expands the window on click, so hint that
|
|
it is clickable (override the drag `grab` cursor). */
|
|
.minimized .dragBar {
|
|
cursor: pointer;
|
|
}
|
|
|
|
.dragBar {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 9px 11px;
|
|
border-bottom: 1px solid light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-5));
|
|
background: light-dark(#fcfcfd, var(--mantine-color-dark-6));
|
|
flex: none;
|
|
cursor: grab;
|
|
user-select: none;
|
|
}
|
|
|
|
.title {
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
letter-spacing: -0.01em;
|
|
}
|
|
|
|
.headerBtn {
|
|
width: 24px;
|
|
height: 24px;
|
|
border: none;
|
|
background: transparent;
|
|
border-radius: 6px;
|
|
color: var(--mantine-color-dimmed);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
cursor: pointer;
|
|
padding: 0;
|
|
}
|
|
|
|
.headerBtn:hover {
|
|
background: light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-5));
|
|
}
|
|
|
|
.badge {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
font-size: 11px;
|
|
color: var(--mantine-color-dimmed);
|
|
background: light-dark(#f6f7f8, var(--mantine-color-dark-6));
|
|
border: 1px solid light-dark(#eceef0, var(--mantine-color-dark-4));
|
|
border-radius: 6px;
|
|
padding: 3px 9px;
|
|
}
|
|
|
|
.historySection {
|
|
padding: 6px 8px;
|
|
border-bottom: 1px solid light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-5));
|
|
flex: none;
|
|
}
|
|
|
|
.historyHeader {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 5px;
|
|
padding: 4px 6px;
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
color: var(--mantine-color-dimmed);
|
|
font-weight: 500;
|
|
}
|
|
|
|
.newChatBtn {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
padding: 4px 7px;
|
|
border-radius: 6px;
|
|
border: none;
|
|
background: transparent;
|
|
color: var(--mantine-color-dimmed);
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.newChatBtn:hover {
|
|
background: light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-5));
|
|
}
|
|
|
|
.body {
|
|
flex: 1;
|
|
min-height: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
padding: 13px 14px 12px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.resizeHandle {
|
|
position: absolute;
|
|
right: 3px;
|
|
bottom: 3px;
|
|
color: light-dark(#ced4da, var(--mantine-color-dark-3));
|
|
pointer-events: none;
|
|
display: flex;
|
|
}
|