Doormile AI: replace the bot popover with a right-side slide-over

Renames the operator assistant to "Doormile AI / Operations Copilot" and
moves it from a header Popover to a portalled slide-over panel.

- DoormileAI/ — trigger, panel, welcome, message, composer and shared
  primitives. Assistant turns are deliberately NOT bubbles; that is what
  keeps this reading as part of the dashboard rather than a bolted-on
  chatbot.
- pageContext.js — route-aware suggested questions. Every suggestion is a
  phrasing the matcher actually resolves; a chip that returns "I can't
  answer that" is worse than no chip.
- DoormileAI.css — panel styling, animation, responsive and reduced-motion.
  Selectors that style an Astryx Stack are written `.dai-root .x` because
  `padding={0}` emits a StyleX atomic at the same specificity as a bare
  class, so a single class can lose on stylesheet order.
- Messages are JSON round-tripped through localStorage, so icons are
  referenced by key rather than stored as React elements — an element does
  not survive the trip and the rehydrated value crashes the next render.
- Errors now render a polished state and log the real error to the console,
  instead of surfacing raw API messages in a toast.

The route, sidebar entry and i18n key for the old standalone Assistant page
are removed with it — this is a header surface, not a page.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-18 15:53:40 +05:30
parent e420223622
commit 5165e9d697
13 changed files with 1689 additions and 167 deletions

View File

