Files
gitmost/apps/client/src/features/ai-chat/components/ai-chat-window.module.css
T
agent_coder 85b303e387 feat(ai-chat): dock the floating chat window into the sidebar (closes #276)
Drag the floating AI-chat window onto the sidebar and release over it to DOCK it
— the window pins to the live navbar rect, overlaying the page tree; a drop-zone
highlight shows while dragging over it. Closing the chat re-shows the tree.
Undock via a header button or by dragging the docked window back onto content
(pops out floating at the drop point). The docked/floating mode persists in
localStorage and the docked window follows the navbar width (manual resize,
space<->shared route change) via a ResizeObserver + sidebar-toggle/transitionend
re-sync; when the navbar is collapsed/absent the window falls back to floating
instead of vanishing. Dock/undock only flips a mode atom + geometry — ChatThread
is never remounted, so an in-flight response stream is not interrupted.
Frontend only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 01:57:00 +03:00

195 lines
5.5 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;
}
/* Docked into the sidebar: the window pins itself to the live navbar rect
(position/size supplied inline). It sits flush inside the navbar area, so we
drop the floating chrome — no border-radius, drop shadow or user resize — and
remove the floating min/max clamps so the size is driven ENTIRELY by the
inline navbar rect (which may be narrower than the floating min-width of
300px, e.g. the 220px navbar minimum). z-index 105 keeps it above the page
tree (navbar 101) but below the header and Mantine overlays. */
.docked {
border-radius: 0;
box-shadow: none;
resize: none;
min-width: 0;
min-height: 0;
max-width: none;
max-height: none;
}
/* Drop-zone highlight shown over the navbar bounds while a floating window is
dragged onto the sidebar. Sits just above the docked window (106) so the cue
is visible; purely decorative, so it never intercepts pointer events. */
.dockHighlight {
position: fixed;
z-index: 106;
border: 2px dashed light-dark(var(--mantine-color-blue-5), var(--mantine-color-blue-4));
background: light-dark(rgba(34, 139, 230, 0.08), rgba(34, 139, 230, 0.14));
border-radius: var(--mantine-radius-sm);
pointer-events: none;
}
/* 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;
}