From ebce66a928315613dccc761f9cea9a9469de8a5c Mon Sep 17 00:00:00 2001 From: pat-s Date: Thu, 6 Aug 2026 06:41:27 +0000 Subject: [PATCH 001/133] feat: mobile support for the agent dashboard Lets an agent triage and reply to conversations from a phone. Admin and reports are deliberately untouched, per the discussion in #470. `apps/main/index.html` pinned `width=1280, initial-scale=0.29`, which makes a phone lay the page out at 1280 CSS pixels and scale the document to 29 percent. Switching it to `width=device-width` requires the layout to cope with a real viewport: - Add `useIsMobile`, using `max-width: 767px` so it is the exact complement of Tailwind's `md:`. Align the sidebar primitive's own query to match; at exactly 768px it previously reported mobile while `md:` already applied. - Fold the 3rem icon rail into the drawer. The mobile `Sheet` in `ui/sidebar/Sidebar.vue` was already implemented but unreachable, since the layout viewport could never match its media query. The rail is `collapsible="none"`, which returns before the Sheet branch, so its destinations move into the one drawer rather than nesting a second. - Render the inbox one pane at a time below `md`. Both `ResizablePanelGroup` minimums are percentages of the window, giving a ~205px thread beside a ~117px contact sidebar at 390px, which no class change can fix. The routes are already nested, so list-to-detail is push navigation with a back affordance; only the desktop branch reads the persisted panel sizes. - Show the contact sidebar as a `Sheet`. It has no intrinsic width, being sized entirely by its panel, and `collapse()`/`expand()` are splitter specific, so this is a parallel path off the same emitter event. - Collapse the composer to a tap target that opens the existing fullscreen editor, and make that dialog full-bleed below `sm`; its `max-w-[60%]` gave a 234px modal at 390px. Inline it takes ~280px, which with a soft keyboard raised leaves the thread nothing. - Widen message bubbles and trim the hardcoded 47px gutters on phones, and use `overflow-wrap: break-word` so ordinary words stop splitting mid-character. Reveal the private-note menu on touch via `[@media(hover:hover)]`. - Remove `SmallScreenOverlay`, whose tuned `scale(3)` existed only to cancel out the old `initial-scale`. Desktop is unchanged, verified by screenshot comparison at 1440px. --- frontend/apps/main/index.html | 2 +- frontend/apps/main/src/App.vue | 84 ++--------------- .../src/components/SmallScreenOverlay.vue | 19 ---- .../sidebar/CloseDrawerOnNavigate.vue | 29 ++++++ .../components/sidebar/MobileDrawerNav.vue | 33 +++++++ .../components/sidebar/NotificationBell.vue | 16 +++- .../components/sidebar/PrimaryNavItems.vue | 92 +++++++++++++++++++ .../main/src/components/sidebar/Sidebar.vue | 8 ++ .../src/components/sidebar/SidebarNavUser.vue | 10 +- .../apps/main/src/composables/useIsMobile.js | 10 ++ .../features/conversation/Conversation.vue | 44 ++++++++- .../src/features/conversation/ReplyBox.vue | 31 ++++++- .../features/conversation/ReplyBoxMenuBar.vue | 2 +- .../conversation/message/MessageBubble.vue | 9 +- .../main/src/layouts/inbox/InboxLayout.vue | 71 +++++++++----- .../conversation/ConversationDetailView.vue | 40 +++++++- frontend/shared-ui/assets/styles/main.scss | 17 +++- .../components/ui/sidebar/SidebarProvider.vue | 5 +- 18 files changed, 385 insertions(+), 137 deletions(-) delete mode 100644 frontend/apps/main/src/components/SmallScreenOverlay.vue create mode 100644 frontend/apps/main/src/components/sidebar/CloseDrawerOnNavigate.vue create mode 100644 frontend/apps/main/src/components/sidebar/MobileDrawerNav.vue create mode 100644 frontend/apps/main/src/components/sidebar/PrimaryNavItems.vue create mode 100644 frontend/apps/main/src/composables/useIsMobile.js diff --git a/frontend/apps/main/index.html b/frontend/apps/main/index.html index 1aa0515e..45d72de4 100644 --- a/frontend/apps/main/index.html +++ b/frontend/apps/main/index.html @@ -3,7 +3,7 @@ - + - - -
- - +
+ + - - - - - - - - - - -

{{ t('globals.terms.inbox', 2) }}

-
-
-
- - - - - - - - - - -

{{ t('globals.terms.contact', 2) }}

-
-
-
- - - - - - - - - - -

