diff --git a/CHANGELOG.md b/CHANGELOG.md index 10c64acb48..9651d49946 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to - 🥅(frontend) intercept 401 error on GET threads #1754 - 🦺(frontend) check content type pdf on PdfBlock #1756 - ✈️(frontend) pause Posthog when offline #1755 +- 📱(frontend) toolbar to the bottom when mobile #1774 ### Fixed diff --git a/src/frontend/apps/impress/src/core/AppProvider.tsx b/src/frontend/apps/impress/src/core/AppProvider.tsx index b52f8fd223..ac5f12e7e3 100644 --- a/src/frontend/apps/impress/src/core/AppProvider.tsx +++ b/src/frontend/apps/impress/src/core/AppProvider.tsx @@ -52,14 +52,17 @@ export function AppProvider({ children }: { children: React.ReactNode }) { const { theme } = useCunninghamTheme(); const { replace } = useRouter(); - const initializeResizeListener = useResponsiveStore( - (state) => state.initializeResizeListener, - ); + const { initializeResizeListener, initializeInputDetection } = + useResponsiveStore(); useEffect(() => { return initializeResizeListener(); }, [initializeResizeListener]); + useEffect(() => { + return initializeInputDetection(); + }, [initializeInputDetection]); + /** * Update the global router replace function * This allows us to use the router replace function globally diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteToolBar/BlockNoteToolbar.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteToolBar/BlockNoteToolbar.tsx index b88e09c7a4..f5f9eee3f4 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteToolBar/BlockNoteToolbar.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteToolBar/BlockNoteToolbar.tsx @@ -1,15 +1,26 @@ +import { FormattingToolbarExtension } from '@blocknote/core/extensions'; import { + ExperimentalMobileFormattingToolbarController, FormattingToolbar, FormattingToolbarController, blockTypeSelectItems, getFormattingToolbarItems, + useBlockNoteEditor, useDictionary, + useExtensionState, } from '@blocknote/react'; import React, { useCallback, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; +import { Box } from '@/components'; import { useConfig } from '@/core/config/api'; +import { useResponsiveStore } from '@/stores'; +import { + DocsBlockSchema, + DocsInlineContentSchema, + DocsStyleSchema, +} from '../../types'; import { CommentToolbarButton } from '../comments/CommentToolbarButton'; import { getCalloutFormattingToolbarItems } from '../custom-blocks'; @@ -24,6 +35,7 @@ export const BlockNoteToolbar = () => { const [onConfirm, setOnConfirm] = useState<() => void | Promise>(); const { t } = useTranslation(); const { data: conf } = useConfig(); + const { isTablet, isInputTouch } = useResponsiveStore(); const toolbarItems = useMemo(() => { let toolbarItems = getFormattingToolbarItems([ @@ -84,7 +96,13 @@ export const BlockNoteToolbar = () => { return ( <> - + {isInputTouch && isTablet ? ( + + ) : ( + + )} {confirmOpen && ( setIsConfirmOpen(false)} @@ -94,3 +112,38 @@ export const BlockNoteToolbar = () => { ); }; + +const MobileFormattingToolbarController = ({ + formattingToolbar, +}: { + formattingToolbar: () => React.ReactNode; +}) => { + const editor = useBlockNoteEditor< + DocsBlockSchema, + DocsInlineContentSchema, + DocsStyleSchema + >(); + const show = useExtensionState(FormattingToolbarExtension, { + editor, + }); + + if (!show) { + return null; + } + + return ( + div { + left: 50%; + transform: translate(0px, 0px) scale(1) translateX(-50%)!important; + } + `} + > + + + ); +}; diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteToolBar/MarkdownButton.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteToolBar/MarkdownButton.tsx index 753e7b298c..87935246ae 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteToolBar/MarkdownButton.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteToolBar/MarkdownButton.tsx @@ -74,7 +74,9 @@ export function MarkdownButton() { }; const show = useMemo(() => { - return !!selectedBlocks.find((block) => block.content !== undefined); + return ( + selectedBlocks.filter((block) => block.content !== undefined).length !== 0 + ); }, [selectedBlocks]); if (!show || !editor.isEditable || !Components) { diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-blocks/PdfBlock.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-blocks/PdfBlock.tsx index 1130d2b58b..04b7543283 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-blocks/PdfBlock.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-blocks/PdfBlock.tsx @@ -59,11 +59,7 @@ interface PdfBlockComponentProps { >; } -const PdfBlockComponent = ({ - editor, - block, - contentRef, -}: PdfBlockComponentProps) => { +const PdfBlockComponent = ({ editor, block }: PdfBlockComponentProps) => { const pdfUrl = block.props.url; const { i18n, t } = useTranslation(); const lang = i18n.resolvedLanguage; @@ -114,27 +110,34 @@ const PdfBlockComponent = ({ void validatePDFContent(); }, [pdfUrl]); + if (isPDFContentLoading) { + return ; + } + + if (!isPDFContentLoading && isPDFContent !== null && !isPDFContent) { + return ( + editor.setTextCursorPosition(block)} + > + {t('Invalid or missing PDF file.')} + + ); + } + return ( - + <> - {isPDFContentLoading && } - {!isPDFContentLoading && isPDFContent !== null && !isPDFContent && ( - editor.setTextCursorPosition(block)} - > - {t('Invalid or missing PDF file.')} - - )} @@ -158,7 +161,7 @@ const PdfBlockComponent = ({ /> )} - + ); }; diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/styles.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/styles.tsx index 6c9c1645a1..fa94cdcda9 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/styles.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/styles.tsx @@ -180,6 +180,9 @@ export const cssEditor = css` & .bn-editor { padding-right: 36px; } + & .bn-toolbar { + max-width: 100vw; + } } @media screen and (width <= 560px) { diff --git a/src/frontend/apps/impress/src/layouts/MainLayout.tsx b/src/frontend/apps/impress/src/layouts/MainLayout.tsx index 39abde49a9..9a295bae02 100644 --- a/src/frontend/apps/impress/src/layouts/MainLayout.tsx +++ b/src/frontend/apps/impress/src/layouts/MainLayout.tsx @@ -120,7 +120,7 @@ const MainContent = ({ $css={css` overflow-y: auto; overflow-x: clip; - &:focus { + &:focus-visible { outline: 3px solid ${colorsTokens['brand-400']}; outline-offset: -3px; } diff --git a/src/frontend/apps/impress/src/stores/useResponsiveStore.tsx b/src/frontend/apps/impress/src/stores/useResponsiveStore.tsx index 7da3133ca0..cc9ea092c3 100644 --- a/src/frontend/apps/impress/src/stores/useResponsiveStore.tsx +++ b/src/frontend/apps/impress/src/stores/useResponsiveStore.tsx @@ -1,6 +1,7 @@ import { create } from 'zustand'; export type ScreenSize = 'small-mobile' | 'mobile' | 'tablet' | 'desktop'; +export type InputMethod = 'touch' | 'mouse' | 'unknown'; export interface UseResponsiveStore { isMobile: boolean; @@ -10,7 +11,11 @@ export interface UseResponsiveStore { screenWidth: number; setScreenSize: (size: ScreenSize) => void; isDesktop: boolean; + isTouchCapable: boolean; + isInputTouch: boolean; + inputMethod: InputMethod; initializeResizeListener: () => () => void; + initializeInputDetection: () => () => void; } const initialState = { @@ -20,6 +25,9 @@ const initialState = { isDesktop: false, screenSize: 'desktop' as ScreenSize, screenWidth: 0, + isTouchCapable: false, + isInputTouch: false, + inputMethod: 'unknown' as InputMethod, }; export const useResponsiveStore = create((set) => ({ @@ -29,6 +37,9 @@ export const useResponsiveStore = create((set) => ({ isTablet: initialState.isTablet, screenSize: initialState.screenSize, screenWidth: initialState.screenWidth, + isTouchCapable: initialState.isTouchCapable, + isInputTouch: initialState.isInputTouch, + inputMethod: initialState.inputMethod, setScreenSize: (size: ScreenSize) => set(() => ({ screenSize: size })), initializeResizeListener: () => { const resizeHandler = () => { @@ -84,4 +95,32 @@ export const useResponsiveStore = create((set) => ({ window.removeEventListener('resize', debouncedResizeHandler); }; }, + initializeInputDetection: () => { + // Detect if device has touch capability + const isTouchCapable = + 'ontouchstart' in window || + navigator.maxTouchPoints > 0 || + // @ts-ignore - for older browsers + navigator.msMaxTouchPoints > 0; + + set({ isTouchCapable }); + + // Track actual input method being used + const handleTouchStart = () => { + set({ inputMethod: 'touch', isInputTouch: true }); + }; + + const handleMouseMove = () => { + set({ inputMethod: 'mouse', isInputTouch: false }); + }; + + // Listen for first interaction to determine input method + window.addEventListener('touchstart', handleTouchStart, { once: false }); + window.addEventListener('mousemove', handleMouseMove, { once: false }); + + return () => { + window.removeEventListener('touchstart', handleTouchStart); + window.removeEventListener('mousemove', handleMouseMove); + }; + }, }));