Files
coder/site/src/index.css
T
TJandJaayden Halko 95386f526a fix(site): improve agents page mobile view (#24508)
closes DES-22030

## Summary

Mobile view cleanup for the agents page — all changes are behind the
`md:` breakpoint so desktop is unchanged.

**Dropdowns:** Full-width on mobile with dynamic positioning via a
`--mobile-dropdown-bottom` CSS custom property set by a `ResizeObserver`
on the chat input box. Three position variants: `-bottom` (above chat
input), `-top` (below header), `-top-below-header` (below sidebar
header). Viewport branching uses a new `isBelowMdViewport()` helper (`<
768px`) so 640–767 px landscape phones pick the mobile branch instead of
the desktop flyout.

**Layout:** On the main agents page, mobile ordering is header → chat
list → chat input using CSS `order` and `contents` on the content
wrapper. The chat input aligns to the bottom of available space. The
sidebar list uses a top/bottom fade mask on mobile to hint at scrollable
content.

**Header:** Settings, Analytics, sound, and notification icons
consolidated into a single meatball menu dropdown on mobile.
Sound/notification toggles use `e.preventDefault()` to keep the menu
open for state feedback. Chime and notification state is lifted into
`AgentCreatePage` and passed down, so the mobile meatball menu and the
desktop `ChimeButton`/`WebPushButton` stay in sync.

**Workspace pill:** Icon-only on mobile (`size-7` round button with
`StatusIcon`), full pill on desktop. Tooltip hidden on mobile to prevent
ghost tooltip after dropdown close.

**Plus menu:** Workspace picker replaces the flyout with an inline
sub-panel on mobile (back button + search list). Desktop flyout
unchanged. `modal={false}` prevents double-tap when switching between
dropdowns.

**Model selector:** Truncated via `shrink` + `min-w-0` on mobile
(flex-based, no fixed max-width), inline provider/context subtext per
item, tooltip hidden on mobile. Added `open` / `onOpenChange` /
`onTriggerTouchStart` props for external control.

**Consistency:** All back/close buttons normalized to `ArrowLeftIcon`.
Right panel, sidebar settings, header `mobileBack`, and workspace
sub-panel all match the chat top bar pattern.

**Misc polish:** Chat tree nodes use `select-none` +
`-webkit-touch-callout:none` on coarse pointers to suppress the
long-press selection/callout on mobile.

<details>
<summary>Files changed (18)</summary>

- `site/src/index.css` — mobile dropdown CSS with 3 position variants
- `site/src/utils/mobile.ts` — new `isBelowMdViewport()` helper
(`<768px`)
- `site/src/pages/AgentsPage/AgentChatPageView.tsx` — bottom padding
`pb-3`
- `site/src/pages/AgentsPage/AgentCreatePage.tsx` — lift chime + webpush
state; pass handlers to header and buttons
- `site/src/pages/AgentsPage/AgentsPageView.tsx` — `contents` wrapper +
sidebar `border-b`
- `site/src/pages/AgentsPage/components/AgentChatInput.tsx` —
`ResizeObserver` composer ref, `plusMenuView` state, inline workspace
picker, `modal={false}`, mobile branching via `isBelowMdViewport`, bg
- `site/src/pages/AgentsPage/components/AgentCreateForm.tsx` —
`order-last` + `items-end` on mobile
- `site/src/pages/AgentsPage/components/AgentPageHeader.tsx` — meatball
menu (controlled chime/webpush props), `ArrowLeftIcon`, `order-first`,
padding, desktop/mobile branching via `matchMedia`
- `site/src/pages/AgentsPage/components/AgentPageHeader.stories.tsx` —
new Storybook coverage + `play` assertions that toggle state stays in
sync across breakpoints
- `site/src/pages/AgentsPage/components/ChimeButton.tsx` — optional
controlled `enabled` / `onToggle` props
- `site/src/pages/AgentsPage/components/WebPushButton.tsx` — optional
controlled `webPush` / `onToggle` props
-
`site/src/pages/AgentsPage/components/ChatElements/CompactOrgSelector.tsx`
— full-width dropdown class
- `site/src/pages/AgentsPage/components/ChatElements/ModelSelector.tsx`
— truncation, inline subtext, tooltip hidden on mobile, new props
(`open`, `onOpenChange`, `onTriggerTouchStart`)
- `site/src/pages/AgentsPage/components/ChatTopBar.tsx` — full-width
dropdown class
- `site/src/pages/AgentsPage/components/ContextUsageIndicator.tsx` —
full-width dropdown class (mobile branch)
- `site/src/pages/AgentsPage/components/Sidebar/AgentsSidebar.tsx` —
`ArrowLeftIcon`, filter dropdown class, top/bottom fade mask on scroll
area, `select-none` on tree nodes
- `site/src/pages/AgentsPage/components/Sidebar/SidebarTabView.tsx` —
`ArrowLeftIcon`, padding, back button placement
- `site/src/pages/AgentsPage/components/WorkspacePill.tsx` — compact
icon trigger, tooltip hidden on mobile, full-width dropdown class

</details>

> 🤖 Generated by Coder Agents

---------

Co-authored-by: Jaayden Halko <jaayden@coder.com>
2026-04-23 13:41:44 +01:00

328 lines
9.4 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
To enable opacity in colors, please use the new HSL format.
Related issue: https://github.com/shadcn-ui/ui/issues/805#issuecomment-1616021820
*/
@tailwind base;
@tailwind components;
@tailwind utilities;
@font-face {
font-family: "seti";
src: url("/seti.woff") format("woff");
font-weight: normal;
font-style: normal;
font-display: swap;
}
@layer base {
:root,
.light {
--content-primary: 240 10% 4%;
--content-secondary: 240 5% 34%;
--content-link: 221 83% 53%;
--content-invert: 0 0% 98%;
--content-disabled: 240 5% 65%;
--content-success: 142 72% 29%;
--content-warning: 27 96% 61%;
--content-destructive: 0 84% 60%;
--surface-primary: 0 0% 100%;
--surface-secondary: 240 5% 96%;
--surface-tertiary: 240 6% 90%;
--surface-quaternary: 240 5% 84%;
--surface-invert-primary: 240 4% 16%;
--surface-invert-secondary: 240 5% 26%;
--surface-destructive: 0 93% 94%;
--surface-green: 141 79% 85%;
--surface-grey: 240 5% 96%;
--surface-orange: 34 100% 92%;
--surface-sky: 191, 67%, 89%;
--surface-red: 0 93% 94%;
--surface-purple: 259, 100%, 95%;
--surface-magenta: 289 100% 98%;
--border-purple: 255 92% 76%;
--border-default: 240 6% 90%;
--border-success: 142 76% 36%;
--border-sky: 203 90% 40%;
--border-green: 138 82% 82%;
--border-magenta: 295, 68%, 46%;
--border-warning: 30.66, 97.16%, 72.35%;
--border-destructive: 0 84% 60%;
--border-secondary: 240 5% 65%;
--overlay-default: 240 5% 84% / 80%;
--radius: 0.5rem;
--highlight-purple: 271, 61%, 35%;
--highlight-green: 143 64% 24%;
--highlight-orange: 30 100% 32%;
--highlight-sky: 195, 61%, 22%;
--highlight-red: 0 74% 42%;
--highlight-magenta: 295, 68%, 40%;
--syntax-key: 211 95% 33%;
--syntax-string: 0 77% 36%;
--syntax-number: 158 88% 28%;
--syntax-boolean: 240 100% 50%;
--git-added: 142 72% 29%;
--git-deleted: 0 74% 42%;
--git-modified: 17 88% 40%;
--git-merged: 271 91% 65%;
--git-added-bright: 142 72% 29%;
--git-deleted-bright: 0 74% 42%;
--git-merged-bright: 272 72% 47%;
--surface-git-added: 141 84% 93%;
--surface-git-deleted: 0 93% 94%;
--surface-git-merged: 269 100% 95%;
--border: 240 5.9% 90%;
--input: 240 5.9% 90%;
--ring: 240 10% 3.9%;
/*
shadcn-compatible aliases consumed by streamdown and
internal components (e.g. shimmer). These names are
intentionally generic so they match shadcn conventions.
Do NOT rename without updating every consumer.
*/
--background: var(--surface-primary);
--foreground: var(--content-primary);
--muted: var(--surface-secondary);
--muted-foreground: var(--content-secondary);
--primary: var(--content-link);
--primary-foreground: var(--surface-primary);
--avatar-lg: 2.5rem;
--avatar-default: 2rem;
--avatar-sm: 1.5rem;
}
.dark {
--content-primary: 0 0% 100%;
--content-secondary: 240 5% 65%;
--content-link: 213 94% 68%;
--content-invert: 240 10% 4%;
--content-disabled: 240 5% 26%;
--content-success: 142 76% 36%;
--content-warning: 31 97% 72%;
--content-destructive: 0 91% 71%;
--surface-primary: 240 10% 4%;
--surface-secondary: 240 6% 10%;
--surface-tertiary: 240 4% 16%;
--surface-quaternary: 240 5% 26%;
--surface-invert-primary: 240 6% 90%;
--surface-invert-secondary: 240 5% 65%;
--surface-destructive: 0 75% 15%;
--surface-green: 145 80% 10%;
--surface-grey: 240 6% 10%;
--surface-orange: 13 81% 15%;
--surface-sky: 196, 67%, 12%;
--surface-red: 0 75% 15%;
--surface-purple: 268, 68%, 17%;
--surface-magenta: 291, 74%, 15%;
--border-purple: 255 92% 76%;
--border-default: 240 4% 16%;
--border-success: 142 76% 36%;
--border-magenta: 292, 100%, 78%;
--border-warning: 30.66, 97.16%, 72.35%;
--border-destructive: 0 91% 71%;
--border-sky: 194 90% 62%;
--border-green: 143 77% 87%;
--border-secondary: 240 5% 26%;
--overlay-default: 240 10% 4% / 80%;
--highlight-purple: 269, 100%, 74%;
--highlight-green: 141 79% 85%;
--highlight-orange: 31 100% 70%;
--highlight-sky: 188, 75%, 80%;
--highlight-red: 0 91% 71%;
--highlight-magenta: 292, 100%, 78%;
--syntax-key: 201 98% 80%;
--syntax-string: 18 47% 64%;
--syntax-number: 100 28% 73%;
--syntax-boolean: 207 61% 59%;
--git-added: 142 77% 73%;
--git-deleted: 0 94% 82%;
--git-modified: 31 97% 72%;
--git-merged: 271 91% 65%;
--git-added-bright: 142 71% 45%;
--git-deleted-bright: 0 91% 71%;
--git-merged-bright: 270 95% 75%;
--surface-git-added: 145 80% 10%;
--surface-git-deleted: 0 75% 15%;
--surface-git-merged: 274 87% 21%;
--border: 240 3.7% 15.9%;
--input: 240 3.7% 15.9%;
--ring: 240 4.9% 83.9%;
/* shadcn-compatible aliases – see :root block for details. */
--background: var(--surface-primary);
--foreground: var(--content-primary);
--muted: var(--surface-secondary);
--muted-foreground: var(--content-secondary);
--primary: var(--content-link);
--primary-foreground: var(--surface-primary);
}
}
@layer components {
/* Map each stripe variant to a color token so the
pseudo-element rules can stay DRY. */
.navbar-stripe-devel {
--stripe-color: var(--content-warning);
}
.navbar-stripe-rc {
--stripe-color: var(--border-sky);
}
/* Thin stripe bars at the top and bottom edges of the
navbar. Using pseudo-elements keeps the stripes out of
the content area so nav links stay readable. */
.navbar-stripe-devel::before,
.navbar-stripe-devel::after,
.navbar-stripe-rc::before,
.navbar-stripe-rc::after {
content: "";
position: absolute;
left: 0;
right: 0;
height: 4px;
background: repeating-linear-gradient(
-45deg,
transparent,
transparent 4px,
hsl(var(--stripe-color) / 0.5) 4px,
hsl(var(--stripe-color) / 0.5) 8px
);
pointer-events: none;
}
.navbar-stripe-devel::before,
.navbar-stripe-rc::before {
top: 0;
}
.navbar-stripe-devel::after,
.navbar-stripe-rc::after {
bottom: 0;
}
@media (max-width: 767px) {
/*
* Full-width mobile dropdowns. We set a --mobile-dropdown-bottom
* custom property on the chat input container so the dropdown
* position tracks the actual input box, not a hardcoded offset.
*/
[data-radix-popper-content-wrapper]:has(> .mobile-full-width-dropdown) {
position: fixed !important;
left: 1rem !important;
width: calc(100vw - 2rem) !important;
min-width: 0 !important;
transform: none !important;
bottom: var(--mobile-dropdown-bottom, 5rem) !important;
top: auto !important;
}
[data-radix-popper-content-wrapper]:has(> .mobile-full-width-dropdown-top) {
bottom: auto !important;
top: var(--mobile-dropdown-top, 3.5rem) !important;
}
[data-radix-popper-content-wrapper]:has(
> .mobile-full-width-dropdown-top-below-header
) {
bottom: auto !important;
top: 5rem !important;
}
.mobile-full-width-dropdown {
width: 100% !important;
min-width: 0 !important;
max-width: none !important;
}
}
}
@layer base {
* {
@apply border-border;
}
html,
body,
#root,
#storybook-root {
@apply h-full;
}
button,
input {
@apply font-sans;
}
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus,
input:-webkit-autofill:active {
-webkit-box-shadow: 0 0 0 100px hsl(var(--surface-primary)) inset !important;
}
::placeholder {
@apply text-content-secondary;
}
fieldset {
@apply border-none p-0 m-0 w-full;
}
/* Global scrollbar color baseline: surface-quaternary thumb on
transparent track. scrollbar-color is inherited, so this cascades
to all scroll containers. We intentionally do NOT set
scrollbar-width globally because any non-auto value opts out of
overlay scrollbars on macOS/Chromium, causing a permanent gutter
on every container (even when content does not overflow). */
:root {
scrollbar-color: hsl(var(--surface-quaternary)) transparent;
}
/* Prevent layout shift when modals open by maintaining scrollbar width.
scrollbar-gutter: stable reserves space for the scrollbar so Radix's
scroll-bar compensation (margin-right/padding-right) is unnecessary
and causes double spacing. We zero it out with !important to win
over Radix's injected !important styles. The @supports guard ensures
we only do this on browsers that actually support scrollbar-gutter.
There’s a related issue on GitHub: Radix UI Primitives Issue #3251
https://github.com/radix-ui/primitives/issues/3251
*/
@supports (scrollbar-gutter: stable) {
html {
scrollbar-gutter: stable;
}
html body[data-scroll-locked] {
--removed-body-scroll-bar-size: 0 !important;
margin-right: 0 !important;
overflow-y: scroll !important;
}
}
/*
This is a temporary fix for MUI Modals/Popovers until they are removed.
When html has scrollbar-gutter: stable, the browser reserves space for the scrollbar.
MUI Modals/Popovers, when locking body scroll, add `overflow: hidden` and `padding-right`
to the body to compensate for the scrollbar they are hiding. This added padding-right
conflicts with the already reserved gutter space, causing a layout shift.
This rule overrides MUI's added padding-right on the body specifically when MUI
is likely to have set both overflow:hidden and padding-right.
*/
body[style*="overflow: hidden"][style*="padding-right"] {
padding-right: 0px;
}
}
@layer utilities {
.text-xs-mono {
@apply text-xs font-mono font-medium;
}
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}