{{ t('globals.terms.report', 2) }}

-
-
-
- - - - - - - - - - -

{{ t('globals.terms.admin') }}

-
-
-
+
@@ -155,8 +96,6 @@ import { toast as sooner } from 'vue-sonner' import Sidebar from '@main/components/sidebar/Sidebar.vue' import Command from '@/features/command/CommandBox.vue' import CreateConversation from '@/features/conversation/CreateConversation.vue' -import { Inbox, Shield, FileLineChart, BookUser } from 'lucide-vue-next' -import SmallScreenOverlay from '@/components/SmallScreenOverlay.vue' import { useI18n } from 'vue-i18n' import { useRoute } from 'vue-router' import { @@ -166,24 +105,19 @@ import { SidebarGroup, SidebarMenu, SidebarGroupContent, - SidebarMenuButton, SidebarMenuItem, SidebarProvider } from '@shared-ui/components/ui/sidebar' import { Tooltip, TooltipContent, TooltipTrigger } from '@shared-ui/components/ui/tooltip' import SidebarNavUser from '@main/components/sidebar/SidebarNavUser.vue' import NotificationBell from '@main/components/sidebar/NotificationBell.vue' +import PrimaryNavItems from '@main/components/sidebar/PrimaryNavItems.vue' +import { useIsMobile } from './composables/useIsMobile' import api from '@main/api' const route = useRoute() const emitter = useEmitter() - -// Small screen overlay - shown once per session for screens < 768px. -const showSmallScreenOverlay = ref(window.screen.width < 768 && !sessionStorage.getItem('smallScreenDismissed')) -function dismissSmallScreen() { - sessionStorage.setItem('smallScreenDismissed', '1') - showSmallScreenOverlay.value = false -} +const isMobile = useIsMobile() // Remember last inbox path so navigating back from admin/contacts/reports restores it const lastInboxPath = useStorage('lastInboxPath', '') diff --git a/frontend/apps/main/src/components/SmallScreenOverlay.vue b/frontend/apps/main/src/components/SmallScreenOverlay.vue deleted file mode 100644 index 7daf88d4..00000000 --- a/frontend/apps/main/src/components/SmallScreenOverlay.vue +++ /dev/null @@ -1,19 +0,0 @@ - - - diff --git a/frontend/apps/main/src/components/sidebar/CloseDrawerOnNavigate.vue b/frontend/apps/main/src/components/sidebar/CloseDrawerOnNavigate.vue new file mode 100644 index 00000000..aca1cb43 --- /dev/null +++ b/frontend/apps/main/src/components/sidebar/CloseDrawerOnNavigate.vue @@ -0,0 +1,29 @@ + diff --git a/frontend/apps/main/src/components/sidebar/MobileDrawerNav.vue b/frontend/apps/main/src/components/sidebar/MobileDrawerNav.vue new file mode 100644 index 00000000..f1759bfd --- /dev/null +++ b/frontend/apps/main/src/components/sidebar/MobileDrawerNav.vue @@ -0,0 +1,33 @@ + + + diff --git a/frontend/apps/main/src/components/sidebar/NotificationBell.vue b/frontend/apps/main/src/components/sidebar/NotificationBell.vue index a750eb0d..d85b5c51 100644 --- a/frontend/apps/main/src/components/sidebar/NotificationBell.vue +++ b/frontend/apps/main/src/components/sidebar/NotificationBell.vue @@ -9,9 +9,19 @@ > {{ notificationStore.unreadCount > 99 ? '99' : notificationStore.unreadCount }} + + {{ t('globals.terms.notification', 2) }} - + + @@ -19,13 +29,17 @@ diff --git a/frontend/apps/main/src/components/sidebar/Sidebar.vue b/frontend/apps/main/src/components/sidebar/Sidebar.vue index 1dbce356..19028f9d 100644 --- a/frontend/apps/main/src/components/sidebar/Sidebar.vue +++ b/frontend/apps/main/src/components/sidebar/Sidebar.vue @@ -106,6 +106,8 @@ import { AlertDialogHeader, AlertDialogTitle } from '@shared-ui/components/ui/alert-dialog' +import MobileDrawerNav from './MobileDrawerNav.vue' +import CloseDrawerOnNavigate from './CloseDrawerOnNavigate.vue' import { filterNavItems } from '@main/utils/nav-permissions' import { permissions } from '@main/constants/permissions' import { useStorage } from '@vueuse/core' @@ -252,6 +254,7 @@ const viewToDelete = ref(null) :default-open="sidebarOpen" v-on:update:open="sidebarOpen = $event" > +