diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 60c2af7e4..2124a2b1f 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -27,6 +27,7 @@ "i18next-browser-languagedetector": "^8.1.0", "i18next-http-backend": "^3.0.2", "jszip": "^3.10.1", + "material-symbols": "^0.33.0", "pdf-lib": "^1.17.1", "pdfjs-dist": "^3.11.174", "react": "^19.1.0", @@ -4175,6 +4176,12 @@ "semver": "bin/semver.js" } }, + "node_modules/material-symbols": { + "version": "0.33.0", + "resolved": "https://registry.npmjs.org/material-symbols/-/material-symbols-0.33.0.tgz", + "integrity": "sha512-t9/Gz+14fClRgN7oVOt5CBuwsjFLxSNP9BRDyMrI5el3IZNvoD94IDGJha0YYivyAow24rCS0WOkAv4Dp+YjNg==", + "license": "Apache-2.0" + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index aa5251545..4f7284717 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -23,6 +23,7 @@ "i18next-browser-languagedetector": "^8.1.0", "i18next-http-backend": "^3.0.2", "jszip": "^3.10.1", + "material-symbols": "^0.33.0", "pdf-lib": "^1.17.1", "pdfjs-dist": "^3.11.174", "react": "^19.1.0", diff --git a/frontend/src/components/shared/QuickAccessBar.css b/frontend/src/components/shared/QuickAccessBar.css new file mode 100644 index 000000000..b1d22fcc3 --- /dev/null +++ b/frontend/src/components/shared/QuickAccessBar.css @@ -0,0 +1,179 @@ +.activeIconScale { + transform: scale(1.3); + transition: transform 0.2s; + z-index: 1; +} + +.iconContainer { + display: flex; + align-items: center; + justify-content: center; + width: 2rem; + height: 2rem; +} + +/* Action icon styles */ +.action-icon-style { + background-color: var(--icon-user-bg); + color: var(--icon-user-color); + border-radius: 50%; + width: 1.5rem; + height: 1.5rem; +} + +/* Main container styles */ +.quick-access-bar-main { + background-color: var(--bg-muted); + width: 5rem; + min-width: 5rem; + max-width: 5rem; + position: relative; + z-index: 10; +} + +/* Rainbow mode container */ +.quick-access-bar-main.rainbow-mode { + background-color: var(--bg-muted); + width: 5rem; + min-width: 5rem; + max-width: 5rem; + position: relative; + z-index: 10; +} + +/* Header padding */ +.quick-access-header { + padding: 1rem 0.5rem 0.5rem 0.5rem; +} + +.nav-header { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + margin-bottom: 0; + gap: 0.5rem; +} + +/* Nav header divider */ +.nav-header-divider { + width: 3.75rem; + border-color: var(--color-gray-300); + margin-top: 0.5rem; + margin-bottom: 1rem; +} + +/* All tools text styles */ +.all-tools-text { + margin-top: 0.75rem; + font-size: 0.75rem; + text-rendering: optimizeLegibility; + font-synthesis: none; +} + +.all-tools-text.active { + color: var(--text-primary); + font-weight: bold; +} + +.all-tools-text.inactive { + color: var(--color-gray-700); + font-weight: normal; +} + +/* Overflow divider */ +.overflow-divider { + width: 3.75rem; + border-color: var(--color-gray-300); + margin: 0 0.5rem; +} + +/* Scrollable content area */ +.quick-access-bar { + overflow-x: auto; + overflow-y: auto; + scrollbar-gutter: stable both-edges; + -webkit-overflow-scrolling: touch; + padding: 0 0.5rem 1rem 0.5rem; +} + +/* Scrollable content container */ +.scrollable-content { + display: flex; + flex-direction: column; + height: 100%; + min-height: 100%; +} + +/* Button text styles */ +.button-text { + margin-top: 0.75rem; + font-size: 0.75rem; + text-rendering: optimizeLegibility; + font-synthesis: none; +} + +.button-text.active { + color: var(--text-primary); + font-weight: bold; +} + +.button-text.inactive { + color: var(--color-gray-700); + font-weight: normal; +} + +/* Content divider */ +.content-divider { + width: 3.75rem; + border-color: var(--color-gray-300); +} + +/* Spacer */ +.spacer { + flex: 1; + margin-top: 1rem; +} + +/* Config button text */ +.config-button-text { + margin-top: 0.75rem; + font-size: 0.75rem; + color: var(--color-gray-700); + font-weight: normal; + text-rendering: optimizeLegibility; + font-synthesis: none; +} + +/* Font size utility */ +.font-size-20 { + font-size: 20px; +} + +/* Hide scrollbar by default, show on scroll (Webkit browsers - Chrome, Safari, Edge) */ +.quick-access-bar::-webkit-scrollbar { + width: 0.5rem; + height: 0.5rem; + background: transparent; +} + +.quick-access-bar:hover::-webkit-scrollbar, +.quick-access-bar:active::-webkit-scrollbar, +.quick-access-bar:focus::-webkit-scrollbar { + background: rgba(0, 0, 0, 0.1); +} + +.quick-access-bar::-webkit-scrollbar-thumb { + background: rgba(0, 0, 0, 0.2); + border-radius: 0.25rem; +} + +.quick-access-bar::-webkit-scrollbar-track { + background: transparent; +} + +/* Firefox scrollbar styling */ +.quick-access-bar { + scrollbar-width: auto; + scrollbar-color: rgba(0, 0, 0, 0.2) transparent; +} \ No newline at end of file diff --git a/frontend/src/components/shared/QuickAccessBar.tsx b/frontend/src/components/shared/QuickAccessBar.tsx index 45f3b28c7..22a49617e 100644 --- a/frontend/src/components/shared/QuickAccessBar.tsx +++ b/frontend/src/components/shared/QuickAccessBar.tsx @@ -1,11 +1,17 @@ -import React, { useState } from "react"; -import { ActionIcon, Stack, Tooltip } from "@mantine/core"; -import MenuBookIcon from "@mui/icons-material/MenuBook"; -import AppsIcon from "@mui/icons-material/Apps"; -import SettingsIcon from "@mui/icons-material/Settings"; +import React, { useState, useRef } from "react"; +import { ActionIcon, Stack, Tooltip, Divider } from "@mantine/core"; +import MenuBookIcon from "@mui/icons-material/MenuBookRounded"; +import AppsIcon from "@mui/icons-material/AppsRounded"; +import SettingsIcon from "@mui/icons-material/SettingsRounded"; +import AutoAwesomeIcon from "@mui/icons-material/AutoAwesomeRounded"; +import FolderIcon from "@mui/icons-material/FolderRounded"; +import PersonIcon from "@mui/icons-material/PersonRounded"; +import NotificationsIcon from "@mui/icons-material/NotificationsRounded"; import { useRainbowThemeContext } from "./RainbowThemeProvider"; import rainbowStyles from '../../styles/rainbow.module.css'; import AppConfigModal from './AppConfigModal'; +import { useIsOverflowing } from '../../hooks/useIsOverflowing'; +import './QuickAccessBar.css'; interface QuickAccessBarProps { onToolsClick: () => void; @@ -16,6 +22,86 @@ interface QuickAccessBarProps { readerMode: boolean; } +interface ButtonConfig { + id: string; + name: string; + icon: React.ReactNode; + tooltip: string; + isRound?: boolean; + size?: 'sm' | 'md' | 'lg' | 'xl'; + onClick: () => void; +} + +function NavHeader({ + activeButton, + setActiveButton, + onReaderToggle, + onToolsClick +}: { + activeButton: string; + setActiveButton: (id: string) => void; + onReaderToggle: () => void; + onToolsClick: () => void; +}) { + return ( + <> +
+ + + + + + + + + + +
+ {/* Divider after top icons */} + + {/* All Tools button below divider */} + +
+ { + setActiveButton('tools'); + onReaderToggle(); + onToolsClick(); + }} + style={{ + backgroundColor: activeButton === 'tools' ? 'var(--icon-tools-bg)' : 'var(--icon-inactive-bg)', + color: activeButton === 'tools' ? 'var(--icon-tools-color)' : 'var(--icon-inactive-color)', + border: 'none', + borderRadius: '8px', + }} + className={activeButton === 'tools' ? 'activeIconScale' : ''} + > + + + + + + All Tools + +
+
+ + ); +} + const QuickAccessBar = ({ onToolsClick, onReaderToggle, @@ -26,55 +112,201 @@ const QuickAccessBar = ({ }: QuickAccessBarProps) => { const { isRainbowMode } = useRainbowThemeContext(); const [configModalOpen, setConfigModalOpen] = useState(false); + const [activeButton, setActiveButton] = useState('tools'); + const scrollableRef = useRef(null); + const isOverflow = useIsOverflowing(scrollableRef); + + const buttonConfigs: ButtonConfig[] = [ + { + id: 'read', + name: 'Read', + icon: , + tooltip: 'Read documents', + size: 'lg', + isRound: false, + onClick: () => { + setActiveButton('read'); + onReaderToggle(); + } + }, + { + id: 'sign', + name: 'Sign', + icon: + + signature + , + tooltip: 'Sign your document', + size: 'lg', + isRound: false, + onClick: () => setActiveButton('sign') + }, + { + id: 'automate', + name: 'Automate', + icon: , + tooltip: 'Automate workflows', + size: 'lg', + isRound: false, + onClick: () => setActiveButton('automate') + }, + { + id: 'files', + name: 'Files', + icon: , + tooltip: 'Manage files', + isRound: true, + size: 'lg', + onClick: () => setActiveButton('files') + }, + { + id: 'activity', + name: 'Activity', + icon: + + vital_signs + , + tooltip: 'View activity and analytics', + isRound: true, + size: 'lg', + onClick: () => setActiveButton('activity') + }, + { + id: 'config', + name: 'Config', + icon: , + tooltip: 'Configure settings', + size: 'lg', + onClick: () => { + setConfigModalOpen(true); + } + } + ]; + + const CIRCULAR_BORDER_RADIUS = '50%'; + const ROUND_BORDER_RADIUS = '8px'; + + const getBorderRadius = (config: ButtonConfig): string => { + return config.isRound ? CIRCULAR_BORDER_RADIUS : ROUND_BORDER_RADIUS; + }; + + const getButtonStyle = (config: ButtonConfig) => { + const isActive = activeButton === config.id; + + if (isActive) { + return { + backgroundColor: `var(--icon-${config.id}-bg)`, + color: `var(--icon-${config.id}-color)`, + border: 'none', + borderRadius: getBorderRadius(config), + }; + } + + // Inactive state - use consistent inactive colors + return { + backgroundColor: 'var(--icon-inactive-bg)', + color: 'var(--icon-inactive-color)', + border: 'none', + borderRadius: getBorderRadius(config), + }; + }; return (
- - {/* All Tools Button */} -
- - - - Tools -
+ {/* Fixed header outside scrollable area */} +
+ +
- {/* Reader Mode Button */} -
- - - - Read -
+ {/* Conditional divider when overflowing */} + {isOverflow && ( + + )} - {/* Spacer */} -
- - {/* Config Modal Button (for testing) */} -
- setConfigModalOpen(true)} - > - - - Config + {/* Scrollable content area */} +
{ + // Prevent the wheel event from bubbling up to parent containers + e.stopPropagation(); + }} + > +
+ {/* Top section with main buttons */} + + {buttonConfigs.slice(0, -1).map((config, index) => ( + + +
+ + + {config.icon} + + + + {config.name} + +
+
+ + {/* Add divider after Automate button (index 2) */} + {index === 2 && ( + + )} +
+ ))} +
+ + {/* Spacer to push Config button to bottom */} +
+ + {/* Config button at the bottom */} + +
+ { + setConfigModalOpen(true); + }} + style={{ + backgroundColor: 'var(--icon-inactive-bg)', + color: 'var(--icon-inactive-color)', + border: 'none', + borderRadius: '8px', + }} + > + + + + + + Config + +
+
- +
{ + if (isOverflow) { + // Do something + } + }, [isOverflow]); + + const scrollableRef = useRef(null); + const isOverflow = useIsOverflowing(scrollableRef); + + Fallback example (for browsers without ResizeObserver): + + return ( +
+ {Content that might overflow} +
+ ); +*/ + + +export const useIsOverflowing = (ref: React.RefObject, callback?: (isOverflow: boolean) => void) => { + // State to track overflow status + const [isOverflow, setIsOverflow] = React.useState(undefined); + + React.useLayoutEffect(() => { + const { current } = ref; + + // Function to check if element is overflowing + const trigger = () => { + if (!current) return; + + // Compare scroll height (total content height) vs client height (visible height) + const hasOverflow = current.scrollHeight > current.clientHeight; + setIsOverflow(hasOverflow); + + // Call optional callback with overflow state + if (callback) callback(hasOverflow); + }; + + if (current) { + // Use ResizeObserver for modern browsers (real-time detection) + if ('ResizeObserver' in window) { + const resizeObserver = new ResizeObserver(trigger); + resizeObserver.observe(current); + + // Cleanup function to disconnect observer + return () => { + resizeObserver.disconnect(); + }; + } + + // Fallback for browsers without ResizeObserver support + // Add a small delay to ensure the element is fully rendered + setTimeout(trigger, 0); + } + }, [callback, ref]); + + return isOverflow; +}; \ No newline at end of file diff --git a/frontend/src/index.css b/frontend/src/index.css index ec2585e8c..f7e5e0865 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -1,3 +1,9 @@ +@import 'material-symbols/rounded.css'; + +.material-symbols-rounded { + font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24; +} + body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', diff --git a/frontend/src/pages/HomePage.tsx b/frontend/src/pages/HomePage.tsx index 1ee0c99ed..d24c58b44 100644 --- a/frontend/src/pages/HomePage.tsx +++ b/frontend/src/pages/HomePage.tsx @@ -70,7 +70,7 @@ function HomePageContent() { }, [clearToolSelection]); const handleReaderToggle = useCallback(() => { - setReaderMode(!readerMode); + setReaderMode(true); }, [readerMode]); const handleViewChange = useCallback((view: string) => { @@ -104,7 +104,7 @@ function HomePageContent() { {/* Left: Tool Picker or Selected Tool Panel */}
{/* Top Controls */}