Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 64ce2d449a | |||
| 2be5cda88e | |||
| 1211e04a45 |
@@ -17,6 +17,7 @@ and this project adheres to
|
|||||||
- 🥅(frontend) intercept 401 error on GET threads #1754
|
- 🥅(frontend) intercept 401 error on GET threads #1754
|
||||||
- 🦺(frontend) check content type pdf on PdfBlock #1756
|
- 🦺(frontend) check content type pdf on PdfBlock #1756
|
||||||
- ✈️(frontend) pause Posthog when offline #1755
|
- ✈️(frontend) pause Posthog when offline #1755
|
||||||
|
- 📱(frontend) toolbar to the bottom when mobile #1774
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
|||||||
@@ -52,14 +52,17 @@ export function AppProvider({ children }: { children: React.ReactNode }) {
|
|||||||
const { theme } = useCunninghamTheme();
|
const { theme } = useCunninghamTheme();
|
||||||
const { replace } = useRouter();
|
const { replace } = useRouter();
|
||||||
|
|
||||||
const initializeResizeListener = useResponsiveStore(
|
const { initializeResizeListener, initializeInputDetection } =
|
||||||
(state) => state.initializeResizeListener,
|
useResponsiveStore();
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return initializeResizeListener();
|
return initializeResizeListener();
|
||||||
}, [initializeResizeListener]);
|
}, [initializeResizeListener]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return initializeInputDetection();
|
||||||
|
}, [initializeInputDetection]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the global router replace function
|
* Update the global router replace function
|
||||||
* This allows us to use the router replace function globally
|
* This allows us to use the router replace function globally
|
||||||
|
|||||||
+54
-1
@@ -1,15 +1,26 @@
|
|||||||
|
import { FormattingToolbarExtension } from '@blocknote/core/extensions';
|
||||||
import {
|
import {
|
||||||
|
ExperimentalMobileFormattingToolbarController,
|
||||||
FormattingToolbar,
|
FormattingToolbar,
|
||||||
FormattingToolbarController,
|
FormattingToolbarController,
|
||||||
blockTypeSelectItems,
|
blockTypeSelectItems,
|
||||||
getFormattingToolbarItems,
|
getFormattingToolbarItems,
|
||||||
|
useBlockNoteEditor,
|
||||||
useDictionary,
|
useDictionary,
|
||||||
|
useExtensionState,
|
||||||
} from '@blocknote/react';
|
} from '@blocknote/react';
|
||||||
import React, { useCallback, useMemo, useState } from 'react';
|
import React, { useCallback, useMemo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { Box } from '@/components';
|
||||||
import { useConfig } from '@/core/config/api';
|
import { useConfig } from '@/core/config/api';
|
||||||
|
import { useResponsiveStore } from '@/stores';
|
||||||
|
|
||||||
|
import {
|
||||||
|
DocsBlockSchema,
|
||||||
|
DocsInlineContentSchema,
|
||||||
|
DocsStyleSchema,
|
||||||
|
} from '../../types';
|
||||||
import { CommentToolbarButton } from '../comments/CommentToolbarButton';
|
import { CommentToolbarButton } from '../comments/CommentToolbarButton';
|
||||||
import { getCalloutFormattingToolbarItems } from '../custom-blocks';
|
import { getCalloutFormattingToolbarItems } from '../custom-blocks';
|
||||||
|
|
||||||
@@ -24,6 +35,7 @@ export const BlockNoteToolbar = () => {
|
|||||||
const [onConfirm, setOnConfirm] = useState<() => void | Promise<void>>();
|
const [onConfirm, setOnConfirm] = useState<() => void | Promise<void>>();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { data: conf } = useConfig();
|
const { data: conf } = useConfig();
|
||||||
|
const { isTablet, isInputTouch } = useResponsiveStore();
|
||||||
|
|
||||||
const toolbarItems = useMemo(() => {
|
const toolbarItems = useMemo(() => {
|
||||||
let toolbarItems = getFormattingToolbarItems([
|
let toolbarItems = getFormattingToolbarItems([
|
||||||
@@ -84,7 +96,13 @@ export const BlockNoteToolbar = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<FormattingToolbarController formattingToolbar={formattingToolbar} />
|
{isInputTouch && isTablet ? (
|
||||||
|
<MobileFormattingToolbarController
|
||||||
|
formattingToolbar={formattingToolbar}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<FormattingToolbarController formattingToolbar={formattingToolbar} />
|
||||||
|
)}
|
||||||
{confirmOpen && (
|
{confirmOpen && (
|
||||||
<ModalConfirmDownloadUnsafe
|
<ModalConfirmDownloadUnsafe
|
||||||
onClose={() => setIsConfirmOpen(false)}
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
+3
-1
@@ -74,7 +74,9 @@ export function MarkdownButton() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const show = useMemo(() => {
|
const show = useMemo(() => {
|
||||||
return !!selectedBlocks.find((block) => block.content !== undefined);
|
return (
|
||||||
|
selectedBlocks.filter((block) => block.content !== undefined).length !== 0
|
||||||
|
);
|
||||||
}, [selectedBlocks]);
|
}, [selectedBlocks]);
|
||||||
|
|
||||||
if (!show || !editor.isEditable || !Components) {
|
if (!show || !editor.isEditable || !Components) {
|
||||||
|
|||||||
+28
-25
@@ -59,11 +59,7 @@ interface PdfBlockComponentProps {
|
|||||||
>;
|
>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PdfBlockComponent = ({
|
const PdfBlockComponent = ({ editor, block }: PdfBlockComponentProps) => {
|
||||||
editor,
|
|
||||||
block,
|
|
||||||
contentRef,
|
|
||||||
}: PdfBlockComponentProps) => {
|
|
||||||
const pdfUrl = block.props.url;
|
const pdfUrl = block.props.url;
|
||||||
const { i18n, t } = useTranslation();
|
const { i18n, t } = useTranslation();
|
||||||
const lang = i18n.resolvedLanguage;
|
const lang = i18n.resolvedLanguage;
|
||||||
@@ -114,27 +110,34 @@ const PdfBlockComponent = ({
|
|||||||
void validatePDFContent();
|
void validatePDFContent();
|
||||||
}, [pdfUrl]);
|
}, [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 (
|
return (
|
||||||
<Box ref={contentRef} className="bn-file-block-content-wrapper">
|
<>
|
||||||
<PDFBlockStyle />
|
<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
|
<ResizableFileBlockWrapper
|
||||||
buttonIcon={
|
buttonIcon={
|
||||||
<Icon iconName="upload" $size="24px" $css="line-height: normal;" />
|
<Icon iconName="upload" $size="24px" $css="line-height: normal;" />
|
||||||
@@ -158,7 +161,7 @@ const PdfBlockComponent = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</ResizableFileBlockWrapper>
|
</ResizableFileBlockWrapper>
|
||||||
</Box>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -180,6 +180,9 @@ export const cssEditor = css`
|
|||||||
& .bn-editor {
|
& .bn-editor {
|
||||||
padding-right: 36px;
|
padding-right: 36px;
|
||||||
}
|
}
|
||||||
|
& .bn-toolbar {
|
||||||
|
max-width: 100vw;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (width <= 560px) {
|
@media screen and (width <= 560px) {
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ const MainContent = ({
|
|||||||
$css={css`
|
$css={css`
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overflow-x: clip;
|
overflow-x: clip;
|
||||||
&:focus {
|
&:focus-visible {
|
||||||
outline: 3px solid ${colorsTokens['brand-400']};
|
outline: 3px solid ${colorsTokens['brand-400']};
|
||||||
outline-offset: -3px;
|
outline-offset: -3px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
|
|
||||||
export type ScreenSize = 'small-mobile' | 'mobile' | 'tablet' | 'desktop';
|
export type ScreenSize = 'small-mobile' | 'mobile' | 'tablet' | 'desktop';
|
||||||
|
export type InputMethod = 'touch' | 'mouse' | 'unknown';
|
||||||
|
|
||||||
export interface UseResponsiveStore {
|
export interface UseResponsiveStore {
|
||||||
isMobile: boolean;
|
isMobile: boolean;
|
||||||
@@ -10,7 +11,11 @@ export interface UseResponsiveStore {
|
|||||||
screenWidth: number;
|
screenWidth: number;
|
||||||
setScreenSize: (size: ScreenSize) => void;
|
setScreenSize: (size: ScreenSize) => void;
|
||||||
isDesktop: boolean;
|
isDesktop: boolean;
|
||||||
|
isTouchCapable: boolean;
|
||||||
|
isInputTouch: boolean;
|
||||||
|
inputMethod: InputMethod;
|
||||||
initializeResizeListener: () => () => void;
|
initializeResizeListener: () => () => void;
|
||||||
|
initializeInputDetection: () => () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
@@ -20,6 +25,9 @@ const initialState = {
|
|||||||
isDesktop: false,
|
isDesktop: false,
|
||||||
screenSize: 'desktop' as ScreenSize,
|
screenSize: 'desktop' as ScreenSize,
|
||||||
screenWidth: 0,
|
screenWidth: 0,
|
||||||
|
isTouchCapable: false,
|
||||||
|
isInputTouch: false,
|
||||||
|
inputMethod: 'unknown' as InputMethod,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useResponsiveStore = create<UseResponsiveStore>((set) => ({
|
export const useResponsiveStore = create<UseResponsiveStore>((set) => ({
|
||||||
@@ -29,6 +37,9 @@ export const useResponsiveStore = create<UseResponsiveStore>((set) => ({
|
|||||||
isTablet: initialState.isTablet,
|
isTablet: initialState.isTablet,
|
||||||
screenSize: initialState.screenSize,
|
screenSize: initialState.screenSize,
|
||||||
screenWidth: initialState.screenWidth,
|
screenWidth: initialState.screenWidth,
|
||||||
|
isTouchCapable: initialState.isTouchCapable,
|
||||||
|
isInputTouch: initialState.isInputTouch,
|
||||||
|
inputMethod: initialState.inputMethod,
|
||||||
setScreenSize: (size: ScreenSize) => set(() => ({ screenSize: size })),
|
setScreenSize: (size: ScreenSize) => set(() => ({ screenSize: size })),
|
||||||
initializeResizeListener: () => {
|
initializeResizeListener: () => {
|
||||||
const resizeHandler = () => {
|
const resizeHandler = () => {
|
||||||
@@ -84,4 +95,32 @@ export const useResponsiveStore = create<UseResponsiveStore>((set) => ({
|
|||||||
window.removeEventListener('resize', debouncedResizeHandler);
|
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