mirror of
https://github.com/suitenumerique/docs.git
synced 2026-04-26 01:25:05 +02:00
Compare commits
3 Commits
fix/link-p
...
responsive
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
64ce2d449a | ||
|
|
2be5cda88e | ||
|
|
1211e04a45 |
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<void>>();
|
||||
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 (
|
||||
<>
|
||||
<FormattingToolbarController formattingToolbar={formattingToolbar} />
|
||||
{isInputTouch && isTablet ? (
|
||||
<MobileFormattingToolbarController
|
||||
formattingToolbar={formattingToolbar}
|
||||
/>
|
||||
) : (
|
||||
<FormattingToolbarController formattingToolbar={formattingToolbar} />
|
||||
)}
|
||||
{confirmOpen && (
|
||||
<ModalConfirmDownloadUnsafe
|
||||
onClose={() => 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 (
|
||||
<Box
|
||||
$position="absolute"
|
||||
$css={`
|
||||
& > div {
|
||||
left: 50%;
|
||||
transform: translate(0px, 0px) scale(1) translateX(-50%)!important;
|
||||
}
|
||||
`}
|
||||
>
|
||||
<ExperimentalMobileFormattingToolbarController
|
||||
formattingToolbar={formattingToolbar}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 <Loading />;
|
||||
}
|
||||
|
||||
if (!isPDFContentLoading && isPDFContent !== null && !isPDFContent) {
|
||||
return (
|
||||
<Box
|
||||
$align="center"
|
||||
$justify="center"
|
||||
$color="#666"
|
||||
$background="#f5f5f5"
|
||||
$border="1px solid #ddd"
|
||||
$height="300px"
|
||||
$css={css`
|
||||
text-align: center;
|
||||
`}
|
||||
$width="100%"
|
||||
contentEditable={false}
|
||||
onClick={() => editor.setTextCursorPosition(block)}
|
||||
>
|
||||
{t('Invalid or missing PDF file.')}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box ref={contentRef} className="bn-file-block-content-wrapper">
|
||||
<>
|
||||
<PDFBlockStyle />
|
||||
{isPDFContentLoading && <Loading />}
|
||||
{!isPDFContentLoading && isPDFContent !== null && !isPDFContent && (
|
||||
<Box
|
||||
$align="center"
|
||||
$justify="center"
|
||||
$color="#666"
|
||||
$background="#f5f5f5"
|
||||
$border="1px solid #ddd"
|
||||
$height="300px"
|
||||
$css={css`
|
||||
text-align: center;
|
||||
`}
|
||||
contentEditable={false}
|
||||
onClick={() => editor.setTextCursorPosition(block)}
|
||||
>
|
||||
{t('Invalid or missing PDF file.')}
|
||||
</Box>
|
||||
)}
|
||||
<ResizableFileBlockWrapper
|
||||
buttonIcon={
|
||||
<Icon iconName="upload" $size="24px" $css="line-height: normal;" />
|
||||
@@ -158,7 +161,7 @@ const PdfBlockComponent = ({
|
||||
/>
|
||||
)}
|
||||
</ResizableFileBlockWrapper>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -180,6 +180,9 @@ export const cssEditor = css`
|
||||
& .bn-editor {
|
||||
padding-right: 36px;
|
||||
}
|
||||
& .bn-toolbar {
|
||||
max-width: 100vw;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (width <= 560px) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<UseResponsiveStore>((set) => ({
|
||||
@@ -29,6 +37,9 @@ export const useResponsiveStore = create<UseResponsiveStore>((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<UseResponsiveStore>((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);
|
||||
};
|
||||
},
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user