Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 9 additions & 10 deletions apps/web/src/components/mobile-not-supported.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,19 @@ import { Smartphone } from 'lucide-react';

export const MobileNotSupported = () => {
return (
<div className="min-w-screen flex h-full min-h-screen w-full items-center justify-center">
<div className="flex flex-col items-center gap-8 text-center w-128">
<div className="min-w-screen flex h-full min-h-screen w-full items-center justify-center p-6">
<div className="flex flex-col items-center gap-8 text-center max-w-md">
<Smartphone className="h-10 w-10 text-gray-800" />
<h2 className="text-4xl text-gray-800">Mobile not supported</h2>
<h2 className="text-2xl font-semibold text-gray-800">
Phone version coming soon
</h2>
<p className="text-sm text-gray-500">
Hey there! Thanks for checking out Brainbox.
Brainbox works best on tablets and desktops. A native phone app is in
development and will be available soon.
</p>
<p className="text-sm text-gray-500">
Right now, Brainbox is not quite ready for mobile devices just yet.
For the best experience, please hop onto a desktop or laptop. We're
working hard to bring you an awesome mobile experience soon.
</p>
<p className="text-sm text-gray-500 mt-4">
Thanks for your patience and support!
In the meantime, please use a tablet or desktop browser for the best
experience.
</p>
</div>
</div>
Expand Down
19 changes: 15 additions & 4 deletions apps/web/src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,20 @@ export const isOpfsSupported = async (): Promise<boolean> => {
}
};

const mobileDeviceRegex =
/Android|iPhone|iPad|iPod|Opera Mini|IEMobile|WPDesktop/i;
// Phone detection - blocks phones, allows tablets
const phoneDeviceRegex = /iPhone|iPod|Opera Mini|IEMobile|WPDesktop/i;

export const isMobileDevice = (): boolean => {
return mobileDeviceRegex.test(navigator.userAgent);
// Android phone detection (excludes tablets)
const isAndroidPhone = (): boolean => {
const ua = navigator.userAgent;
// Android tablets typically have "Tablet" or larger screen identifiers
// Android phones have "Mobile" in the user agent
return /Android/i.test(ua) && /Mobile/i.test(ua);
};

export const isPhoneDevice = (): boolean => {
return phoneDeviceRegex.test(navigator.userAgent) || isAndroidPhone();
Comment on lines +21 to +32
Copy link

Copilot AI Dec 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The phone detection logic removed "iPad" from the regex, but modern iPads (iPadOS 13+) report their user agent as "Macintosh" when requesting desktop sites, making them indistinguishable from actual desktops via user agent alone. While this change allows tablets through, it may inadvertently allow iPads that could benefit from tablet-specific UX. Consider adding detection for iPads using feature detection (e.g., checking for touch support combined with large screen size) or relying solely on the viewport-based breakpoint detection in the useDevice hook.

Copilot uses AI. Check for mistakes.
};

// Keep for backwards compatibility, but deprecate
export const isMobileDevice = isPhoneDevice;
6 changes: 3 additions & 3 deletions apps/web/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ import { eventBus } from '@brainbox/client/lib';
import { BrowserNotSupported } from '@brainbox/web/components/browser-not-supported';
import { MobileNotSupported } from '@brainbox/web/components/mobile-not-supported';
import { ColanodeWorkerApi } from '@brainbox/web/lib/types';
import { isMobileDevice, isOpfsSupported } from '@brainbox/web/lib/utils';
import { isPhoneDevice, isOpfsSupported } from '@brainbox/web/lib/utils';
import { Root } from '@brainbox/web/root';
import DedicatedWorker from '@brainbox/web/workers/dedicated?worker';

const initializeApp = async () => {
const isMobile = isMobileDevice();
if (isMobile) {
const isPhone = isPhoneDevice();
if (isPhone) {
const root = createRoot(document.getElementById('root') as HTMLElement);
root.render(<MobileNotSupported />);
return;
Expand Down
11 changes: 11 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion packages/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,15 +73,16 @@
"date-fns": "^4.1.0",
"framer-motion": "^12.23.26",
"is-hotkey": "^0.2.0",
"markdown-it": "^14.1.0",
"lucide-react": "^0.539.0",
"markdown-it": "^14.1.0",
"prosemirror-markdown": "^1.13.1",
"re-resizable": "^6.11.2",
"react": "^19.1.1",
"react-circular-progressbar": "^2.2.0",
"react-day-picker": "^9.8.1",
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
"react-dnd-touch-backend": "^16.0.1",
"react-dom": "^19.1.1",
"react-hook-form": "^7.62.0",
"react-intersection-observer": "^9.16.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
import { Separator } from '@brainbox/ui/components/ui/separator';
import { useDatabase } from '@brainbox/ui/contexts/database';
import { useDatabaseView } from '@brainbox/ui/contexts/database-view';
import { useDevice } from '@brainbox/ui/hooks/use-device';
import { isFilterableField, isSortableField } from '@brainbox/ui/lib/databases';
import { cn } from '@brainbox/ui/lib/utils';

Expand All @@ -46,6 +47,7 @@ export const TableViewFieldHeader = memo(
({ viewField }: TableViewFieldHeaderProps) => {
const database = useDatabase();
const view = useDatabaseView();
const { isTouch } = useDevice();

// Find current sort for this field
const currentSort = view.sorts.find(
Expand Down Expand Up @@ -327,16 +329,15 @@ export const TableViewFieldHeader = memo(
}}
handleClasses={{
right: cn(
// Wider invisible hit area; avoid adding any extra border line
'bg-transparent transition-colors duration-150',
// Only show subtle background while actively resizing to indicate grip
'transition-colors duration-150',
isTouch ? 'bg-border/50' : 'bg-transparent',
isResizing && 'bg-primary/50'
),
}}
handleStyles={{
right: {
width: '8px',
right: '-4px',
width: isTouch ? '24px' : '8px',
right: isTouch ? '-12px' : '-4px',
top: '0px',
height: '100%',
cursor: 'col-resize',
Expand Down
27 changes: 20 additions & 7 deletions packages/ui/src/components/layouts/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { SettingsDialog } from '@brainbox/ui/components/settings/settings-dialog
import { LayoutContext } from '@brainbox/ui/contexts/layout';
import { useServer } from '@brainbox/ui/contexts/server';
import { useWorkspace } from '@brainbox/ui/contexts/workspace';
import { useDevice } from '@brainbox/ui/hooks/use-device';
import { useLayoutState } from '@brainbox/ui/hooks/use-layout-state';
import { useLiveQuery } from '@brainbox/ui/hooks/use-live-query';
import { useWindowSize } from '@brainbox/ui/hooks/use-window-size';
Expand All @@ -24,6 +25,8 @@ export const Layout = () => {
const server = useServer();
const workspace = useWorkspace();
const windowSize = useWindowSize();
const { isTablet, isTouch } = useDevice();
const useTouchHandles = isTablet || isTouch;
const [showShortcuts, setShowShortcuts] = useState(false);
const [showSearch, setShowSearch] = useState(false);
const [showSettings, setShowSettings] = useState(false);
Expand Down Expand Up @@ -69,7 +72,7 @@ export const Layout = () => {
!server.isOutdated && leftContainerMetadata.tabs.length > 0;

const shouldDisplayRight =
!server.isOutdated && rightContainerMetadata.tabs.length > 0;
!server.isOutdated && rightContainerMetadata.tabs.length > 0 && !isTablet;

useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
Expand All @@ -91,6 +94,12 @@ export const Layout = () => {
return () => window.removeEventListener('keydown', handleKeyDown);
}, [handleSidebarToggle]);

useEffect(() => {
if (isTablet && !sidebarMetadata.collapsed) {
handleSidebarToggle();
}
}, [isTablet]);
Copy link

Copilot AI Dec 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The useEffect has a missing dependency on handleSidebarToggle. When handleSidebarToggle changes, this effect won't re-run, which could lead to stale closures. The effect should either include handleSidebarToggle in its dependency array, or should be structured to avoid the dependency (e.g., by checking sidebar state and calling toggle conditionally).

Suggested change
}, [isTablet]);
}, [isTablet, sidebarMetadata.collapsed, handleSidebarToggle]);

Copilot uses AI. Check for mistakes.
Comment on lines +97 to +101
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: missing handleSidebarToggle in dependency array will cause stale closure

Suggested change
useEffect(() => {
if (isTablet && !sidebarMetadata.collapsed) {
handleSidebarToggle();
}
}, [isTablet]);
useEffect(() => {
if (isTablet && !sidebarMetadata.collapsed) {
handleSidebarToggle();
}
}, [isTablet, handleSidebarToggle, sidebarMetadata.collapsed]);
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/ui/src/components/layouts/layout.tsx
Line: 97:101

Comment:
**logic:** missing `handleSidebarToggle` in dependency array will cause stale closure

```suggestion
  useEffect(() => {
    if (isTablet && !sidebarMetadata.collapsed) {
      handleSidebarToggle();
    }
  }, [isTablet, handleSidebarToggle, sidebarMetadata.collapsed]);
```

How can I resolve this? If you propose a fix, please make it concise.


return (
<LayoutContext.Provider
value={{
Expand Down Expand Up @@ -136,12 +145,14 @@ export const Layout = () => {
topRight: false,
}}
handleClasses={{
right: 'opacity-0 hover:opacity-100 bg-blue-300 z-30',
right: useTouchHandles
? 'opacity-100 bg-border z-30'
: 'opacity-0 hover:opacity-100 bg-blue-300 z-30',
}}
handleStyles={{
right: {
width: '3px',
right: '-3px',
width: useTouchHandles ? '12px' : '3px',
right: useTouchHandles ? '-6px' : '-3px',
},
}}
onResize={(_, __, ref) => {
Expand Down Expand Up @@ -188,12 +199,14 @@ export const Layout = () => {
topRight: false,
}}
handleClasses={{
left: 'opacity-0 hover:opacity-100 bg-blue-300 z-30',
left: useTouchHandles
? 'opacity-100 bg-border z-30'
: 'opacity-0 hover:opacity-100 bg-blue-300 z-30',
}}
handleStyles={{
left: {
width: '3px',
left: '-3px',
width: useTouchHandles ? '12px' : '3px',
left: useTouchHandles ? '-6px' : '-3px',
},
}}
onResize={(_, __, ref) => {
Expand Down
19 changes: 16 additions & 3 deletions packages/ui/src/components/layouts/sidebars/right-sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { useAccount } from '@brainbox/ui/contexts/account';
import { useLayout } from '@brainbox/ui/contexts/layout';
import { useRadar } from '@brainbox/ui/contexts/radar';
import { useWorkspace } from '@brainbox/ui/contexts/workspace';
import { useDevice } from '@brainbox/ui/hooks/use-device';
import { useLiveQuery } from '@brainbox/ui/hooks/use-live-query';
import { useMutation } from '@brainbox/ui/hooks/use-mutation';
import { usePinnedItems } from '@brainbox/ui/hooks/use-pinned-items';
Expand Down Expand Up @@ -48,6 +49,8 @@ export const RightSidebar = ({ onOpenShortcuts }: RightSidebarProps = {}) => {
const radar = useRadar();
const { mutate } = useMutation();
const { pinnedItems } = usePinnedItems();
const { isTablet, isTouch } = useDevice();
const useLargeTargets = isTablet || isTouch;

const channelsQuery = useLiveQuery({
type: 'node.children.get',
Expand Down Expand Up @@ -578,14 +581,20 @@ export const RightSidebar = ({ onOpenShortcuts }: RightSidebarProps = {}) => {
</div>

{/* Icon Bar */}
<div className="w-12 bg-sidebar border-l border-sidebar-border flex flex-col items-center py-2">
<div
className={cn(
'bg-sidebar border-l border-sidebar-border flex flex-col items-center py-2',
useLargeTargets ? 'w-14' : 'w-12'
)}
>
<div className="flex flex-col gap-1">
<IconBarButton
icon={Hash}
label="Channels"
shortcut="⌥C"
isActive={activePanel === 'channels'}
unreadCount={unreadChannelCount}
useLargeTargets={useLargeTargets}
onClick={() => handleIconClick('channels')}
/>
<IconBarButton
Expand All @@ -594,6 +603,7 @@ export const RightSidebar = ({ onOpenShortcuts }: RightSidebarProps = {}) => {
shortcut="⌥D"
isActive={activePanel === 'chats'}
unreadCount={unreadChatCount}
useLargeTargets={useLargeTargets}
onClick={() => handleIconClick('chats')}
/>
</div>
Expand Down Expand Up @@ -627,6 +637,7 @@ interface IconBarButtonProps {
shortcut: string;
isActive: boolean;
unreadCount: number;
useLargeTargets?: boolean;
onClick: () => void;
}

Expand All @@ -636,6 +647,7 @@ function IconBarButton({
shortcut,
isActive,
unreadCount,
useLargeTargets = false,
onClick,
}: IconBarButtonProps) {
return (
Expand All @@ -644,12 +656,13 @@ function IconBarButton({
<button
onClick={onClick}
className={cn(
'relative flex items-center justify-center size-9 rounded-lg',
'relative flex items-center justify-center rounded-lg',
useLargeTargets ? 'size-11' : 'size-9',
'transition-all duration-150 ease-out',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
isActive
? 'bg-surface-muted text-foreground shadow-sm'
: 'text-muted-foreground hover:text-foreground hover:bg-surface-subtle'
: 'text-muted-foreground hover:text-foreground hover:bg-surface-subtle active:bg-surface-muted'
)}
>
<Icon className="size-[18px]" />
Expand Down
Loading