1238 lines
38 KiB
CSS
1238 lines
38 KiB
CSS
/* ==========================================================================
|
|
Doormile AI — Operations Copilot
|
|
--------------------------------------------------------------------------
|
|
Styling for the right-side slide-over assistant. Follows the same
|
|
convention as OrdersRedesign.css / Dispatch.css (a page-scoped stylesheet
|
|
imported by its own component) rather than inline styles, so the panel's
|
|
hover / focus / media-query / reduced-motion states are all expressible.
|
|
|
|
Every value below resolves to a design token (`--color-*`, `--spacing-*`,
|
|
`--radius-*`) or to a local `--dai-*` token defined once here. The local
|
|
tokens exist because the AI surface needs a few values the shared DT set
|
|
doesn't carry (the AI accent, the scrim, the panel elevation).
|
|
========================================================================== */
|
|
|
|
/* ---------------------------------------------------------------------------
|
|
Dock geometry.
|
|
|
|
These three live on :root, not on .dai-root, because the LAYOUT has to read
|
|
them: the assistant is docked into the page rather than floated over it, so
|
|
the app's content container shifts by exactly the panel's width and animates
|
|
on the same curve. A token only .dai-root can see cannot do that.
|
|
|
|
All three are --dai- prefixed. :root is shared with Dispatch.css and
|
|
globalPolish.css, and an unprefixed name there is decided by bundle order.
|
|
--------------------------------------------------------------------------- */
|
|
:root {
|
|
/* 20vw, NOT 20%.
|
|
|
|
The two consumers resolve a percentage against different boxes: the panel
|
|
is position:fixed, so `width: 20%` is 20% of the VIEWPORT, while
|
|
`padding-right: 20%` on the content container is 20% of ITS containing
|
|
block — which is narrower by the side nav. The page would slide underneath
|
|
the panel by the difference. A viewport unit resolves the same for both.
|
|
|
|
max() rather than a separate min-width for the same reason: a min-width
|
|
that only the panel knows about would reintroduce the mismatch on narrow
|
|
screens.
|
|
|
|
The floor matters more at 20vw than it did at 30vw: 20vw is 256px on a
|
|
1280px laptop, which is too narrow for the composer and a message thread.
|
|
320px is the floor, so below 1600px the panel stops shrinking and holds —
|
|
and the page keeps the rest. On a 1920px monitor 20vw is 384px.
|
|
|
|
Narrowed from 30vw on request. It is the reason the Deliveries tab strip
|
|
started overflowing, so the extra 10vw goes back to the page. */
|
|
/* Default = the NORMAL step. AIPanel overwrites this property on the
|
|
document element when the operator toggles the width, so the panel and
|
|
the page's reserved strip always read the same value. */
|
|
/* clamp, not max: the cap is what makes this work on a large monitor.
|
|
|
|
With `max(320px, 20vw)` the panel just kept growing — 20vw is 512px at
|
|
2560px and 768px at 4K, which is a chat column wider than most documents,
|
|
while the board beside it gets squeezed for no benefit. A conversation
|
|
does not read better past ~460px.
|
|
|
|
So three regimes, one expression:
|
|
< 1600px floor at 320px (20vw would be 256px on a 1280 laptop —
|
|
too narrow for the composer and the thread)
|
|
1600-2300 20vw (scales with the screen, as asked)
|
|
> 2300px cap at 460px (the page keeps everything above this) */
|
|
--dai-dock-width: clamp(320px, 20vw, 460px);
|
|
--dai-duration: 240ms;
|
|
--dai-ease: cubic-bezier(0.16, 1, 0.3, 1);
|
|
}
|
|
|
|
.dai-root {
|
|
/* Brand accent — resolves to the app's accent token, which is black
|
|
(root CLAUDE.md §6.2: "the brand is black", one accent only). Send
|
|
button, active states and the trigger all read from this single
|
|
variable, so switching the assistant to Doormile red is a one-line
|
|
change here rather than a hunt through the components. */
|
|
--dai-accent: var(--color-accent, #0f172a);
|
|
--dai-accent-contrast: #ffffff;
|
|
/* Focus ring, tinted with the accent rather than a flat grey wash. */
|
|
--dai-accent-ring: color-mix(in srgb, var(--dai-accent) 14%, transparent);
|
|
|
|
/* AI identity accent — used ONLY on the spark/orb marks so the assistant
|
|
reads as an AI surface without turning the panel into a purple product. */
|
|
--dai-ai-from: #6366f1;
|
|
--dai-ai-to: #8b5cf6;
|
|
--dai-ai-glow: rgba(99, 102, 241, 0.18);
|
|
|
|
--dai-text: #0f172a;
|
|
--dai-text-secondary: #64748b;
|
|
--dai-text-muted: #94a3b8;
|
|
--dai-surface: #ffffff;
|
|
--dai-surface-alt: #f8fafc;
|
|
--dai-surface-hover: #f1f5f9;
|
|
--dai-border: rgba(15, 23, 42, 0.08);
|
|
--dai-border-strong: rgba(15, 23, 42, 0.14);
|
|
--dai-scrim: rgba(15, 23, 42, 0.08);
|
|
--dai-shadow: 0 8px 30px rgba(15, 23, 42, 0.1);
|
|
--dai-live: #10b981;
|
|
|
|
/* --dai-panel-width was declared here and at the 1279px breakpoint, and read
|
|
nowhere: the panel and the page's reserved strip both size from
|
|
--dai-dock-width. Removed so nobody tunes it expecting the panel to
|
|
change. */
|
|
--dai-inset: 12px;
|
|
--dai-duration: 240ms;
|
|
--dai-ease: cubic-bezier(0.16, 1, 0.3, 1);
|
|
}
|
|
/* --------------------------------------------------------------------------
|
|
Scrim — deliberately light. The dashboard underneath must stay readable;
|
|
this is a layering cue, not a modal blackout.
|
|
-------------------------------------------------------------------------- */
|
|
/* The scrim exists for the MOBILE presentation only (see the media query at
|
|
the end of this block). On desktop the panel is docked into the layout and
|
|
dimming the page behind it would be a lie — the page is still live, still
|
|
scrollable, and still the thing the operator is working on. Dimming is also
|
|
the single strongest cue that something is modal, which is exactly the
|
|
impression this panel should not give. */
|
|
.dai-scrim {
|
|
display: none;
|
|
position: fixed;
|
|
inset: 0;
|
|
z-index: 1300;
|
|
background: var(--dai-scrim);
|
|
opacity: 0;
|
|
transition: opacity var(--dai-duration) var(--dai-ease);
|
|
}
|
|
.dai-scrim[data-open='true'] {
|
|
opacity: 1;
|
|
}
|
|
/* --------------------------------------------------------------------------
|
|
Panel
|
|
-------------------------------------------------------------------------- */
|
|
/* DOCKED, not floating.
|
|
|
|
It used to be an inset card: 12px off every edge, fully rounded, drop
|
|
shadowed, over a scrim — every cue of a popup. It now sits flush in the
|
|
right 30% of the workspace, starting under the app header and running to the
|
|
bottom of the window, with a single hairline on its left edge. The page does
|
|
not move underneath it; it makes room for it.
|
|
|
|
`top` reads Astryx's own measured header height so the dock lines up with
|
|
the bottom of the top nav rather than guessing a number.
|
|
|
|
No radius and no drop shadow: both are what make a surface read as floating
|
|
ABOVE the page. A soft shadow is kept only as a left-edge falloff so the
|
|
seam has depth without the panel detaching. */
|
|
.dai-panel {
|
|
position: fixed;
|
|
/* Inset card geometry matching the reference design:
|
|
Floating cleanly off the top nav, bottom edge, and right edge with rounded corners. */
|
|
top: calc(var(--dai-dock-top, 57px) + 14px);
|
|
right: 16px;
|
|
bottom: 14px;
|
|
z-index: 1200;
|
|
width: calc(var(--dai-dock-width) - 24px);
|
|
display: flex;
|
|
flex-direction: column;
|
|
min-height: 0;
|
|
overflow: hidden;
|
|
background: #ffffff;
|
|
border: 1px solid #e2e8f0;
|
|
border-radius: 20px;
|
|
box-shadow: 0 4px 24px -2px rgba(15, 23, 42, 0.08), 0 1px 3px rgba(15, 23, 42, 0.04);
|
|
transform: translateX(calc(100% + 32px));
|
|
visibility: hidden;
|
|
transition:
|
|
transform var(--dai-duration) var(--dai-ease),
|
|
visibility 0s linear var(--dai-duration);
|
|
}
|
|
.dai-panel[data-open='true'] {
|
|
transform: translateX(0);
|
|
visibility: visible;
|
|
transition:
|
|
transform var(--dai-duration) var(--dai-ease),
|
|
visibility 0s;
|
|
}
|
|
|
|
/* ---- The page makes room -------------------------------------------------
|
|
`.dai-docked` is set on <body> while the panel is open. Padding rather than
|
|
width/margin: the content container is a flex child inside AppShell's own
|
|
height:fill chain, and changing its width there fights that chain, whereas
|
|
padding leaves the box model alone and simply reserves the strip.
|
|
|
|
The transition sits on the container unconditionally so the page slides back
|
|
when the panel closes too — a rule that only exists while `.dai-docked` is
|
|
applied cannot animate its own removal. */
|
|
.dai-page,
|
|
.astryx-layout-content {
|
|
transition: padding-right var(--dai-duration) var(--dai-ease);
|
|
}
|
|
body.dai-docked .dai-page,
|
|
body.dai-docked .astryx-layout-content {
|
|
/* Reserves the dock width plus a generous 20px gutter so the page content
|
|
(tables, cards) never collides with the sidebar and stays distinct. */
|
|
padding-right: calc(var(--dai-dock-width) + 16px);
|
|
}
|
|
|
|
/* ---- Mobile: there is no 30% worth having -------------------------------
|
|
30% of a phone is ~120px. Below the breakpoint the panel becomes what it
|
|
used to be everywhere — a full-width overlay with a scrim — and the page
|
|
stops reserving a strip it cannot afford. */
|
|
@media (max-width: 900px) {
|
|
.dai-panel {
|
|
top: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
width: 100%;
|
|
z-index: 1301;
|
|
border: none;
|
|
border-radius: 0;
|
|
box-shadow: var(--dai-shadow);
|
|
transform: translateX(100%);
|
|
}
|
|
.dai-scrim {
|
|
display: block;
|
|
}
|
|
.dai-scrim[data-open='true'] {
|
|
opacity: 1;
|
|
}
|
|
body.dai-docked .dai-page,
|
|
body.dai-docked .astryx-layout-content {
|
|
padding-right: 0;
|
|
}
|
|
}
|
|
.dai-panel:focus {
|
|
outline: none;
|
|
}
|
|
/* --------------------------------------------------------------------------
|
|
Header
|
|
-------------------------------------------------------------------------- */
|
|
.dai-root .dai-header {
|
|
flex: 0 0 auto;
|
|
padding: 14px 16px;
|
|
border-bottom: 1px solid #f1f5f9;
|
|
background: #ffffff;
|
|
}
|
|
.dai-root .dai-title {
|
|
font-size: 16px;
|
|
font-weight: 700;
|
|
line-height: 1.2;
|
|
letter-spacing: -0.01em;
|
|
color: #0f172a;
|
|
}
|
|
.dai-root .dai-subtitle {
|
|
font-size: 12px;
|
|
line-height: 1.3;
|
|
color: #64748b;
|
|
font-weight: 500;
|
|
}
|
|
/* The AI mark. A soft gradient orb — not a robot face. */
|
|
.dai-root .dai-spark {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
flex: 0 0 auto;
|
|
border-radius: 10px;
|
|
background: #fff1f2;
|
|
border: 1px solid #ffe4e6;
|
|
overflow: hidden;
|
|
}
|
|
/* The mark's own canvas is only 66.8% content — a third of every edge is
|
|
transparent padding, measured off the PNG's alpha bounding box (x and y both
|
|
170..853 of 1024, i.e. perfectly centred). Drawing it at 100% therefore
|
|
rendered a D two-thirds the size the box implied, which is exactly why it
|
|
read as too small. 150% cancels that padding so the D fills its box edge to
|
|
edge; the overflow:hidden above clips only transparent pixels. */
|
|
.dai-root .dai-spark img {
|
|
width: 150%;
|
|
height: 150%;
|
|
max-width: none;
|
|
object-fit: contain;
|
|
display: block;
|
|
}
|
|
.dai-root .dai-spark[data-size='sm'] {
|
|
width: 30px;
|
|
height: 30px;
|
|
}
|
|
.dai-root .dai-spark[data-size='md'] {
|
|
width: 40px;
|
|
height: 40px;
|
|
}
|
|
.dai-root .dai-spark[data-size='lg'] {
|
|
width: 56px;
|
|
height: 56px;
|
|
}
|
|
/* Live indicator — subtle, not a large pill. */
|
|
.dai-root .dai-live {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 5px;
|
|
padding: 3px 8px;
|
|
border-radius: 999px;
|
|
font-size: 11px;
|
|
font-weight: 550;
|
|
color: #047857;
|
|
background: rgba(16, 185, 129, 0.08);
|
|
white-space: nowrap;
|
|
}
|
|
.dai-root .dai-live-dot {
|
|
width: 6px;
|
|
height: 6px;
|
|
border-radius: 999px;
|
|
background: var(--dai-live);
|
|
animation: dai-pulse 2.4s ease-in-out infinite;
|
|
}
|
|
@keyframes dai-pulse {
|
|
0%,
|
|
100% {
|
|
opacity: 1;
|
|
transform: scale(1);
|
|
}
|
|
50% {
|
|
opacity: 0.55;
|
|
transform: scale(0.85);
|
|
}
|
|
}
|
|
/* Page-context strip — "Orders · Today · All locations" */
|
|
.dai-root .dai-context {
|
|
display: none;
|
|
}
|
|
/* --------------------------------------------------------------------------
|
|
Welcome state
|
|
-------------------------------------------------------------------------- */
|
|
.dai-root .dai-welcome {
|
|
padding: 22px 14px 8px;
|
|
}
|
|
.dai-root .dai-welcome-greeting {
|
|
font-size: 17px;
|
|
font-weight: 650;
|
|
letter-spacing: -0.01em;
|
|
color: var(--dai-text);
|
|
}
|
|
.dai-root .dai-welcome-lead {
|
|
font-size: 13.5px;
|
|
line-height: 1.5;
|
|
color: var(--dai-text-secondary);
|
|
}
|
|
.dai-root .dai-welcome-note {
|
|
font-size: 12px;
|
|
line-height: 1.5;
|
|
color: var(--dai-text-muted);
|
|
}
|
|
.dai-root .dai-section-label {
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
letter-spacing: 0.04em;
|
|
text-transform: uppercase;
|
|
color: var(--dai-text-muted);
|
|
}
|
|
/* --------------------------------------------------------------------------
|
|
Suggestion cards
|
|
-------------------------------------------------------------------------- */
|
|
/* The suggestions strip sits between the conversation and the composer, so it
|
|
needs the composer's horizontal rhythm and a hairline to separate it from
|
|
the thread scrolling above it. */
|
|
.dai-root .dai-suggestions-bar {
|
|
/* Shrinkable, with a hard cap.
|
|
|
|
At the normal 25% width the full twenty chips still stack to several rows.
|
|
As `flex: 0 0 auto` that block refuses to shrink, so on a short laptop it
|
|
pushes the composer off the bottom of the panel — the input disappears and
|
|
the assistant cannot be used at all. The cap is a safety valve, not a
|
|
layout choice: it only engages when the strip would otherwise cost more
|
|
than a third of the panel, and it never applies once a thread exists,
|
|
because the strip is four chips by then. */
|
|
flex: 0 1 auto;
|
|
min-height: 0;
|
|
max-height: 34vh;
|
|
overflow-y: auto;
|
|
padding: 10px 14px 8px;
|
|
border-top: 1px solid var(--dai-border);
|
|
background: var(--dai-surface);
|
|
}
|
|
|
|
/* Follow-up mode: a handful of chips, so the strip is FIXED — it never shrinks
|
|
and never scrolls.
|
|
|
|
The cap and `flex-shrink: 1` above exist for the cold panel's twenty chips.
|
|
Once a conversation starts the strip is three or four, and leaving it
|
|
shrinkable meant a long thread squeezed it: the flex row gave the space to
|
|
the conversation, `overflow-y: auto` quietly hid the remainder, and the
|
|
follow-ups the operator was meant to act on disappeared below a fold nobody
|
|
could see. Few chips must always be fully visible. */
|
|
.dai-root .dai-suggestions-bar[data-compact='true'] {
|
|
flex: 0 0 auto;
|
|
max-height: none;
|
|
overflow: visible;
|
|
}
|
|
|
|
/* ---- Suggested questions: chips ------------------------------------------
|
|
They wrap, they never scroll. A horizontal scroller can always leave a chip
|
|
half-visible at the edge, and a half-visible button is a bug no amount of
|
|
fade masking fixes. */
|
|
.dai-root .dai-suggestions-bar .dai-section-label {
|
|
display: none;
|
|
}
|
|
.dai-root .dai-suggestions {
|
|
/* Row + wrap, which is what the comment above always claimed and what the
|
|
component asks for — SuggestionChips renders an HStack with wrap="wrap".
|
|
This rule said `column`, overriding it, so four chips became four full
|
|
rows and the strip ate a third of the panel before a single question had
|
|
been asked. Two short chips now share a row and only long ones take one to
|
|
themselves. */
|
|
width: 100%;
|
|
display: flex;
|
|
flex-direction: row;
|
|
flex-wrap: wrap;
|
|
gap: 8px;
|
|
align-items: flex-start;
|
|
align-content: flex-start;
|
|
}
|
|
|
|
.dai-suggestion {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
max-width: 100%;
|
|
/* Trimmed from 8px 18px. At the 320px floor the old horizontal padding put
|
|
36px of empty pill around every label, which is what stopped two chips
|
|
ever fitting side by side. */
|
|
padding: 7px 13px;
|
|
text-align: left;
|
|
/* A chip is one line. Without this the flex row can hand a chip a width
|
|
narrower than its label and the text wraps inside the pill, which is what
|
|
made the strip look ragged. The ellipsis itself is on
|
|
.dai-suggestion-text — see there. */
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
font: inherit;
|
|
font-size: 13px;
|
|
line-height: 1.4;
|
|
font-weight: 500;
|
|
color: #334155;
|
|
background: #ffffff;
|
|
border: 1px solid #e2e8f0;
|
|
border-radius: 9999px;
|
|
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.03);
|
|
cursor: pointer;
|
|
transition: all 140ms ease;
|
|
}
|
|
.dai-suggestion:hover {
|
|
background: #f8fafc;
|
|
border-color: #cbd5e1;
|
|
color: #0f172a;
|
|
transform: translateY(-1px);
|
|
box-shadow: 0 2px 5px rgba(15, 23, 42, 0.06);
|
|
}
|
|
.dai-suggestion:active {
|
|
transform: translateY(0);
|
|
}
|
|
.dai-suggestion:focus-visible {
|
|
outline: 2px solid #0f172a;
|
|
outline-offset: 2px;
|
|
}
|
|
.dai-root .dai-suggestion-icon {
|
|
flex: 0 0 auto;
|
|
color: #64748b;
|
|
transition: color 140ms ease;
|
|
}
|
|
.dai-suggestion:hover .dai-suggestion-icon {
|
|
color: #0f172a;
|
|
}
|
|
.dai-root .dai-suggestion-text {
|
|
/* The ellipsis lives here, not on the button: text-overflow needs the
|
|
element that actually holds the text, and the button is a flex container
|
|
whose children are the icon and this span. min-width:0 is what lets the
|
|
span shrink below its content width so the ellipsis can engage at all —
|
|
a flex item defaults to min-width:auto and would push the pill wider
|
|
instead. The button already carries title={item.text}, so the full
|
|
question is still available when a label is cut. */
|
|
min-width: 0;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
/* Plain text link-button ("View more", "Sources") */
|
|
.dai-link {
|
|
font: inherit;
|
|
font-size: 12px;
|
|
color: var(--dai-text-secondary);
|
|
background: none;
|
|
border: none;
|
|
padding: 2px 0;
|
|
cursor: pointer;
|
|
align-self: flex-start;
|
|
}
|
|
.dai-link:hover {
|
|
color: var(--dai-text);
|
|
text-decoration: underline;
|
|
}
|
|
.dai-link:focus-visible {
|
|
outline: 2px solid var(--dai-accent);
|
|
outline-offset: 2px;
|
|
border-radius: 5px;
|
|
}
|
|
/* --------------------------------------------------------------------------
|
|
Conversation
|
|
-------------------------------------------------------------------------- */
|
|
/* --------------------------------------------------------------------------
|
|
Scroll region
|
|
-------------------------------------------------------------------------- */
|
|
.dai-scroll {
|
|
flex: 1 1 auto;
|
|
min-height: 0;
|
|
overflow-y: auto;
|
|
/* Never scroll sideways. A single wide child (the create form's name row)
|
|
used to push the whole message area horizontally. */
|
|
overflow-x: hidden;
|
|
overscroll-behavior: contain;
|
|
scrollbar-width: thin;
|
|
scrollbar-color: var(--dai-border-strong) transparent;
|
|
}
|
|
|
|
.dai-scroll::-webkit-scrollbar {
|
|
width: 8px;
|
|
}
|
|
|
|
.dai-scroll::-webkit-scrollbar-thumb {
|
|
background: var(--dai-border-strong);
|
|
border-radius: 999px;
|
|
border: 2px solid transparent;
|
|
background-clip: content-box;
|
|
}
|
|
|
|
.dai-scroll::-webkit-scrollbar-track {
|
|
background: transparent;
|
|
}
|
|
|
|
.dai-scroll-wrap {
|
|
position: relative;
|
|
flex: 1 1 auto;
|
|
min-height: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.dai-root .dai-thread {
|
|
padding: 16px 14px 8px;
|
|
}
|
|
|
|
/* Only the newest turn animates in. Applying this to every .dai-msg meant
|
|
opening a panel with a restored thread started 50 simultaneous opacity +
|
|
transform animations, which is a visible hitch before the first scroll. The
|
|
base state IS the animation's end state, so a turn that stops being last
|
|
simply keeps its finished appearance. */
|
|
.dai-root .dai-thread > .dai-msg:last-child {
|
|
animation: dai-enter 220ms var(--dai-ease) both;
|
|
}
|
|
|
|
@keyframes dai-enter {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(4px);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: none;
|
|
}
|
|
}
|
|
|
|
.dai-root .dai-msg-user {
|
|
max-width: 82%;
|
|
margin-left: auto;
|
|
padding: 8px 12px;
|
|
border-radius: 5px;
|
|
border-bottom-right-radius: 5px;
|
|
font-size: 13.5px;
|
|
line-height: 1.45;
|
|
color: var(--dai-accent-contrast);
|
|
background: var(--dai-accent);
|
|
white-space: pre-wrap;
|
|
overflow-wrap: anywhere;
|
|
}
|
|
/* Assistant — no bubble. Text sits on the panel surface. */
|
|
.dai-root .dai-msg-ai {
|
|
font-size: 13.5px;
|
|
line-height: 1.5;
|
|
color: var(--dai-text);
|
|
overflow-wrap: anywhere;
|
|
}
|
|
.dai-root .dai-msg-ai-detail {
|
|
font-size: 12.5px;
|
|
line-height: 1.55;
|
|
color: var(--dai-text-secondary);
|
|
white-space: pre-line;
|
|
}
|
|
.dai-root .dai-msg-name {
|
|
font-size: 11.5px;
|
|
font-weight: 600;
|
|
color: var(--dai-text-secondary);
|
|
}
|
|
.dai-root .dai-msg-footer {
|
|
font-size: 11px;
|
|
color: var(--dai-text-muted);
|
|
}
|
|
/* Copy button — only revealed on hover of the message row. */
|
|
.dai-root .dai-msg-actions {
|
|
opacity: 0;
|
|
transition: opacity 140ms ease;
|
|
}
|
|
.dai-root .dai-msg-row:hover .dai-msg-actions,
|
|
.dai-root .dai-msg-row:focus-within .dai-msg-actions {
|
|
opacity: 1;
|
|
}
|
|
/* --------------------------------------------------------------------------
|
|
Structured metrics
|
|
-------------------------------------------------------------------------- */
|
|
.dai-root .dai-stat-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
gap: 6px;
|
|
width: 100%;
|
|
}
|
|
.dai-root .dai-stat {
|
|
padding: 9px 10px;
|
|
border: 1px solid var(--dai-border);
|
|
border-radius: 5px;
|
|
background: var(--dai-surface-alt);
|
|
}
|
|
.dai-root .dai-stat-value {
|
|
font-size: 19px;
|
|
font-weight: 650;
|
|
line-height: 1.1;
|
|
letter-spacing: -0.02em;
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
.dai-root .dai-stat-label {
|
|
font-size: 11px;
|
|
color: var(--dai-text-muted);
|
|
}
|
|
/* Headline metric — the "primary number is large" rule. */
|
|
.dai-root .dai-metric-value {
|
|
font-size: 28px;
|
|
font-weight: 680;
|
|
line-height: 1.05;
|
|
letter-spacing: -0.025em;
|
|
color: var(--dai-text);
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
.dai-root .dai-metric-label {
|
|
font-size: 12px;
|
|
color: var(--dai-text-secondary);
|
|
}
|
|
/* --------------------------------------------------------------------------
|
|
Typing / loading
|
|
-------------------------------------------------------------------------- */
|
|
.dai-root .dai-typing {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
height: 18px;
|
|
}
|
|
.dai-root .dai-typing span {
|
|
width: 5px;
|
|
height: 5px;
|
|
border-radius: 999px;
|
|
background: var(--dai-text-muted);
|
|
animation: dai-bounce 1.3s ease-in-out infinite;
|
|
}
|
|
.dai-root .dai-typing span:nth-child(2) {
|
|
animation-delay: 0.16s;
|
|
}
|
|
.dai-root .dai-typing span:nth-child(3) {
|
|
animation-delay: 0.32s;
|
|
}
|
|
@keyframes dai-bounce {
|
|
0%,
|
|
70%,
|
|
100% {
|
|
opacity: 0.3;
|
|
transform: translateY(0);
|
|
}
|
|
35% {
|
|
opacity: 0.9;
|
|
transform: translateY(-3px);
|
|
}
|
|
}
|
|
.dai-root .dai-shimmer {
|
|
height: 9px;
|
|
border-radius: 999px;
|
|
background: linear-gradient(90deg, var(--dai-surface-hover) 25%, #e9eef5 37%, var(--dai-surface-hover) 63%);
|
|
background-size: 400% 100%;
|
|
animation: dai-shimmer 1.5s ease infinite;
|
|
}
|
|
@keyframes dai-shimmer {
|
|
from {
|
|
background-position: 100% 50%;
|
|
}
|
|
to {
|
|
background-position: 0 50%;
|
|
}
|
|
}
|
|
/* --------------------------------------------------------------------------
|
|
States (error / empty)
|
|
-------------------------------------------------------------------------- */
|
|
.dai-root .dai-state {
|
|
padding: 12px;
|
|
border: 1px solid var(--dai-border);
|
|
border-radius: 5px;
|
|
background: var(--dai-surface-alt);
|
|
}
|
|
.dai-root .dai-state-title {
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
color: var(--dai-text);
|
|
}
|
|
.dai-root .dai-state-body {
|
|
font-size: 12.5px;
|
|
line-height: 1.5;
|
|
color: var(--dai-text-secondary);
|
|
}
|
|
.dai-root .dai-state-icon {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 26px;
|
|
height: 26px;
|
|
border-radius: 5px;
|
|
flex: 0 0 auto;
|
|
}
|
|
/* --------------------------------------------------------------------------
|
|
Jump-to-latest
|
|
-------------------------------------------------------------------------- */
|
|
.dai-jump {
|
|
position: absolute;
|
|
left: 50%;
|
|
bottom: 8px;
|
|
transform: translateX(-50%);
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
padding: 5px 11px;
|
|
font: inherit;
|
|
font-size: 12px;
|
|
color: var(--dai-text);
|
|
background: var(--dai-surface);
|
|
border: 1px solid var(--dai-border-strong);
|
|
border-radius: 999px;
|
|
box-shadow: 0 4px 14px rgba(15, 23, 42, 0.1);
|
|
cursor: pointer;
|
|
z-index: 2;
|
|
}
|
|
.dai-jump:hover {
|
|
background: var(--dai-surface-alt);
|
|
}
|
|
.dai-jump:focus-visible {
|
|
outline: 2px solid var(--dai-accent);
|
|
outline-offset: 2px;
|
|
}
|
|
/* --------------------------------------------------------------------------
|
|
Composer
|
|
-------------------------------------------------------------------------- */
|
|
.dai-root .dai-composer-wrap {
|
|
flex: 0 0 auto;
|
|
padding: 12px 16px calc(12px + env(safe-area-inset-bottom, 0px));
|
|
border-top: 1px solid #f1f5f9;
|
|
background: #ffffff;
|
|
}
|
|
.dai-root .dai-composer {
|
|
border: 1px solid #e2e8f0;
|
|
border-radius: 9999px;
|
|
background: #f8fafc;
|
|
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.03);
|
|
padding: 6px 6px 6px 16px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
width: 100%;
|
|
transition:
|
|
border-color 140ms ease,
|
|
box-shadow 140ms ease,
|
|
background-color 140ms ease;
|
|
}
|
|
.dai-root .dai-composer[data-focused='true'] {
|
|
background: #ffffff;
|
|
border-color: #cbd5e1;
|
|
box-shadow: 0 0 0 3px rgba(15, 23, 42, 0.05);
|
|
}
|
|
|
|
/* --------------------------------------------------------------------------
|
|
Conversation history.
|
|
|
|
A list of rows, not cards: these are destinations, and dense rows scan
|
|
faster than boxes when you are looking for one you remember.
|
|
-------------------------------------------------------------------------- */
|
|
.dai-root .dai-history {
|
|
padding: 4px 0;
|
|
}
|
|
.dai-root .dai-history-row {
|
|
width: 100%;
|
|
border-radius: 10px;
|
|
padding-right: 4px;
|
|
transition: background-color 140ms ease;
|
|
}
|
|
.dai-root .dai-history-row:hover {
|
|
background: var(--dai-surface-alt);
|
|
}
|
|
.dai-root .dai-history-open {
|
|
flex: 1 1 auto;
|
|
min-width: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2px;
|
|
align-items: flex-start;
|
|
text-align: left;
|
|
padding: 9px 10px;
|
|
border: none;
|
|
background: transparent;
|
|
font: inherit;
|
|
cursor: pointer;
|
|
border-radius: 10px;
|
|
}
|
|
.dai-root .dai-history-open:focus-visible {
|
|
outline: 2px solid var(--dai-accent);
|
|
outline-offset: -2px;
|
|
}
|
|
.dai-root .dai-history-title {
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
color: var(--dai-text);
|
|
display: block;
|
|
width: 100%;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
.dai-root .dai-history-meta {
|
|
font-size: 11px;
|
|
color: var(--dai-text-muted);
|
|
}
|
|
.dai-root .dai-history-empty-title {
|
|
font-size: 13.5px;
|
|
font-weight: 600;
|
|
color: var(--dai-text);
|
|
}
|
|
.dai-root .dai-history-empty {
|
|
font-size: 12.5px;
|
|
line-height: 1.5;
|
|
color: var(--dai-text-muted);
|
|
}
|
|
|
|
/* Microphone.
|
|
|
|
Astryx's smallest dictation button is 28px, two short of the send button it
|
|
sits beside — a visible step between two controls on the same row. Pinned to
|
|
match, with the glyph scaled through font-size because the icon inside is
|
|
sized in em units (the same reason the side nav icons needed font-size
|
|
rather than width). */
|
|
.dai-root .dai-mic {
|
|
width: 30px;
|
|
height: 30px;
|
|
min-width: 30px;
|
|
border-radius: 50%;
|
|
font-size: 16px;
|
|
}
|
|
.dai-root .dai-mic svg {
|
|
width: 1em;
|
|
height: 1em;
|
|
}
|
|
|
|
/* --------------------------------------------------------------------------
|
|
Send button.
|
|
|
|
This had NO styles at all — the class was referenced once, in a
|
|
reduced-motion reset, and nowhere else. It rendered as a bare 16px arrow
|
|
glyph on a transparent background with square corners: not a button, just an
|
|
icon sitting next to the microphone. Verified in the browser before fixing.
|
|
-------------------------------------------------------------------------- */
|
|
.dai-root .dai-send {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
flex: 0 0 auto;
|
|
width: 32px;
|
|
height: 32px;
|
|
padding: 0;
|
|
border: none;
|
|
border-radius: 50%;
|
|
background: #2563eb;
|
|
color: #ffffff;
|
|
cursor: pointer;
|
|
transition:
|
|
background-color 140ms ease,
|
|
opacity 140ms ease,
|
|
transform 140ms ease;
|
|
}
|
|
.dai-root .dai-send:hover:not(:disabled) {
|
|
background: #1d4ed8;
|
|
transform: scale(1.05);
|
|
}
|
|
.dai-root .dai-send:active:not(:disabled) {
|
|
transform: scale(0.96);
|
|
}
|
|
.dai-root .dai-send:focus-visible {
|
|
outline: 2px solid #2563eb;
|
|
outline-offset: 2px;
|
|
}
|
|
/* Disabled is the resting state until something is typed, so it has to read as
|
|
"not yet", not as "broken" — a filled grey circle, same shape and weight. */
|
|
.dai-root .dai-send:disabled {
|
|
background: #f1f5f9;
|
|
color: #94a3b8;
|
|
cursor: default;
|
|
}
|
|
.dai-root .dai-composer textarea {
|
|
display: block;
|
|
flex: 1 1 auto;
|
|
width: 100%;
|
|
min-width: 0;
|
|
border: none;
|
|
outline: none;
|
|
resize: none;
|
|
padding: 4px 0;
|
|
margin: 0;
|
|
font: inherit;
|
|
font-size: 13.5px;
|
|
line-height: 1.45;
|
|
color: var(--dai-text);
|
|
background: transparent;
|
|
max-height: 108px; /* ~5 lines */
|
|
overflow-y: auto;
|
|
}
|
|
.dai-root .dai-composer textarea::placeholder {
|
|
color: var(--dai-text-muted);
|
|
}
|
|
.dai-root .dai-hint {
|
|
font-size: 10.5px;
|
|
line-height: 1.3;
|
|
color: var(--dai-text-muted);
|
|
padding-inline: 2px;
|
|
}
|
|
/* --------------------------------------------------------------------------
|
|
The trigger's styles lived here and are gone with it.
|
|
|
|
`.dai-trigger` / `.dai-trigger-mark` styled DoormileAI/index.jsx, a trigger
|
|
button component that nothing rendered — AdminLayout has always declared
|
|
its own button in JSX with Tailwind classes. Component and CSS removed
|
|
together so neither is left looking like the thing that controls the
|
|
header button. To change that button, edit AdminLayout.
|
|
-------------------------------------------------------------------------- */
|
|
/* --------------------------------------------------------------------------
|
|
Responsive
|
|
-------------------------------------------------------------------------- */
|
|
/* Tablet and small laptop, 768-900px: the panel is a full-width overlay with a
|
|
scrim (see the max-width:900px block above), so the only thing left to do is
|
|
stop the suggestion strip from taking a third of a short screen — there is
|
|
much less height to spend here than on a desktop. */
|
|
@media (max-width: 900px) {
|
|
.dai-root .dai-suggestions-bar {
|
|
max-height: 26vh;
|
|
}
|
|
}
|
|
|
|
/* The block that used to sit here set `--dai-panel-width: 396px` at 1279px.
|
|
That variable is declared and never read — the panel sizes from
|
|
--dai-dock-width — so the rule had no effect on anything. Removed rather
|
|
than left looking load-bearing. */
|
|
|
|
@media (max-width: 767px) {
|
|
.dai-root {
|
|
--dai-inset: 0px;
|
|
}
|
|
/* Phone: chips get a touch target and room to breathe, and the strip is
|
|
capped harder still because the on-screen keyboard takes the bottom half
|
|
of the viewport the moment the composer is focused. */
|
|
.dai-root .dai-suggestions-bar {
|
|
max-height: 22vh;
|
|
padding: 8px 12px 6px;
|
|
}
|
|
.dai-suggestion {
|
|
padding: 9px 14px;
|
|
font-size: 13px;
|
|
}
|
|
.dai-panel {
|
|
width: 100vw;
|
|
max-width: 100vw;
|
|
border-radius: 0;
|
|
border: none;
|
|
}
|
|
}
|
|
/* --------------------------------------------------------------------------
|
|
Reduced motion — no slide, no pulse, no shimmer. Opacity only.
|
|
-------------------------------------------------------------------------- */
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.dai-panel,
|
|
.dai-scrim {
|
|
transition: opacity 1ms linear;
|
|
}
|
|
.dai-panel {
|
|
transform: none;
|
|
}
|
|
.dai-msg,
|
|
.dai-live-dot,
|
|
.dai-typing span,
|
|
.dai-shimmer,
|
|
.dai-suggestion,
|
|
.dai-send {
|
|
animation: none !important;
|
|
transition: none !important;
|
|
}
|
|
}
|
|
/* --------------------------------------------------------------------------
|
|
Write-action confirm card & buttons
|
|
-------------------------------------------------------------------------- */
|
|
.dai-root .dai-action {
|
|
padding: 12px 14px;
|
|
border: 1px solid #e2e8f0;
|
|
border-radius: 12px;
|
|
background: #ffffff;
|
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04), 0 1px 2px rgba(0, 0, 0, 0.02);
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
}
|
|
.dai-root .dai-action-icon {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
flex: 0 0 auto;
|
|
width: 28px;
|
|
height: 28px;
|
|
border-radius: 8px;
|
|
color: #4f46e5;
|
|
background: rgba(99, 102, 241, 0.1);
|
|
}
|
|
.dai-root .dai-action-summary {
|
|
font-size: 13px;
|
|
line-height: 1.4;
|
|
font-weight: 600;
|
|
color: var(--dai-text);
|
|
}
|
|
.dai-root .dai-action-note {
|
|
font-size: 12px;
|
|
color: #64748b;
|
|
line-height: 1.4;
|
|
}
|
|
.dai-root .dai-ok {
|
|
color: #047857;
|
|
font-weight: 500;
|
|
}
|
|
.dai-root .dai-err {
|
|
color: #b91c1c;
|
|
font-weight: 500;
|
|
}
|
|
.dai-root .dai-action button {
|
|
cursor: pointer;
|
|
}
|
|
/* --------------------------------------------------------------------------
|
|
Overflow containment
|
|
--------------------------------------------------------------------------
|
|
Flex items default to `min-width: auto`, which means they refuse to shrink
|
|
below their content's intrinsic width. Two side-by-side TextInputs in the
|
|
create-customer form therefore pushed the message column wider than the
|
|
panel and produced a horizontal scrollbar. Every link in the chain from the
|
|
scroll region down to the field has to opt out of that.
|
|
-------------------------------------------------------------------------- */
|
|
.dai-root .dai-thread,
|
|
.dai-root .dai-msg,
|
|
.dai-root .dai-msg-row,
|
|
.dai-root .dai-action,
|
|
.dai-root .dai-form {
|
|
min-width: 0;
|
|
max-width: 100%;
|
|
}
|
|
/* Two fields per row that genuinely share the width. `flex-wrap` is the
|
|
belt-and-braces part: if a field ever can't compress far enough (a long
|
|
label, a narrower panel), the row drops to two lines instead of pushing the
|
|
panel sideways again. */
|
|
.dai-root .dai-form-row {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 8px;
|
|
width: 100%;
|
|
min-width: 0;
|
|
}
|
|
.dai-root .dai-form-row > * {
|
|
flex: 1 1 140px;
|
|
min-width: 0;
|
|
}
|
|
/* Astryx's TextInput sizes itself to content unless told otherwise. */
|
|
.dai-root .dai-form input,
|
|
.dai-root .dai-form-row input {
|
|
width: 100%;
|
|
min-width: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
.dai-root .dai-field-err {
|
|
font-size: 11.5px;
|
|
line-height: 1.4;
|
|
color: #b91c1c;
|
|
}
|
|
/* --------------------------------------------------------------------------
|
|
Source footer + full lists
|
|
-------------------------------------------------------------------------- */
|
|
|
|
/* Timestamp left, Sources toggle hard right, both on ONE line. It was a
|
|
wrapping baseline row, and the toggle — a shrinkable flex item with inline
|
|
content — gave up its chevron to a second line whenever the panel got
|
|
narrow, which read as a stray arrow floating under the text. */
|
|
.dai-root .dai-msg-foot {
|
|
display: flex;
|
|
align-items: center;
|
|
flex-wrap: nowrap;
|
|
gap: 10px;
|
|
width: 100%;
|
|
min-width: 0;
|
|
}
|
|
/* The timestamp is the only part allowed to give up space, and it ellipsises
|
|
rather than wrapping. */
|
|
.dai-root .dai-msg-foot .dai-msg-footer {
|
|
flex: 1 1 auto;
|
|
min-width: 0;
|
|
overflow: hidden;
|
|
white-space: nowrap;
|
|
text-overflow: ellipsis;
|
|
}
|
|
/* The toggle is one atom: label and chevron cannot be separated. */
|
|
.dai-root .dai-msg-foot .dai-sources {
|
|
flex: 0 0 auto;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
align-self: center;
|
|
white-space: nowrap;
|
|
padding: 3px 7px;
|
|
margin-right: -7px;
|
|
border-radius: 5px;
|
|
color: var(--dai-text-muted);
|
|
transition: background 140ms ease, color 140ms ease;
|
|
}
|
|
.dai-root .dai-msg-foot .dai-sources:hover {
|
|
background: var(--dai-surface-hover);
|
|
color: var(--dai-text);
|
|
text-decoration: none;
|
|
}
|
|
.dai-root .dai-msg-foot .dai-sources svg {
|
|
flex: 0 0 auto;
|
|
transition: transform 160ms var(--dai-ease);
|
|
}
|
|
.dai-root .dai-msg-foot .dai-sources[aria-expanded='true'] svg {
|
|
transform: rotate(180deg);
|
|
}
|
|
/* A full, readable list instead of "…and 4 more". Scrolls past ~12 rows so a
|
|
long list can never push the panel's height around. */
|
|
.dai-root .dai-list {
|
|
width: 100%;
|
|
min-width: 0;
|
|
border: 1px solid var(--dai-border);
|
|
border-radius: 5px;
|
|
background: var(--dai-surface-alt);
|
|
overflow: hidden;
|
|
}
|
|
.dai-root .dai-list-head {
|
|
display: flex;
|
|
align-items: baseline;
|
|
justify-content: space-between;
|
|
gap: 8px;
|
|
padding: 7px 10px;
|
|
border-bottom: 1px solid var(--dai-border);
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
letter-spacing: 0.04em;
|
|
text-transform: uppercase;
|
|
color: var(--dai-text-muted);
|
|
}
|
|
.dai-root .dai-list-body {
|
|
max-height: 260px;
|
|
overflow-y: auto;
|
|
overflow-x: hidden;
|
|
overscroll-behavior: contain;
|
|
}
|
|
.dai-root .dai-list-item {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
align-items: baseline;
|
|
gap: 2px 8px;
|
|
padding: 6px 10px;
|
|
font-size: 12.5px;
|
|
line-height: 1.4;
|
|
color: var(--dai-text);
|
|
border-top: 1px solid var(--dai-divider, rgba(15, 23, 42, 0.05));
|
|
}
|
|
.dai-root .dai-list-item:first-child {
|
|
border-top: none;
|
|
}
|
|
.dai-root .dai-list-index {
|
|
flex: 0 0 auto;
|
|
min-width: 18px;
|
|
font-size: 11px;
|
|
font-variant-numeric: tabular-nums;
|
|
color: var(--dai-text-muted);
|
|
}
|
|
/* The label must not be crushed to zero width by a long value — that is what
|
|
made "address" render one letter per line. It sizes to its content and the
|
|
value takes the remaining space, wrapping onto its own full-width line when
|
|
there isn't enough room for both. */
|
|
.dai-root .dai-list-label {
|
|
flex: 0 1 auto;
|
|
min-width: 0;
|
|
overflow-wrap: anywhere;
|
|
}
|
|
.dai-root .dai-list-meta {
|
|
flex: 1 1 auto;
|
|
min-width: 0;
|
|
text-align: right;
|
|
font-size: 11.5px;
|
|
color: var(--dai-text-secondary);
|
|
overflow-wrap: anywhere;
|
|
}
|
|
/* A value that wrapped to its own line reads better left-aligned under its
|
|
label than pinned to the right edge. */
|
|
.dai-root .dai-list-item > .dai-list-meta:only-child,
|
|
.dai-root .dai-list-meta[data-wrapped='true'] {
|
|
text-align: left;
|
|
}
|
|
/* --------------------------------------------------------------------------
|
|
Documentation answers
|
|
--------------------------------------------------------------------------
|
|
Retrieved passages are shown VERBATIM with their source file. They are prose
|
|
rather than a figure, so they get a quieter, wider treatment than a metric —
|
|
and the source line matters as much as the text, because the operator needs
|
|
to know this came from a document, not from live data.
|
|
-------------------------------------------------------------------------- */
|
|
.dai-root .dai-doc {
|
|
padding: 10px 11px;
|
|
border: 1px solid var(--dai-border);
|
|
border-left: 2px solid var(--dai-ai-from);
|
|
border-radius: 5px;
|
|
background: var(--dai-surface-alt);
|
|
font-size: 12.5px;
|
|
line-height: 1.55;
|
|
color: var(--dai-text);
|
|
white-space: pre-line;
|
|
overflow-wrap: anywhere;
|
|
max-height: 320px;
|
|
overflow-y: auto;
|
|
}
|
|
.dai-root .dai-doc-source {
|
|
font-size: 11px;
|
|
color: var(--dai-text-muted);
|
|
font-variant-numeric: tabular-nums;
|
|
}
|