@@ -0,0 +1,776 @@
/* ==========================================================================
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).
========================================================================== */
.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;
/* 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: 428px;
--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.
-------------------------------------------------------------------------- */
.dai-scrim {
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
-------------------------------------------------------------------------- */
.dai-panel {
position: fixed;
top: var(--dai-inset);
right: var(--dai-inset);
bottom: var(--dai-inset);
z-index: 1301;
width: var(--dai-panel-width);
max-width: calc(100vw - (var(--dai-inset) * 2));
display: flex;
flex-direction: column;
min-height: 0;
overflow: hidden;
background: var(--dai-surface);
border: 1px solid var(--dai-border);
border-radius: 16px;
box-shadow: var(--dai-shadow);
transform: translateX(calc(100% + var(--dai-inset) * 2));
opacity: 0;
transition:
transform var(--dai-duration) var(--dai-ease),
opacity var(--dai-duration) var(--dai-ease);
}
.dai-panel[data-open='true'] {
transform: translateX(0);
opacity: 1;
}
.dai-panel:focus {
outline: none;
}
/* --------------------------------------------------------------------------
Header
-------------------------------------------------------------------------- */
.dai-root .dai-header {
flex: 0 0 auto;
padding: 14px 12px 12px 14px;
border-bottom: 1px solid var(--dai-border);
}
.dai-root .dai-title {
font-size: 15px;
font-weight: 650;
line-height: 1.2;
letter-spacing: -0.01em;
color: var(--dai-text);
}
.dai-root .dai-subtitle {
font-size: 12px;
line-height: 1.3;
color: var(--dai-text-muted);
}
/* 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: 999px;
color: #ffffff;
background: linear-gradient(135deg, var(--dai-ai-from), var(--dai-ai-to));
box-shadow: 0 0 0 3px var(--dai-ai-glow);
}
.dai-root .dai-spark[data-size='sm'] {
width: 22px;
height: 22px;
}
.dai-root .dai-spark[data-size='md'] {
width: 30px;
height: 30px;
}
.dai-root .dai-spark[data-size='lg'] {
width: 44px;
height: 44px;
box-shadow: 0 0 0 6px var(--dai-ai-glow);
}
/* 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 {
flex: 0 0 auto;
padding: 7px 14px;
font-size: 11.5px;
color: var(--dai-text-muted);
background: var(--dai-surface-alt);
border-bottom: 1px solid var(--dai-border);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* --------------------------------------------------------------------------
Scroll region
-------------------------------------------------------------------------- */
.dai-scroll {
flex: 1 1 auto;
min-height: 0;
overflow-y: auto;
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;
}
/* --------------------------------------------------------------------------
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
-------------------------------------------------------------------------- */
.dai-suggestion {
display: flex;
align-items: center;
gap: 10px;
width: 100%;
padding: 10px 11px;
text-align: left;
font: inherit;
color: var(--dai-text);
background: var(--dai-surface);
border: 1px solid var(--dai-border);
border-radius: 12px;
cursor: pointer;
transition:
background-color 140ms ease,
border-color 140ms ease,
transform 140ms ease;
}
.dai-suggestion:hover {
background: var(--dai-surface-alt);
border-color: var(--dai-border-strong);
}
.dai-suggestion:active {
transform: scale(0.99);
}
.dai-suggestion:focus-visible {
outline: 2px solid var(--dai-accent);
outline-offset: 2px;
}
.dai-root .dai-suggestion-icon {
display: inline-flex;
align-items: center;
justify-content: center;
flex: 0 0 auto;
width: 26px;
height: 26px;
border-radius: 8px;
background: var(--dai-surface-hover);
color: var(--dai-text-secondary);
}
.dai-root .dai-suggestion-text {
flex: 1 1 auto;
font-size: 13px;
line-height: 1.35;
}
.dai-root .dai-suggestion-arrow {
flex: 0 0 auto;
color: var(--dai-text-muted);
opacity: 0;
transform: translateX(-3px);
transition:
opacity 140ms ease,
transform 140ms ease;
}
.dai-suggestion:hover .dai-suggestion-arrow,
.dai-suggestion:focus-visible .dai-suggestion-arrow {
opacity: 1;
transform: translateX(0);
}
/* 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: 4px;
}
/* --------------------------------------------------------------------------
Conversation
-------------------------------------------------------------------------- */
.dai-root .dai-thread {
padding: 16px 14px 8px;
}
.dai-root .dai-msg {
animation: dai-enter 220ms var(--dai-ease) both;
}
@keyframes dai-enter {
from {
opacity: 0;
transform: translateY(4px);
}
to {
opacity: 1;
transform: none;
}
}
/* User — compact, right aligned, brand surface. */
.dai-root .dai-msg-user {
max-width: 82%;
margin-left: auto;
padding: 8px 12px;
border-radius: 14px;
border-bottom-right-radius: 6px;
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: 10px;
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: 12px;
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: 8px;
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;
}
.dai-scroll-wrap {
position: relative;
flex: 1 1 auto;
min-height: 0;
display: flex;
flex-direction: column;
}
/* --------------------------------------------------------------------------
Composer
-------------------------------------------------------------------------- */
.dai-root .dai-composer-wrap {
flex: 0 0 auto;
padding: 10px 12px calc(10px + env(safe-area-inset-bottom, 0px));
border-top: 1px solid var(--dai-border);
background: var(--dai-surface);
}
.dai-root .dai-composer {
border: 1px solid var(--dai-border-strong);
border-radius: 16px;
background: var(--dai-surface);
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
padding: 8px 8px 6px 12px;
transition:
border-color 140ms ease,
box-shadow 140ms ease;
}
.dai-root .dai-composer[data-focused='true'] {
border-color: var(--dai-accent);
box-shadow: 0 0 0 3px rgba(15, 23, 42, 0.06);
}
.dai-root .dai-composer textarea {
display: block;
width: 100%;
border: none;
outline: none;
resize: none;
padding: 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-send {
display: inline-flex;
align-items: center;
justify-content: center;
flex: 0 0 auto;
width: 30px;
height: 30px;
border: none;
border-radius: 999px;
color: var(--dai-accent-contrast);
background: var(--dai-accent);
cursor: pointer;
transition:
opacity 140ms ease,
transform 140ms ease;
}
.dai-send:hover:not(:disabled) {
opacity: 0.88;
}
.dai-send:active:not(:disabled) {
transform: scale(0.93);
}
.dai-send:disabled {
opacity: 0.28;
cursor: default;
}
.dai-send:focus-visible {
outline: 2px solid var(--dai-accent);
outline-offset: 2px;
}
.dai-root .dai-hint {
font-size: 11px;
color: var(--dai-text-muted);
}
/* --------------------------------------------------------------------------
Trigger (lives in the app TopNav)
-------------------------------------------------------------------------- */
.dai-trigger {
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
padding: 0;
border-radius: 9px;
border: 1px solid transparent;
background: transparent;
color: #4f46e5;
cursor: pointer;
transition:
background-color 140ms ease,
border-color 140ms ease,
transform 140ms ease;
}
.dai-trigger:hover {
background: rgba(99, 102, 241, 0.09);
border-color: rgba(99, 102, 241, 0.16);
}
.dai-trigger:active {
transform: scale(0.94);
}
.dai-trigger:focus-visible {
outline: 2px solid var(--dai-accent);
outline-offset: 2px;
}
.dai-trigger[data-active='true'] {
color: #ffffff;
background: linear-gradient(135deg, var(--dai-ai-from), var(--dai-ai-to));
border-color: transparent;
}
/* --------------------------------------------------------------------------
Responsive
-------------------------------------------------------------------------- */
@media (max-width: 1279px) {
.dai-root {
--dai-panel-width: 396px;
}
}
@media (max-width: 767px) {
.dai-root {
--dai-inset: 0px;
}
.dai-panel {
width: 100vw;
max-width: 100vw;
border-radius: 0;
border: none;
}
.dai-msg-user {
max-width: 88%;
}
}
/* --------------------------------------------------------------------------
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,
.dai-trigger {
animation: none !important;
transition: none !important;
}
}