Sheet mobile (#2502)

* feat: make mobile popovers as sheet

* feat: sheets for other big modals mobile

* (fix):multiselectpopover, prevent placeholder re-render

* fix: google drive thing

* chore: push new notification text

* revert: remove vite dev server overrides

Drops the host/allowedHosts block added for local Tailscale testing —
it shouldn't have been committed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: ManishMadan2882 <manishmadan321@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
authored and GitHub committed 2026-05-26 15:48:04 +01:00
1 parent b2d1d7c2c3
commit 80d2082529
14 files changed
+282 -167

No files matched your search

@@ -18,7 +18,7 @@ class GoogleDriveAuth(BaseConnectorAuth):
"""
SCOPES = [
'https://www.googleapis.com/auth/drive.file'
'https://www.googleapis.com/auth/drive.readonly'
]
def __init__(self):
@@ -43,23 +43,27 @@ class GoogleDriveAuth(BaseConnectorAuth):
"redirect_uris": [self.redirect_uri]
}
},
scopes=self.SCOPES
scopes=self.SCOPES,
# Flow is rebuilt at exchange time, so an auto-generated
# code_verifier wouldn't survive; confidential client doesn't
# need PKCE anyway.
autogenerate_code_verifier=False,
)
flow.redirect_uri = self.redirect_uri
authorization_url, _ = flow.authorization_url(
access_type='offline',
prompt='consent',
include_granted_scopes='false',
state=state
)
return authorization_url
except Exception as e:
logging.error(f"Error generating authorization URL: {e}")
raise
def exchange_code_for_tokens(self, authorization_code: str) -> Dict[str, Any]:
try:
if not authorization_code:
@@ -75,7 +79,8 @@ class GoogleDriveAuth(BaseConnectorAuth):
"redirect_uris": [self.redirect_uri]
}
},
scopes=self.SCOPES
scopes=self.SCOPES,
autogenerate_code_verifier=False,
)
flow.redirect_uri = self.redirect_uri
@@ -175,7 +175,8 @@ class GoogleDriveLoader(BaseConnectorLoader):
try:
file_metadata = self.service.files().get(
fileId=file_id,
fields='id,name,mimeType,size,createdTime,modifiedTime,parents'
fields='id,name,mimeType,size,createdTime,modifiedTime,parents',
supportsAllDrives=True
).execute()
return self._process_file(file_metadata, load_content=load_content)
@@ -228,7 +229,9 @@ class GoogleDriveLoader(BaseConnectorLoader):
fields='nextPageToken,files(id,name,mimeType,size,createdTime,modifiedTime,parents)',
pageToken=page_token,
pageSize=page_size,
orderBy='name'
orderBy='name',
supportsAllDrives=True,
includeItemsFromAllDrives=True
).execute()
items = results.get('files', [])
@@ -308,7 +311,7 @@ class GoogleDriveLoader(BaseConnectorLoader):
mimeType=export_mime_type
)
else:
request = self.service.files().get_media(fileId=file_id)
request = self.service.files().get_media(fileId=file_id, supportsAllDrives=True)
file_io = io.BytesIO()
downloader = MediaIoBaseDownload(file_io, request)
@@ -378,7 +381,8 @@ class GoogleDriveLoader(BaseConnectorLoader):
def _download_single_file(self, file_id: str, local_dir: str) -> bool:
file_metadata = self.service.files().get(
fileId=file_id,
fields='name,mimeType'
fields='name,mimeType',
supportsAllDrives=True
).execute()
file_name = file_metadata['name']
@@ -400,7 +404,7 @@ class GoogleDriveLoader(BaseConnectorLoader):
if not full_path.endswith(extension):
full_path += extension
else:
request = self.service.files().get_media(fileId=file_id)
request = self.service.files().get_media(fileId=file_id, supportsAllDrives=True)
with open(full_path, 'wb') as f:
downloader = MediaIoBaseDownload(f, request)
@@ -423,7 +427,9 @@ class GoogleDriveLoader(BaseConnectorLoader):
q=query,
fields='nextPageToken, files(id, name, mimeType)',
pageToken=page_token,
pageSize=1000
pageSize=1000,
supportsAllDrives=True,
includeItemsFromAllDrives=True
).execute()
items = results.get('files', [])
@@ -516,7 +522,8 @@ class GoogleDriveLoader(BaseConnectorLoader):
try:
folder_metadata = self.service.files().get(
fileId=folder_id,
fields='name'
fields='name',
supportsAllDrives=True
).execute()
folder_name = folder_metadata.get('name', '')
folder_path = os.path.join(local_dir, folder_name)
+2 -2
View File
@@ -2,6 +2,6 @@
VITE_BASE_URL=http://localhost:5173
VITE_API_HOST=http://127.0.0.1:7091
VITE_API_STREAMING=true
VITE_NOTIFICATION_TEXT="What's new in 0.15.0 — Changelog"
VITE_NOTIFICATION_LINK="https://blog.docsgpt.cloud/docsgpt-0-15-masters-long-term-memory-and-tooling/"
VITE_NOTIFICATION_TEXT="What's new in 0.17 — Changelog"
VITE_NOTIFICATION_LINK="https://blog.docsgpt.cloud/docsgpt-0-17-production-grade-from-the-ground-up/"
VITE_GOOGLE_CLIENT_ID=896376503572-u46l78n8ctgtdr4dlei4u06jv6rbpqc5.apps.googleusercontent.com
@@ -199,6 +199,7 @@ export default function ScheduleFormModal({
: t('agents.schedules.modal.create')
}
size="md"
mobileVariant="sheet"
className="w-[min(560px,92vw)] sm:p-6"
contentClassName="max-h-[80vh]"
>
+99 -71
View File
@@ -78,81 +78,109 @@ const ConnectorAuth: React.FC<ConnectorAuthProps> = ({
}
};
const handleAuth = async () => {
try {
completedRef.current = false;
// Close any popup left over from a previous click before wiping
// the ref — otherwise the old window keeps living with no
// interval watching it and no listener handling its messages.
if (authWindowRef.current && !authWindowRef.current.closed) {
authWindowRef.current.close();
}
authWindowRef.current = null;
cleanup();
const authResponse = await userService.getConnectorAuthUrl(
provider,
token,
);
if (!mountedRef.current) return;
if (!authResponse.ok) {
throw new Error(
`${t('modals.uploadDoc.connectors.auth.authUrlFailed')}: ${authResponse.status}`,
);
}
const authData = await authResponse.json();
if (!mountedRef.current) return;
if (!authData.success || !authData.authorization_url) {
throw new Error(
authData.error || t('modals.uploadDoc.connectors.auth.authUrlFailed'),
);
}
const authWindow = window.open(
authData.authorization_url,
`${provider}-auth`,
'width=500,height=600,scrollbars=yes,resizable=yes',
);
if (!authWindow) {
throw new Error(t('modals.uploadDoc.connectors.auth.popupBlocked'));
}
authWindowRef.current = authWindow;
messageHandlerRef.current = handleAuthMessage;
window.addEventListener('message', handleAuthMessage as any);
const checkClosed = window.setInterval(() => {
if (authWindow.closed) {
clearInterval(checkClosed);
intervalRef.current = null;
if (messageHandlerRef.current) {
window.removeEventListener(
'message',
messageHandlerRef.current as any,
);
messageHandlerRef.current = null;
}
authWindowRef.current = null;
if (!completedRef.current) {
onError(t('modals.uploadDoc.connectors.auth.authCancelled'));
}
}
}, 1000);
intervalRef.current = checkClosed;
} catch (error) {
if (!mountedRef.current) return;
onError(
error instanceof Error
? error.message
: t('modals.uploadDoc.connectors.auth.authFailed'),
);
const handleAuth = () => {
completedRef.current = false;
// Close any popup left over from a previous click before wiping
// the ref — otherwise the old window keeps living with no
// interval watching it and no listener handling its messages.
if (authWindowRef.current && !authWindowRef.current.closed) {
authWindowRef.current.close();
}
authWindowRef.current = null;
cleanup();
// Synchronous popup inside the click; navigated below after the
// async URL fetch resolves. Otherwise Safari blocks it as non-gesture.
const authWindow = window.open(
'about:blank',
`${provider}-auth`,
'width=500,height=600,scrollbars=yes,resizable=yes',
);
if (!authWindow) {
onError(t('modals.uploadDoc.connectors.auth.popupBlocked'));
return;
}
authWindowRef.current = authWindow;
(async () => {
try {
const authResponse = await userService.getConnectorAuthUrl(
provider,
token,
);
if (!mountedRef.current) {
authWindow.close();
return;
}
if (!authResponse.ok) {
authWindow.close();
throw new Error(
`${t('modals.uploadDoc.connectors.auth.authUrlFailed')}: ${authResponse.status}`,
);
}
const authData = await authResponse.json();
if (!mountedRef.current) {
authWindow.close();
return;
}
if (!authData.success || !authData.authorization_url) {
authWindow.close();
throw new Error(
authData.error ||
t('modals.uploadDoc.connectors.auth.authUrlFailed'),
);
}
if (authWindow.closed) {
// User closed the placeholder before we resolved.
authWindowRef.current = null;
onError(t('modals.uploadDoc.connectors.auth.authCancelled'));
return;
}
authWindow.location.href = authData.authorization_url;
messageHandlerRef.current = handleAuthMessage;
window.addEventListener('message', handleAuthMessage as any);
const checkClosed = window.setInterval(() => {
if (authWindow.closed) {
clearInterval(checkClosed);
intervalRef.current = null;
if (messageHandlerRef.current) {
window.removeEventListener(
'message',
messageHandlerRef.current as any,
);
messageHandlerRef.current = null;
}
authWindowRef.current = null;
if (!completedRef.current) {
onError(t('modals.uploadDoc.connectors.auth.authCancelled'));
}
}
}, 1000);
intervalRef.current = checkClosed;
} catch (error) {
if (!authWindow.closed) {
authWindow.close();
}
authWindowRef.current = null;
if (!mountedRef.current) return;
onError(
error instanceof Error
? error.message
: t('modals.uploadDoc.connectors.auth.authFailed'),
);
}
})();
};
// Release interval, message listener, and popup on unmount only.
useEffect(() => {
// Re-arm on mount; React 19 strict-mode's double-invoke would otherwise
// leave this stuck at false from the prior cleanup, aborting later awaits.
mountedRef.current = true;
return () => {
mountedRef.current = false;
cleanup();
+10 -2
View File
@@ -1,6 +1,12 @@
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import useDrivePicker from 'react-google-drive-picker';
import drivePickerImport from 'react-google-drive-picker';
// Vite 8 CJS interop returns the namespace object for this lib instead of
// unwrapping `default`; tolerate both shapes.
const useDrivePicker = ((
drivePickerImport as unknown as { default?: typeof drivePickerImport }
).default ?? drivePickerImport) as typeof drivePickerImport;
import userService from '../api/services/userService';
import ConnectorAuth from './ConnectorAuth';
@@ -116,6 +122,8 @@ const GoogleDrivePicker: React.FC<GoogleDrivePickerProps> = ({
try {
const clientId: string = import.meta.env.VITE_GOOGLE_CLIENT_ID;
const developerKey: string =
import.meta.env.VITE_GOOGLE_PICKER_API_KEY ?? '';
// Derive appId from clientId (extract numeric part before first dash)
const appId = clientId ? clientId.split('-')[0] : null;
@@ -129,7 +137,7 @@ const GoogleDrivePicker: React.FC<GoogleDrivePickerProps> = ({
openPicker({
clientId: clientId,
developerKey: '',
developerKey,
appId: appId,
setSelectFolderEnabled: false,
viewId: 'DOCS',
+84 -57
View File
@@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next';
import CheckmarkIcon from '../assets/checkmark.svg';
import NoFilesDarkIcon from '../assets/no-files-dark.svg';
import NoFilesIcon from '../assets/no-files.svg';
import { useDarkTheme } from '../hooks';
import { useDarkTheme, useMediaQuery } from '../hooks';
import { cn } from '@/lib/utils';
import Spinner from './Spinner';
import {
@@ -16,6 +16,7 @@ import {
CommandList,
} from './ui/command';
import { Popover, PopoverContent, PopoverTrigger } from './ui/popover';
import { Sheet, SheetContent, SheetTitle, SheetTrigger } from './ui/sheet';
export type MultiSelectPopoverItem = {
id: string;
@@ -84,6 +85,7 @@ export function MultiSelectPopover({
}: MultiSelectPopoverProps) {
const { t } = useTranslation();
const [isDarkTheme] = useDarkTheme();
const { isMobile } = useMediaQuery();
const selectedSet = React.useMemo(() => new Set(selectedIds), [selectedIds]);
const orderedItems = React.useMemo(() => {
@@ -175,6 +177,86 @@ export function MultiSelectPopover({
</div>
);
const commandBody = (
<Command shouldFilter={searchable} className="bg-transparent">
{title && (
<div className="shrink-0 px-4 pt-4">
<h3 className="text-lg font-medium text-gray-900 dark:text-white">
{title}
</h3>
</div>
)}
{searchable && (
<div className="shrink-0 px-4 pt-4">
<CommandInput placeholder={effectivePlaceholder} className="h-10" />
</div>
)}
{loading ? (
<div className="text-foreground flex items-center justify-center py-8 dark:text-white">
<Spinner size="small" />
</div>
) : (
<div className="dark:border-border mx-4 my-4 min-h-0 grow overflow-hidden rounded-md border border-[#D9D9D9]">
<CommandList className="max-h-full overflow-y-auto">
<CommandEmpty>{renderEmptyState()}</CommandEmpty>
{hasGroups ? (
grouped.groupOrder.map((groupKey) => {
const groupItems = grouped.map.get(groupKey) || [];
if (groupItems.length === 0) return null;
return (
<CommandGroup
key={`group-${groupKey || 'ungrouped'}`}
heading={groupKey || undefined}
className="[&_[cmdk-group-heading]]:bg-muted/50 [&_[cmdk-group-heading]]:dark:bg-card [&_[cmdk-group-heading]]:text-muted-foreground p-0 [&_[cmdk-group-heading]]:sticky [&_[cmdk-group-heading]]:top-0 [&_[cmdk-group-heading]]:z-10 [&_[cmdk-group-heading]]:border-b [&_[cmdk-group-heading]]:border-[#D9D9D9] [&_[cmdk-group-heading]]:px-3 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-semibold [&_[cmdk-group-heading]]:uppercase [&_[cmdk-group-heading]]:dark:border-[#2E2E2E]"
>
{groupItems.map(renderItem)}
</CommandGroup>
);
})
) : (
<CommandGroup className="p-0">
{orderedItems.map(renderItem)}
</CommandGroup>
)}
</CommandList>
</div>
)}
{footer && (
<div className="border-border dark:border-border shrink-0 border-t px-4 py-4">
{footer}
</div>
)}
</Command>
);
if (isMobile) {
return (
<Sheet open={open} onOpenChange={onOpenChange}>
<SheetTrigger asChild>{trigger}</SheetTrigger>
<SheetContent
side="bottom"
showCloseButton={false}
onOpenAutoFocus={(e) => e.preventDefault()}
className={cn(
'border-border bg-background dark:border-border dark:bg-card flex max-h-[85vh] flex-col gap-0 overflow-hidden rounded-t-2xl p-0 pb-[env(safe-area-inset-bottom)]',
contentClassName,
)}
>
<SheetTitle className="sr-only">
{title || effectivePlaceholder}
</SheetTitle>
<div
className="mx-auto mt-2 mb-1 h-1.5 w-12 shrink-0 rounded-full bg-gray-300 dark:bg-gray-600"
aria-hidden="true"
/>
{commandBody}
</SheetContent>
</Sheet>
);
}
return (
<Popover open={open} onOpenChange={onOpenChange}>
<PopoverTrigger asChild>{trigger}</PopoverTrigger>
@@ -186,62 +268,7 @@ export function MultiSelectPopover({
contentClassName,
)}
>
<Command shouldFilter={searchable} className="bg-transparent">
{title && (
<div className="shrink-0 px-4 pt-4">
<h3 className="text-lg font-medium text-gray-900 dark:text-white">
{title}
</h3>
</div>
)}
{searchable && (
<div className="shrink-0 px-4 pt-4">
<CommandInput
placeholder={effectivePlaceholder}
className="h-10"
/>
</div>
)}
{loading ? (
<div className="text-foreground flex items-center justify-center py-8 dark:text-white">
<Spinner size="small" />
</div>
) : (
<div className="dark:border-border mx-4 my-4 grow overflow-hidden rounded-md border border-[#D9D9D9]">
<CommandList className="max-h-none">
<CommandEmpty>{renderEmptyState()}</CommandEmpty>
{items.length === 0 ? (
renderEmptyState()
) : hasGroups ? (
grouped.groupOrder.map((groupKey) => {
const groupItems = grouped.map.get(groupKey) || [];
if (groupItems.length === 0) return null;
return (
<CommandGroup
key={`group-${groupKey || 'ungrouped'}`}
heading={groupKey || undefined}
className="[&_[cmdk-group-heading]]:bg-muted/50 [&_[cmdk-group-heading]]:dark:bg-card [&_[cmdk-group-heading]]:text-muted-foreground p-0 [&_[cmdk-group-heading]]:sticky [&_[cmdk-group-heading]]:top-0 [&_[cmdk-group-heading]]:z-10 [&_[cmdk-group-heading]]:border-b [&_[cmdk-group-heading]]:border-[#D9D9D9] [&_[cmdk-group-heading]]:px-3 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-semibold [&_[cmdk-group-heading]]:uppercase [&_[cmdk-group-heading]]:dark:border-[#2E2E2E]"
>
{groupItems.map(renderItem)}
</CommandGroup>
);
})
) : (
<CommandGroup className="p-0">
{orderedItems.map(renderItem)}
</CommandGroup>
)}
</CommandList>
</div>
)}
{footer && (
<div className="border-border dark:border-border shrink-0 border-t px-4 py-4">
{footer}
</div>
)}
</Command>
{commandBody}
</PopoverContent>
</Popover>
);
+29 -5
View File
@@ -12,10 +12,12 @@ import {
DialogPortal,
DialogTitle,
} from '@/components/ui/dialog';
import { useMediaQuery } from '../../hooks';
import { cn } from '@/lib/utils';
import * as DialogPrimitive from '@radix-ui/react-dialog';
type ModalSize = 'sm' | 'md' | 'lg' | 'xl' | 'full';
type ModalMobileVariant = 'modal' | 'sheet';
const SIZE_CLASSES: Record<ModalSize, string> = {
sm: 'sm:max-w-sm',
@@ -38,6 +40,7 @@ export type ModalProps = {
showCloseButton?: boolean;
isPerformingTask?: boolean;
size?: ModalSize;
mobileVariant?: ModalMobileVariant;
};
const Modal = React.forwardRef<HTMLDivElement, ModalProps>(function Modal(
@@ -54,9 +57,12 @@ const Modal = React.forwardRef<HTMLDivElement, ModalProps>(function Modal(
showCloseButton = true,
isPerformingTask = false,
size = 'md',
mobileVariant = 'modal',
},
ref,
) {
const { isMobile } = useMediaQuery();
const isMobileSheet = mobileVariant === 'sheet' && isMobile;
const shouldShowCloseButton = showCloseButton && !isPerformingTask;
// When a task is performing, block click-outside / pointer-outside to
@@ -94,8 +100,12 @@ const Modal = React.forwardRef<HTMLDivElement, ModalProps>(function Modal(
<DialogPrimitive.Content
ref={ref}
data-slot="modal-content"
data-mobile-sheet={isMobileSheet ? '' : undefined}
onPointerDownOutside={blockOutsideInteractions}
onInteractOutside={blockOutsideInteractions}
onOpenAutoFocus={
isMobileSheet ? (event) => event.preventDefault() : undefined
}
// Radix portals this to <body> in the DOM, but React still bubbles
// synthetic events through the JSX tree. Stop the bubble at the
// modal boundary so consumers mounted inside clickable cards (e.g.
@@ -103,25 +113,39 @@ const Modal = React.forwardRef<HTMLDivElement, ModalProps>(function Modal(
// onClick when the user interacts inside the modal.
onClick={(event) => event.stopPropagation()}
className={cn(
'bg-card text-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 shadow-modal fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-2xl p-8 duration-200 outline-none',
SIZE_CLASSES[size],
className,
'bg-card text-foreground data-[state=open]:animate-in data-[state=closed]:animate-out shadow-modal fixed z-50 duration-200 outline-none',
isMobileSheet
? 'data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 flex max-h-[90vh] w-full flex-col gap-3 rounded-t-2xl px-4 pt-2 pb-[max(env(safe-area-inset-bottom),1rem)]'
: cn(
'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 top-[50%] left-[50%] grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-2xl p-8',
SIZE_CLASSES[size],
className,
),
)}
>
{isMobileSheet && (
<div
className="mx-auto h-1.5 w-12 shrink-0 rounded-full bg-gray-300 dark:bg-gray-600"
aria-hidden="true"
/>
)}
{titleNode}
{descriptionNode}
<div
className={cn(
'no-scrollbar text-foreground overflow-y-auto',
// overflow-y-auto forces overflow-x:auto, clipping focus-ring
// box-shadows; px-1 keeps them inside the clip rect.
isMobileSheet && 'min-h-0 grow px-1',
contentClassName,
)}
>
{children}
</div>
{footer ? (
<div className="flex justify-end gap-2">{footer}</div>
<div className="flex shrink-0 justify-end gap-2">{footer}</div>
) : null}
{shouldShowCloseButton && (
{shouldShowCloseButton && !isMobileSheet && (
<DialogClose
className="ring-offset-background focus:ring-ring absolute top-3 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none"
aria-label="Close"
+25 -15
View File
@@ -3,7 +3,7 @@ layer(base);
@import 'tailwindcss';
@import "tw-animate-css";
@import 'tw-animate-css';
@custom-variant dark (&:is(.dark *));
@@ -55,12 +55,12 @@ layer(base);
/* Light theme scrollbar */
&::-webkit-scrollbar-thumb {
background: #E2E8F0;
background: #e2e8f0;
border-radius: 9999px;
}
&::-webkit-scrollbar-thumb:hover {
background: #8C9198;
background: #8c9198;
}
/* Dark theme scrollbar */
@@ -70,11 +70,11 @@ layer(base);
}
.dark &::-webkit-scrollbar-thumb:hover {
background: #F0F0F0;
background: #f0f0f0;
}
scrollbar-width: thin;
scrollbar-color: #E2E8F0 transparent;
scrollbar-color: #e2e8f0 transparent;
.dark & {
scrollbar-color: #949494 transparent;
@@ -97,11 +97,11 @@ layer(base);
}
&:hover::-webkit-scrollbar-thumb {
background: #E2E8F0;
background: #e2e8f0;
}
&:hover::-webkit-scrollbar-thumb:hover {
background: #8C9198;
background: #8c9198;
}
.dark &::-webkit-scrollbar-thumb {
@@ -114,7 +114,7 @@ layer(base);
}
.dark &:hover::-webkit-scrollbar-thumb:hover {
background: #F0F0F0;
background: #f0f0f0;
}
/* Standard scrollbar properties (Chrome 121+, Firefox) */
@@ -122,7 +122,7 @@ layer(base);
scrollbar-color: transparent transparent;
&:hover {
scrollbar-color: #E2E8F0 transparent;
scrollbar-color: #e2e8f0 transparent;
}
.dark & {
@@ -173,7 +173,7 @@ layer(base);
& th,
& td {
scrollbar-width: thin;
scrollbar-color: #E2E8F0 transparent;
scrollbar-color: #e2e8f0 transparent;
}
.dark & th,
@@ -206,24 +206,24 @@ layer(base);
background: transparent;
}
::-webkit-scrollbar-thumb {
background: #E2E8F0;
background: #e2e8f0;
border-radius: 9999px;
}
::-webkit-scrollbar-thumb:hover {
background: #8C9198;
background: #8c9198;
}
.dark ::-webkit-scrollbar-thumb {
background: #949494;
}
.dark ::-webkit-scrollbar-thumb:hover {
background: #F0F0F0;
background: #f0f0f0;
}
/* Firefox: base scrollbar styles (Firefox ignores ::-webkit-scrollbar) */
@supports (-moz-appearance: none) {
* {
scrollbar-width: thin;
scrollbar-color: #E2E8F0 transparent;
scrollbar-color: #e2e8f0 transparent;
}
.dark * {
scrollbar-color: #949494 transparent;
@@ -741,7 +741,9 @@ Avoid over-scrolling in mobile browsers
}
@keyframes thinkingBounce {
0%, 80%, 100% {
0%,
80%,
100% {
transform: scale(0.6);
opacity: 0.4;
}
@@ -902,3 +904,11 @@ Avoid over-scrolling in mobile browsers
@apply bg-background text-foreground;
}
}
/* Picker portals outside Radix's tree; force interactivity + raise z so
clicks land when a Modal/Sheet is also open. */
.picker-dialog,
.picker-dialog-bg {
pointer-events: auto !important;
z-index: 9999 !important;
}
+1
View File
@@ -297,6 +297,7 @@ export default function CustomModelModal({
: t('settings.customModels.addTitle')
}
size="lg"
mobileVariant="sheet"
className="max-w-[600px] md:w-[80vw] lg:w-[60vw]"
>
<div className="flex h-full flex-col">
+1
View File
@@ -593,6 +593,7 @@ export default function MCPServerModal({
: t('settings.tools.mcp.addServer')
}
size="lg"
mobileVariant="sheet"
className="max-w-[600px] md:w-[80vw] lg:w-[60vw]"
>
<div className="flex h-full flex-col">
@@ -157,6 +157,7 @@ export default function SearchConversationsModal({
hideTitle
title={t('modals.searchConversations.searchPlaceholder')}
showCloseButton={false}
mobileVariant="sheet"
className="w-[92vw] !max-w-xl !p-0"
contentClassName="max-h-[70vh]"
>
+2 -1
View File
@@ -703,7 +703,8 @@ export default function PromptsModal({
hideTitle
title={type === 'ADD' ? 'Add Prompt' : 'Edit Prompt'}
size="lg"
className="bg-card dark:bg-card mx-4 mt-16 w-[95vw] max-w-[650px] rounded-2xl px-4 py-4 sm:px-6 sm:py-6 md:max-w-[860px] md:px-8 md:py-6 lg:max-w-[980px]"
mobileVariant="sheet"
className="bg-card dark:bg-card w-[95vw] max-w-[650px] rounded-2xl px-4 py-4 sm:px-6 sm:py-6 md:max-w-[860px] md:px-8 md:py-6 lg:max-w-[980px]"
contentClassName="!overflow-visible"
>
{view}
+1
View File
@@ -897,6 +897,7 @@ function Upload({
hideTitle
title={t('modals.uploadDoc.label')}
size="lg"
mobileVariant="sheet"
className="max-h-[90vh] w-11/12 sm:max-h-none sm:w-auto sm:min-w-[600px] md:min-w-[700px]"
contentClassName="max-h-[80vh] sm:max-h-none"
>