diff --git a/src/cloud/components/DocPage/SharedDocPage.tsx b/src/cloud/components/DocPage/SharedDocPage.tsx index af8e2ef4b6..e0d69fc421 100644 --- a/src/cloud/components/DocPage/SharedDocPage.tsx +++ b/src/cloud/components/DocPage/SharedDocPage.tsx @@ -7,9 +7,9 @@ import SyncStatus from '../Topbar/SyncStatus' import PresenceIcons from '../Topbar/PresenceIcons' import SharePageTopbar from '../SharePageTopBar' import { SerializedDoc } from '../../interfaces/db/doc' -import MarkdownView from '../MarkdownView' import Spinner from '../../../design/components/atoms/Spinner' import ColoredBlock from '../../../design/components/atoms/ColoredBlock' +import CustomizedMarkdownPreviewer from '../MarkdownView/CustomizedMarkdownPreviewer' interface SharedDocPageProps { doc: SerializedDoc @@ -88,7 +88,7 @@ const SharedDocPage = ({ doc, token }: SharedDocPageProps) => { <> {doc.title} - ( diff --git a/src/cloud/components/Editor/EditorThemeSelect.tsx b/src/cloud/components/Editor/EditorThemeSelect.tsx index e9cc36de08..9142bf53d0 100644 --- a/src/cloud/components/Editor/EditorThemeSelect.tsx +++ b/src/cloud/components/Editor/EditorThemeSelect.tsx @@ -58,7 +58,7 @@ const EditorThemeSelect = () => { [] ) - const selectIndentType = useCallback( + const selectEditorTheme = useCallback( (val: string) => { setSettings({ 'general.editorTheme': val as CodeMirrorEditorTheme, @@ -70,7 +70,7 @@ const EditorThemeSelect = () => { [setSettings] ) - const selectIndentSize = useCallback( + const selectCodeBlockTheme = useCallback( (val: string) => { setSettings({ 'general.codeBlockTheme': val as CodeMirrorEditorTheme, @@ -108,7 +108,7 @@ const EditorThemeSelect = () => { > { > - JSX.Element + updateContent?: ( + newContentOrUpdater: string | ((newValue: string) => string) + ) => void + shortcodeHandler?: ({ identifier, entityId }: any) => JSX.Element + headerLinks?: boolean + onRender?: () => void + className?: string + getEmbed?: ( + id: string + ) => Promise | EmbedDoc | undefined + scrollerRef?: React.RefObject + SelectionMenu?: React.ComponentType<{ selection: SelectionState['context'] }> + comments?: HighlightRange[] + commentClick?: (id: string[]) => void + codeFence?: boolean + previewStyle?: string +} + +const CustomizedMarkdownPreviewer = ({ + content, + updateContent, + shortcodeHandler, + headerLinks = true, + onRender, + className, + getEmbed, + scrollerRef, + SelectionMenu, + comments, + commentClick, + codeFence = true, +}: CustomizedMarkdownViewProps) => { + const { previewStyle } = usePreviewStyle() + const { settings } = useSettings() + + return ( + + ) +} + +export default CustomizedMarkdownPreviewer diff --git a/src/cloud/components/MarkdownView/index.tsx b/src/cloud/components/MarkdownView/index.tsx index 58ea7e0fe1..4f137d6995 100644 --- a/src/cloud/components/MarkdownView/index.tsx +++ b/src/cloud/components/MarkdownView/index.tsx @@ -12,8 +12,7 @@ import rehypeSanitize from 'rehype-sanitize' import rehypeCodeMirror from '../../../design/lib/codemirror/rehypeCodeMirror' import rehypeSlug from 'rehype-slug' import { useEffectOnce } from 'react-use' -import { defaultPreviewStyle } from './styles' -import { useSettings } from '../../lib/stores/settings' +import { CodeMirrorEditorTheme } from '../../lib/stores/settings' import { remarkCharts, Flowchart, @@ -47,6 +46,7 @@ import CodeFence from '../../../design/components/atoms/markdown/CodeFence' import { agentType, sendPostMessage } from '../../../mobile/lib/nativeMobile' import { TableOfContents } from './TableOfContents' import ExpandableImage from '../../../design/components/molecules/Image/ExpandableImage' +import { defaultPreviewStyle } from './styles' const remarkAdmonitionOptions = { tag: ':::', @@ -96,7 +96,7 @@ export interface SelectionContext { text: string } -interface SelectionState { +export interface SelectionState { context: SelectionContext position: Rect selection: Selection @@ -119,6 +119,9 @@ interface MarkdownViewProps { SelectionMenu?: React.ComponentType<{ selection: SelectionState['context'] }> comments?: HighlightRange[] commentClick?: (id: string[]) => void + codeFence?: boolean + previewStyle?: string + codeBlockTheme?: CodeMirrorEditorTheme } const MarkdownView = ({ @@ -133,10 +136,12 @@ const MarkdownView = ({ SelectionMenu, comments, commentClick, + codeFence = true, + previewStyle, + codeBlockTheme = 'default', }: MarkdownViewProps) => { const [state, setState] = useState({ type: 'loading' }) const modeLoadCallbackRef = useRef<() => any>() - const { settings } = useSettings() const checkboxIndexRef = useRef(0) const onRenderRef = useRef(onRender) const { push } = useRouter() @@ -246,10 +251,13 @@ const MarkdownView = ({ ) : null }, - pre: CodeFence, }, } + if (codeFence) { + rehypeReactConfig.components.pre = CodeFence + } + if (headerLinks) { rehypeReactConfig.components.h1 = linkableHeader('h1') rehypeReactConfig.components.h2 = linkableHeader('h2') @@ -281,7 +289,7 @@ const MarkdownView = ({ .use(rehypeKatex) .use(rehypeCodeMirror, { ignoreMissing: true, - theme: settings['general.codeBlockTheme'], + theme: codeBlockTheme, }) .use(rehypeMermaid) .use(rehypeHighlight, comments || []) @@ -291,13 +299,14 @@ const MarkdownView = ({ return parser }, [ - content, - settings, - updateContent, shortcodeHandler, + codeFence, headerLinks, getEmbed, + codeBlockTheme, comments, + updateContent, + content, commentClick, ]) @@ -394,6 +403,49 @@ const MarkdownView = ({ } }, [selectionInfo]) + const StyledMarkdownPreview = useMemo(() => { + return styled.div` + position: relative; + ${defaultPreviewStyle}; + ${previewStyle}; + + padding: 0 ${({ theme }) => theme.sizes.spaces.md}px + ${({ theme }) => theme.sizes.spaces.xl}px; + + .block__gutter { + position: absolute; + left: 100%; + top: 0; + max-width: 40px; + } + + .with__gutter { + position: relative; + } + + .comment__count { + height: 20px; + display: flex; + align-items: flex-start; + color: ${({ theme }) => theme.colors.icon.default}; + font-size: ${({ theme }) => theme.sizes.fonts.md}px; + + &:hover { + cursor: pointer; + color: ${({ theme }) => theme.colors.text.primary}; + } + + svg { + margin-right: ${({ theme }) => theme.sizes.spaces.xsm}px; + } + } + + .comment__count__number { + line-height: 1; + } + ` + }, [previewStyle]) + return ( theme.sizes.spaces.md}px ${({ theme }) => - theme.sizes.spaces.xl}px; - - .block__gutter { - position: absolute; - left: 100%; - top: 0; - max-width: 40px; - } - - .with__gutter { - position: relative; - } - - .comment__count { - height: 20px; - display: flex; - align-items: flex-start; - color: ${({ theme }) => theme.colors.icon.default}; - font-size: ${({ theme }) => theme.sizes.fonts.md}px; - - &:hover { - cursor: pointer; - color: ${({ theme }) => theme.colors.text.primary} - } - - svg { - margin-right: ${({ theme }) => theme.sizes.spaces.xsm}px; - } - } - - .comment__count__number { - line-height: 1; - } -` - const StyledTooltipContent = styled.div` background-color: ${({ theme }) => theme.colors.background.secondary}; border: 1px solid ${({ theme }) => theme.colors.border.second}; diff --git a/src/cloud/components/MarkdownView/styles.ts b/src/cloud/components/MarkdownView/styles.ts index d7391de552..46929cde4a 100644 --- a/src/cloud/components/MarkdownView/styles.ts +++ b/src/cloud/components/MarkdownView/styles.ts @@ -110,584 +110,4 @@ export const defaultPreviewStyle = ({ theme }: { theme: BaseTheme }) => ` padding: 0 ${theme.sizes.spaces.sm}px; } } - --ms-text-size-adjust: 100%; --webkit-text-size-adjust: 100%; -line-height: 1.6; -font-family: Lato, -apple-system, BlinkMacSystemFont, Helvetica, Arial, sans-serif; -font-size: 15px; -line-height: 1.6; -word-wrap: break-word; - -.dark, .solarizedDark { - color: #FFF; -} - -details { - display: block; -} - -summary { - display: list-item; -} - -a { - background-color: transparent; -} - -a:active, -a:hover { - outline-width: 0; -} - -strong { - font-weight: inherit; - font-weight: bolder; -} - -h1 { - font-size: 2em; - margin: 0.67em 0; -} - -img { - border-style: none; -} - -code, -kbd, -pre { - font-family: monospace, monospace; - font-size: 1em; -} - -hr { - box-sizing: content-box; - height: 0; - overflow: visible; -} - -input { - font: inherit; - margin: 0; -} - -input { - overflow: visible; -} - -[type='checkbox'] { - box-sizing: border-box; - padding: 0; -} - -* { - box-sizing: border-box; -} - -input { - font-family: inherit; - font-size: inherit; - line-height: inherit; -} - -a { - color: #0366d6; - text-decoration: none; -} - -a:hover { - text-decoration: underline; -} - -strong { - font-weight: 600; -} - -hr { - background: transparent; - border: 0; - border-bottom: 1px solid #dfe2e5; - height: 0; - margin: 15px 0; - overflow: hidden; -} - -hr:before { - content: ''; - display: table; -} - -hr:after { - clear: both; - content: ''; - display: table; -} - -table { - border-collapse: collapse; - border-spacing: 0; -} - -td, -th { - padding: 0; -} - -details summary { - cursor: pointer; -} - -h1, -h2, -h3, -h4, -h5, -h6 { - margin-bottom: 0; - margin-top: 0; -} - -h1 { - font-size: 32px; -} - -h1, -h2 { - font-weight: 500; -} - -h2 { - font-size: 24px; -} - -h3 { - font-size: 20px; -} - -h3, -h4 { - font-weight: 500; -} - -h4 { - font-size: 16px; -} - -h5 { - font-size: 14px; -} - -h5, -h6 { - font-weight: 600; -} - -h6 { - font-size: 12px; -} - -p { - margin-bottom: 10px; - margin-top: 0; -} - -blockquote { - margin: 0; -} - -ol, -ul { - margin-bottom: 0; - margin-top: 0; - padding-left: 0; -} - -ol ol, -ul ol { - list-style-type: lower-roman; -} - -ol ol ol, -ol ul ol, -ul ol ol, -ul ul ol { - list-style-type: lower-alpha; -} - -dd { - margin-left: 0; -} - -code, -pre { - font-family: SFMono-Regular, Consolas, Liberation Mono, Menlo, Courier, - monospace; - font-size: 12px; -} - -pre.CodeMirror, -code.CodeMirror { - height: auto; -} - -pre { - margin-bottom: 0; - margin-top: 0; -} - -input::-webkit-inner-spin-button, -input::-webkit-outer-spin-button { - -webkit-appearance: none; - appearance: none; - margin: 0; -} - -&:before { - content: ''; - display: table; -} - -&:after { - clear: both; - content: ''; - display: table; -} - -> :first-child { - margin-top: 0 !important; -} - -> :last-child { - margin-bottom: 0 !important; -} - -a:not([href]) { - color: inherit; - text-decoration: none; -} - -blockquote, -dl, -ol, -p, -pre, -table, -ul { - margin-bottom: 16px; - margin-top: 0; -} - -hr { - background-color: #e1e4e8; - border: 0; - height: 1px; - margin: 16px 0; - padding: 0; -} - -blockquote { - border-left: 0.25em solid #e1e4e8; - color: #999999; - padding: 0 1em; -} - -blockquote > :first-child { - margin-top: 0; -} - -blockquote > :last-child { - margin-bottom: 0; -} - -kbd { - background-color: #fafbfc; - border: 1px solid #c6cbd1; - border-bottom-color: #959da5; - border-radius: 3px; - box-shadow: inset 0 -1px 0 #959da5; - color: #444d56; - display: inline-block; - font-size: 11px; - line-height: 10px; - padding: 3px 5px; - vertical-align: middle; -} - -h1, -h2, -h3, -h4, -h5, -h6 { - line-height: 1.25; - margin: 8px 0; -} - -h1 { - margin-top: 32px; - font-weight: 600; - font-size: 1.953em; - margin-bottom: 10px; -} - -h2 { - font-weight: 600; - font-size: 1.5em; - margin-top: 16px; - margin-bottom: 8px; -} - -h3 { - font-weight: 600; - font-size: 1.25em; -} - -h4 { - font-weight: 400; - font-size: 1em; -} - -h5 { - font-weight: 400; - font-size: 0.875em; -} - -h6 { - font-weight: 400; - color: #6a737d; - font-size: 0.85em; -} - -ol, -ul { - padding-left: 2em; -} -ol ol, -ol ul, -ul ol, -ul ul { - margin-bottom: 0; - margin-top: 0; -} - -li { - word-wrap: break-all; -} - -li > p { - margin-top: 16px; - - &:first-child { - margin-top: 0; - } - &:last-child { - margin-bottom: 0; - } -} - -li + li { - margin-top: 0.25em; -} - -dl { - padding: 0; -} - -dl dt { - font-size: 1em; - font-style: italic; - font-weight: 600; - margin-top: 16px; - padding: 0; -} - -dl dd { - margin-bottom: 16px; - padding: 0 16px; -} - -table { - display: block; - overflow: auto; - width: 100%; -} - -table th { - font-weight: 600; -} - -table td, -table th { - border: 1px solid #dfe2e5; - padding: 6px 13px; -} - -table tr { - border-top: 1px solid #c6cbd1; -} - -img { - display: block; - max-width: 100%; - height: auto; - margin: 15px 0; - box-sizing: content-box; -} - -img[align='right'] { - padding-left: 20px; -} - -img[align='left'] { - padding-right: 20px; -} - -code { - background-color: rgba(27, 31, 35, 0.05); - border-radius: 3px; - font-size: 85%; - margin: 0; - padding: 0.2em 0.4em; -} - -p code, -li code -{ - padding: 2px; - border-width: 1px; - border-style: solid; - border-radius: 5px; -} - -pre { - word-wrap: normal; -} - -pre > code { - background: transparent; - border: 0; - font-size: 100%; - margin: 0; - padding: 0; - white-space: pre; - word-break: normal; -} - -.highlight { - margin-bottom: 16px; -} - -.highlight pre { - margin-bottom: 0; - word-break: normal; -} - -.highlight pre, -pre { - background-color: #f6f8fa; - border-radius: 3px; - font-size: 85%; - line-height: 1.45; - overflow: auto; - padding: 16px; -} - -pre code { - background-color: transparent; - border: 0; - display: inline; - line-height: inherit; - margin: 0; - max-width: auto; - overflow: visible; - padding: 0; - word-wrap: normal; -} - -kbd { - background-color: #fafbfc; - border: 1px solid #d1d5da; - border-bottom-color: #c6cbd1; - border-radius: 3px; - box-shadow: inset 0 -1px 0 #c6cbd1; - color: #444d56; - display: inline-block; - font: 11px SFMono-Regular, Consolas, Liberation Mono, Menlo, Courier, - monospace; - line-height: 10px; - padding: 3px 5px; - vertical-align: middle; -} - -:checked + .radio-label { - border-color: #0366d6; - position: relative; - z-index: 1; -} - -.task-list-item { - position: relative; - list-style-type: none; - height: fit-content; - min-height: 25px; -} - -.task-list-item + .task-list-item { - margin-top: 3px; -} - -.task-list-item input { - margin: 0 0.2em 0.25em -1.6em; - vertical-align: middle; -} - -.task-list-item input[type=checkbox] { - position: absolute; - top: 6px; -} - -hr { - border-bottom-color: #eee; -} - -.dark p code, -.dark li code -{ - background-color: rgba(255,255,255,0.12); - border-color: rgba(255,255,255,0.3); - color: #5580DC; -} - -.auto p code, -.auto li code -{ - background-color: #F9F9F9; - border-color: rgba(0,0,0,0.26); - color: #5580DC; -} - -.light p code, -.light li code -{ - background-color: #F9F9F9; - border-color: rgba(0,0,0,0.26); - color: #5580DC; -} - -.sepia p code, -.sepia li code -{ - background-color: #efe8d6; - border-color: rgba(0,0,0,0.26); - color: #F77942; -} - -.solarizedDark p code, -.solarizedDark li code -{ - background-color: rgba(255,255,255,0.12); - border-color: rgba(255,255,255,0.3); - color: #36abe3; -} ` diff --git a/src/cloud/components/Modal/contents/TemplatesModal/index.tsx b/src/cloud/components/Modal/contents/TemplatesModal/index.tsx index 0d70030e8b..fdb778426e 100644 --- a/src/cloud/components/Modal/contents/TemplatesModal/index.tsx +++ b/src/cloud/components/Modal/contents/TemplatesModal/index.tsx @@ -32,7 +32,6 @@ import EmojiIcon from '../../../EmojiIcon' import { useSettings } from '../../../../lib/stores/settings' import cc from 'classcat' import CodeMirrorEditor from '../../../../lib/editor/components/CodeMirrorEditor' -import MarkdownView from '../../../MarkdownView' import { useToast } from '../../../../../design/lib/stores/toast' import { useModal } from '../../../../../design/lib/stores/modal' import Switch from '../../../../../design/components/atoms/Switch' @@ -51,6 +50,7 @@ import UpDownList from '../../../../../design/components/atoms/UpDownList' import DoublePane from '../../../../../design/components/atoms/DoublePane' import plur from 'plur' import EditableInput from '../../../../../design/components/atoms/EditableInput' +import CustomizedMarkdownPreviewer from '../../../MarkdownView/CustomizedMarkdownPreviewer' interface TemplatesModalProps { callback?: (template: SerializedTemplate) => void @@ -421,7 +421,7 @@ const TemplatesModal = ({ callback }: TemplatesModalProps) => {
- diff --git a/src/cloud/components/Router.tsx b/src/cloud/components/Router.tsx index f3e84ac88e..65f2f33f41 100644 --- a/src/cloud/components/Router.tsx +++ b/src/cloud/components/Router.tsx @@ -63,8 +63,10 @@ import { TeamPreferencesProvider } from '../lib/stores/teamPreferences' import Application from './Application' import { BaseTheme } from '../../design/lib/styled/types' import { BlocksProvider } from '../lib/stores/blocks' +import { PreviewStyleProvider } from '../../lib/preview' const CombinedProvider = combineProviders( + PreviewStyleProvider, TeamStorageProvider, SidebarCollapseProvider, OnboardingProvider, diff --git a/src/cloud/components/settings/MarkdownTabForm.tsx b/src/cloud/components/settings/MarkdownTabForm.tsx new file mode 100644 index 0000000000..09995e31a0 --- /dev/null +++ b/src/cloud/components/settings/MarkdownTabForm.tsx @@ -0,0 +1,276 @@ +import React, { useState, useCallback, useMemo, useRef } from 'react' +import styled from '../../../design/lib/styled' +import { border, borderRight } from '../../../design/lib/styled/styleFunctions' +import { useTranslation } from 'react-i18next' +import { + CodeMirrorEditorTheme, + codeMirrorEditorThemes, + useSettings, +} from '../../lib/stores/settings' +import { + defaultCustomizablePreviewStyle, + usePreviewStyle, +} from '../../../lib/preview' +import Form from '../../../design/components/molecules/Form' +import CodeMirrorEditor from '../../lib/editor/components/CodeMirrorEditor' +import { SimpleFormSelect } from '../../../design/components/molecules/Form/atoms/FormSelect' +import CodeMirror from 'codemirror' +import { lngKeys } from '../../lib/i18n/types' +import { osName } from '../../../design/lib/platform' +import CustomizedMarkdownPreviewer from '../MarkdownView/CustomizedMarkdownPreviewer' +import { trackEvent } from '../../api/track' +import { MixpanelActionTrackTypes } from '../../interfaces/analytics/mixpanel' +import { useDebounce } from 'react-use' + +const EditorContainer = styled.div` + ${border} +` + +const defaultPreviewContent = `# hello-world.js + +\`\`\`js +function say() { + console.log('Hello, World!') +} +\`\`\` +` +const PreviewContainer = styled.div` + display: flex; + flex-direction: row; + ${border} + .panel { + width: 50%; + + &:first-child { + ${borderRight} + } + + z-index: 0; + } + + .CodeMirrorWrapper, + .CodeMirror-wrap { + height: 100%; + min-height: 360px; + max-height: 360px; + } +` + +const PREVIEW_STYLE_INPUT_DEBOUNCE_MS = 1000 + +const MarkdownTabForm = () => { + const { settings, setSettings } = useSettings() + const { previewStyle, setPreviewStyle } = usePreviewStyle() + const [newPreviewStyle, setNewPreviewStyle] = useState(previewStyle) + const [previewContent, setPreviewContent] = useState(defaultPreviewContent) + const initialPreviewStyle = useRef(previewStyle) + const editorRef = useRef(null) + const { t } = useTranslation() + + useDebounce( + () => { + if (editorRef.current == null) { + return + } + + const newPreviewStyleFromEditor = editorRef.current.getValue() + if (previewStyle !== newPreviewStyleFromEditor) { + setPreviewStyle(newPreviewStyleFromEditor) + } + }, + PREVIEW_STYLE_INPUT_DEBOUNCE_MS, + [editorRef, newPreviewStyle] + ) + + const resetNewPreviewStyle = useCallback(() => { + if (editorRef.current != null) { + editorRef.current.setValue(defaultCustomizablePreviewStyle) + } + }, []) + + const selectCodeBlockTheme = useCallback( + (codeBlockTheme: string) => { + setSettings({ + 'general.codeBlockTheme': codeBlockTheme as CodeMirrorEditorTheme, + }) + + trackEvent(MixpanelActionTrackTypes.ThemeChangeCodeblock, { + theme: codeBlockTheme, + }) + }, + [setSettings] + ) + + const bindCodeMirrorEditorPreviewContent = useCallback( + (editor: CodeMirror.Editor) => { + editor.on('change', (instance) => { + setPreviewContent(instance.getValue()) + }) + }, + [] + ) + + const bindCodeMirrorEditorPreviewStyle = useCallback( + (editor: CodeMirror.Editor) => { + editorRef.current = editor + editor.setValue(initialPreviewStyle.current) + editor.on('change', (instance) => { + setNewPreviewStyle(instance.getValue()) + }) + }, + [] + ) + + const editorConfigPreview: CodeMirror.EditorConfiguration = useMemo(() => { + const editorTheme = settings['general.editorTheme'] + const theme = + editorTheme == null || editorTheme === 'default' + ? settings['general.theme'] === 'light' + ? 'default' + : 'material-darker' + : editorTheme === 'solarized-dark' + ? 'solarized dark' + : editorTheme + const editorIndentType = settings['general.editorIndentType'] + const editorIndentSize = settings['general.editorIndentSize'] + + return { + mode: 'css', + lineNumbers: true, + lineWrapping: true, + theme, + indentWithTabs: editorIndentType === 'tab', + indentUnit: editorIndentSize, + tabSize: editorIndentSize, + inputStyle: osName === 'android' ? 'textarea' : 'contenteditable', + extraKeys: { + Enter: 'newlineAndIndentContinueMarkdownList', + Tab: 'indentMore', + }, + scrollPastEnd: true, + } + }, [settings]) + + const editorConfigMarkdownPreview: CodeMirror.EditorConfiguration = useMemo(() => { + const editorTheme = settings['general.editorTheme'] + const theme = + editorTheme == null || editorTheme === 'default' + ? settings['general.theme'] === 'light' + ? 'default' + : 'material-darker' + : editorTheme === 'solarized-dark' + ? 'solarized dark' + : editorTheme + const editorIndentType = settings['general.editorIndentType'] + const editorIndentSize = settings['general.editorIndentSize'] + + return { + value: defaultPreviewContent, + mode: 'markdown', + lineNumbers: true, + lineWrapping: true, + theme, + indentWithTabs: editorIndentType === 'tab', + indentUnit: editorIndentSize, + tabSize: editorIndentSize, + inputStyle: osName === 'android' ? 'textarea' : 'contenteditable', + extraKeys: { + Enter: 'newlineAndIndentContinueMarkdownList', + Tab: 'indentMore', + }, + scrollPastEnd: true, + } + }, [settings]) + + return ( +
resetNewPreviewStyle(), + }, + }, + ], + }, + { + items: [ + { + type: 'node', + element: ( + + + + ), + }, + ], + }, + { + title: t(lngKeys.SettingsMarkdownPreviewCodeBlockTheme), + items: [ + { + type: 'node', + element: ( + + ), + }, + ], + }, + { + title: t(lngKeys.SettingsMarkdownPreviewShowcase), + items: [ + { + type: 'node', + element: ( + +
+ +
+ +
+ +
+
+
+ ), + }, + ], + }, + ]} + /> + ) +} + +const MarkdownPreviewerPanel = styled.div` + overflow: auto; + text-overflow: ellipsis; + white-space: nowrap; + + max-width: 360px; + max-height: 360px; + min-height: 360px; + height: 100%; +` + +export default MarkdownTabForm diff --git a/src/cloud/components/settings/SettingsComponent.tsx b/src/cloud/components/settings/SettingsComponent.tsx index b9aff96980..db828ed543 100644 --- a/src/cloud/components/settings/SettingsComponent.tsx +++ b/src/cloud/components/settings/SettingsComponent.tsx @@ -47,6 +47,7 @@ import AttachmentsTab from './AttachmentsTab' import ImportTab from './ImportTab' import TeamSubLimit from './TeamSubLimit' import { ExternalLink } from '../../../design/components/atoms/Link' + const SettingsComponent = () => { const { t } = useTranslation() const { diff --git a/src/cloud/components/settings/UserPreferencesForm.tsx b/src/cloud/components/settings/UserPreferencesForm.tsx index e5e3441e25..ca43697176 100644 --- a/src/cloud/components/settings/UserPreferencesForm.tsx +++ b/src/cloud/components/settings/UserPreferencesForm.tsx @@ -20,6 +20,7 @@ import FormSelect, { import FormRow from '../../../design/components/molecules/Form/templates/FormRow' import { lngKeys } from '../../lib/i18n/types' import { useDebounce } from 'react-use' +import MarkdownTabForm from './MarkdownTabForm' const UserPreferencesForm = () => { const { settings, setSettings } = useSettings() @@ -92,19 +93,6 @@ const UserPreferencesForm = () => { [setSettings] ) - const selectCodeBlockTheme = useCallback( - (value: string) => { - setSettings({ - 'general.codeBlockTheme': value as CodeMirrorEditorTheme, - }) - - trackEvent(MixpanelActionTrackTypes.ThemeChangeCodeblock, { - theme: value, - }) - }, - [setSettings] - ) - const selectEditorKeyMap = useCallback( (value: string) => { setSettings({ @@ -172,6 +160,7 @@ const UserPreferencesForm = () => { return ( { }, ], }, - { - title: t(lngKeys.SettingsCodeBlockTheme), - items: [ - { - type: 'select--string', - props: { - options: codeMirrorEditorThemes, - value: settings['general.codeBlockTheme'], - onChange: selectCodeBlockTheme, - }, - }, - ], - }, { title: t(lngKeys.SettingsEditorKeyMap), items: [ @@ -406,6 +382,7 @@ const UserPreferencesForm = () => { ]} > { ], }} /> + , + }, + ], + }} + /> ) } diff --git a/src/cloud/lib/editor/CodeMirror.ts b/src/cloud/lib/editor/CodeMirror.ts index ecea622846..f35666aca1 100644 --- a/src/cloud/lib/editor/CodeMirror.ts +++ b/src/cloud/lib/editor/CodeMirror.ts @@ -2,6 +2,7 @@ import CodeMirror from 'codemirror' import 'codemirror/addon/runmode/runmode' import 'codemirror/mode/markdown/markdown' import 'codemirror/mode/javascript/javascript' +import 'codemirror/mode/css/css' import 'codemirror/lib/codemirror.css' import 'codemirror/addon/edit/continuelist' import 'codemirror/keymap/vim' diff --git a/src/cloud/lib/i18n/enUS.ts b/src/cloud/lib/i18n/enUS.ts index 84b1b4f03d..3195b1eecb 100644 --- a/src/cloud/lib/i18n/enUS.ts +++ b/src/cloud/lib/i18n/enUS.ts @@ -31,6 +31,11 @@ const enTranslation: TranslationSource = { [lngKeys.SettingsTeamInfo]: 'Settings', [lngKeys.SettingsTitle]: 'Settings', [lngKeys.SettingsPersonalInfo]: 'Settings', + [lngKeys.SettingsMarkdownPreview]: 'Markdown', + [lngKeys.SettingsMarkdownPreviewShowcase]: 'Markdown Style Preview', + [lngKeys.SettingsMarkdownPreviewCodeBlockTheme]: 'Code Block Theme', + [lngKeys.SettingsMarkdownPreviewStyleTitle]: 'Preview Style', + [lngKeys.SettingsMarkdownPreviewStyleResetLabel]: 'Use Default Style', [lngKeys.SettingsPreferences]: 'Preferences', [lngKeys.SettingsTeamUpgrade]: 'Upgrade', [lngKeys.SettingsTeamSubscription]: 'Billing', @@ -61,6 +66,8 @@ const enTranslation: TranslationSource = { [lngKeys.SettingsIndentSize]: 'Editor Indent Size', [lngKeys.SettingsUserForum]: 'User Forum (New!)', [lngKeys.ManagePreferences]: 'Manage your preferences.', + [lngKeys.ManagePreferencesMarkdownPreview]: + 'Manage your Markdown preview preferences.', [lngKeys.ManageProfile]: 'Manage your Boost Note profile.', [lngKeys.ManageSpaceSettings]: "Manage your space's settings.", [lngKeys.ManageTeamMembers]: 'Manage who has access to this space.', diff --git a/src/cloud/lib/i18n/fr.ts b/src/cloud/lib/i18n/fr.ts index 6c88f6e0ca..59c9940b78 100644 --- a/src/cloud/lib/i18n/fr.ts +++ b/src/cloud/lib/i18n/fr.ts @@ -31,6 +31,12 @@ const frTranslation: TranslationSource = { [lngKeys.SettingsTeamInfo]: 'Paramètres', [lngKeys.SettingsTitle]: 'Paramètres', [lngKeys.SettingsPersonalInfo]: 'Paramètres', + [lngKeys.SettingsMarkdownPreview]: 'Markdown', + [lngKeys.SettingsMarkdownPreviewShowcase]: 'Aperçu du style Markdown', + [lngKeys.SettingsMarkdownPreviewCodeBlockTheme]: 'Thème du bloc de code', + [lngKeys.SettingsMarkdownPreviewStyleTitle]: 'Aperçu du style', + [lngKeys.SettingsMarkdownPreviewStyleResetLabel]: + 'Utiliser le style par défaut', [lngKeys.SettingsPreferences]: 'Préférences', [lngKeys.SettingsTeamUpgrade]: 'Mise à niveau', [lngKeys.SettingsTeamSubscription]: 'Facturation', @@ -63,6 +69,8 @@ const frTranslation: TranslationSource = { [lngKeys.SettingsIndentSize]: "Taille de l'indentation pour l'éditeur", [lngKeys.SettingsUserForum]: "Forum d'utilisateurs (nouveau!)", [lngKeys.ManagePreferences]: 'Gérez vos préférences.', + [lngKeys.ManagePreferencesMarkdownPreview]: + 'Gérez vos préférences de prévisualisation Markdown.', [lngKeys.ManageProfile]: 'Gérez votre profil.', [lngKeys.ManageSpaceSettings]: 'Gérez les paramètres de votre espace.', [lngKeys.ManageTeamMembers]: 'Gérez qui peut accéder à cet espace.', diff --git a/src/cloud/lib/i18n/ja.ts b/src/cloud/lib/i18n/ja.ts index 1c2f1b97c0..715ae5f5e9 100644 --- a/src/cloud/lib/i18n/ja.ts +++ b/src/cloud/lib/i18n/ja.ts @@ -31,6 +31,12 @@ const jpTranslation: TranslationSource = { [lngKeys.SettingsTeamInfo]: '設定', [lngKeys.SettingsTitle]: '設定', [lngKeys.SettingsPersonalInfo]: '設定', + [lngKeys.SettingsMarkdownPreview]: 'マークダウン', + [lngKeys.SettingsMarkdownPreviewShowcase]: 'マークダウンスタイルのプレビュー', + [lngKeys.SettingsMarkdownPreviewCodeBlockTheme]: 'コードブロックのテーマ', + [lngKeys.SettingsMarkdownPreviewStyleTitle]: 'Pプレビュースタイル', + [lngKeys.SettingsMarkdownPreviewStyleResetLabel]: + 'デフォルトのスタイルを使用', [lngKeys.SettingsPreferences]: 'カスタマイズ', [lngKeys.SettingsTeamUpgrade]: 'アップグレード', [lngKeys.SettingsTeamSubscription]: '請求', @@ -61,6 +67,8 @@ const jpTranslation: TranslationSource = { [lngKeys.SettingsIndentSize]: 'エディタインデントのサイズ', [lngKeys.SettingsUserForum]: 'ユーザーフォーラム(New!!)', [lngKeys.ManagePreferences]: 'あなた好みにカスタマイズしましょう。', + [lngKeys.ManagePreferencesMarkdownPreview]: + 'Markdownプレビュー設定を管理します。', [lngKeys.ManageProfile]: 'Boost Noteでのプロフィールを編集しましょう。', [lngKeys.ManageSpaceSettings]: 'Space情報を編集しましょう。', [lngKeys.ManageTeamMembers]: 'このスペースへのアクセス権限を管理しましょう。', diff --git a/src/cloud/lib/i18n/types.ts b/src/cloud/lib/i18n/types.ts index 610da407ec..7ff00bf68d 100644 --- a/src/cloud/lib/i18n/types.ts +++ b/src/cloud/lib/i18n/types.ts @@ -107,6 +107,11 @@ export enum lngKeys { SettingsNotifications = 'settings.notifications', SettingsTitle = 'settings.title', SettingsPersonalInfo = 'settings.personalInfo', + SettingsMarkdownPreview = 'settings.markdownPreview', + SettingsMarkdownPreviewShowcase = 'settings.markdownPreviewShowcase', + SettingsMarkdownPreviewCodeBlockTheme = 'settings.markdownPreviewCodeBlockTheme', + SettingsMarkdownPreviewStyleTitle = 'settings.markdownPreviewStyleTitle', + SettingsMarkdownPreviewStyleResetLabel = 'settings.markdownPreviewStyleResetLabel', SettingsPreferences = 'settings.preferences', SettingsTeamInfo = 'settings.teamInfo', SettingsTeamUpgrade = 'settings.teamUpgrade', @@ -134,6 +139,7 @@ export enum lngKeys { SettingsSpaceDelete = 'settings.space.delete', SettingsSpaceDeleteWarning = 'settings.space.delete.warning', ManagePreferences = 'manage.preferences', + ManagePreferencesMarkdownPreview = 'manage.preferences.markdownPreview', ManageProfile = 'manage.profile', ManageSpaceSettings = 'manage.space.settings', ManageTeamMembers = 'manage.team.members', diff --git a/src/cloud/lib/i18n/zhCN.ts b/src/cloud/lib/i18n/zhCN.ts index 0e76558579..bdcf3a7a6c 100644 --- a/src/cloud/lib/i18n/zhCN.ts +++ b/src/cloud/lib/i18n/zhCN.ts @@ -32,6 +32,11 @@ const zhTranslation: TranslationSource = { [lngKeys.SettingsTitle]: '设置', [lngKeys.SettingsPersonalInfo]: '设置', [lngKeys.SettingsPreferences]: '首选项', + [lngKeys.SettingsMarkdownPreview]: '降价', + [lngKeys.SettingsMarkdownPreviewShowcase]: 'Markdown 样式预览', + [lngKeys.SettingsMarkdownPreviewCodeBlockTheme]: '代码块主题', + [lngKeys.SettingsMarkdownPreviewStyleTitle]: '预览样式', + [lngKeys.SettingsMarkdownPreviewStyleResetLabel]: '使用默认样式', [lngKeys.SettingsTeamUpgrade]: '更新', [lngKeys.SettingsTeamSubscription]: '付费', [lngKeys.SettingsIntegrations]: '集成', @@ -61,6 +66,7 @@ const zhTranslation: TranslationSource = { [lngKeys.SettingsIndentSize]: '编辑器缩进大小', [lngKeys.SettingsUserForum]: '用户论坛(新!)', [lngKeys.ManagePreferences]: '管理您的首选项。', + [lngKeys.ManagePreferencesMarkdownPreview]: '管理您的 Markdown 预览首选项。', [lngKeys.ManageProfile]: '管理你的档案。', [lngKeys.ManageSpaceSettings]: '管理共享空间的设置。', [lngKeys.ManageTeamMembers]: '管理谁有权访问此空间。', diff --git a/src/lib/preview.ts b/src/lib/preview.ts index 07b13867df..7e55366fba 100644 --- a/src/lib/preview.ts +++ b/src/lib/preview.ts @@ -3,10 +3,10 @@ import { localLiteStorage } from 'ltstrg' import { previewStyleKey } from './localStorageKeys' import { createStoreContext } from './context' -export const defaultPreviewStyle = ` +export const defaultCustomizablePreviewStyle = ` -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%; -color: #24292e; +line-height: 1.6; font-family: Lato, -apple-system, BlinkMacSystemFont, Helvetica, Arial, sans-serif; font-size: 15px; line-height: 1.6; @@ -29,10 +29,46 @@ a { } a:active, -a:hover { + a:hover { outline-width: 0; } +strong { + font-weight: inherit; + font-weight: bolder; +} + +h1 { + font-size: 2em; + margin: 0.67em 0; +} + +img { + border-style: none; +} + +code, + kbd, + pre { + font-family: monospace, monospace; + font-size: 1em; +} + +hr { + box-sizing: content-box; + height: 0; + overflow: visible; +} + +input { + font: inherit; + margin: 0; +} + +input { + overflow: visible; +} + [type='checkbox'] { box-sizing: border-box; padding: 0; @@ -43,9 +79,6 @@ a:hover { } input { - font: inherit; - margin: 0; - overflow: visible; font-family: inherit; font-size: inherit; line-height: inherit; @@ -64,6 +97,15 @@ strong { font-weight: 600; } +hr { + background: transparent; + border: 0; + border-bottom: 1px solid #dfe2e5; + height: 0; + margin: 15px 0; + overflow: hidden; +} + hr:before { content: ''; display: table; @@ -75,8 +117,13 @@ hr:after { display: table; } +table { + border-collapse: collapse; + border-spacing: 0; +} + td, -th { + th { padding: 0; } @@ -84,20 +131,80 @@ details summary { cursor: pointer; } +h1, + h2, + h3, + h4, + h5, + h6 { + margin-bottom: 0; + margin-top: 0; +} + +h1 { + font-size: 32px; +} + +h1, + h2 { + font-weight: 500; +} + +h2 { + font-size: 24px; +} + +h3 { + font-size: 20px; +} + +h3, + h4 { + font-weight: 500; +} + +h4 { + font-size: 16px; +} + +h5 { + font-size: 14px; +} + +h5, + h6 { + font-weight: 600; +} + +h6 { + font-size: 12px; +} + p { margin-bottom: 10px; margin-top: 0; } +blockquote { + margin: 0; +} + +ol, + ul { + margin-bottom: 0; + margin-top: 0; + padding-left: 0; +} + ol ol, -ul ol { + ul ol { list-style-type: lower-roman; } ol ol ol, -ol ul ol, -ul ol ol, -ul ul ol { + ol ul ol, + ul ol ol, + ul ul ol { list-style-type: lower-alpha; } @@ -106,12 +213,22 @@ dd { } code, -pre { + pre { font-family: SFMono-Regular, Consolas, Liberation Mono, Menlo, Courier, monospace; font-size: 12px; } +pre.CodeMirror, + code.CodeMirror { + height: auto; +} + +pre { + margin-bottom: 0; + margin-top: 0; +} + input::-webkit-inner-spin-button, input::-webkit-outer-spin-button { -webkit-appearance: none; @@ -143,24 +260,31 @@ a:not([href]) { text-decoration: none; } -blockquote { - margin: 0; - border-left: 0.25em solid #dfe2e5; - color: #6a737d; - padding: 0 1em; -} - blockquote, -dl, -ol, -p, -pre, -table, -ul { + dl, + ol, + p, + pre, + table, + ul { margin-bottom: 16px; margin-top: 0; } +hr { + background-color: #e1e4e8; + border: 0; + height: 1px; + margin: 16px 0; + padding: 0; +} + +blockquote { + border-left: 0.25em solid #e1e4e8; + color: #999999; + padding: 0 1em; +} + blockquote > :first-child { margin-top: 0; } @@ -169,59 +293,73 @@ blockquote > :last-child { margin-bottom: 0; } -h1, -h2, -h3, -h4, -h5, -h6 { - font-weight: 600; - line-height: 1.25; - margin-bottom: 16px; - margin-top: 24px; +kbd { + background-color: #fafbfc; + border: 1px solid #c6cbd1; + border-bottom-color: #959da5; + border-radius: 3px; + box-shadow: inset 0 -1px 0 #959da5; + color: #444d56; + display: inline-block; + font-size: 11px; + line-height: 10px; + padding: 3px 5px; + vertical-align: middle; } h1, -h2 { - border-bottom: 1px solid #eaecef; - padding-bottom: 0.3em; + h2, + h3, + h4, + h5, + h6 { + line-height: 1.25; + margin: 8px 0; } h1 { - font-size: 2em; - margin: 0.67em 0; + margin-top: 32px; + font-weight: 600; + font-size: 1.953em; + margin-bottom: 10px; } h2 { + font-weight: 600; font-size: 1.5em; + margin-top: 16px; + margin-bottom: 8px; } h3 { + font-weight: 600; font-size: 1.25em; } h4 { + font-weight: 400; font-size: 1em; } h5 { + font-weight: 400; font-size: 0.875em; } h6 { + font-weight: 400; color: #6a737d; font-size: 0.85em; } ol, -ul { + ul { padding-left: 2em; } - ol ol, -ol ul, -ul ol, -ul ul { + ol ul, + ul ol, + ul ul { margin-bottom: 0; margin-top: 0; } @@ -232,6 +370,13 @@ li { li > p { margin-top: 16px; + +&:first-child { + margin-top: 0; + } +&:last-child { + margin-bottom: 0; + } } li + li { @@ -256,8 +401,6 @@ dl dd { } table { - border-collapse: collapse; - border-spacing: 0; display: block; overflow: auto; width: 100%; @@ -268,25 +411,21 @@ table th { } table td, -table th { + table th { border: 1px solid #dfe2e5; padding: 6px 13px; } table tr { - background-color: #fff; border-top: 1px solid #c6cbd1; } -table tr:nth-child(2n) { - background-color: #f6f8fa; -} - img { - border-style: none; - background-color: #fff; - box-sizing: content-box; + display: block; max-width: 100%; + height: auto; + margin: 15px 0; + box-sizing: content-box; } img[align='right'] { @@ -306,7 +445,7 @@ code { } p code, -li code + li code { padding: 2px; border-width: 1px; @@ -338,7 +477,7 @@ pre > code { } .highlight pre, -pre { + pre { background-color: #f6f8fa; border-radius: 3px; font-size: 85%; @@ -381,7 +520,10 @@ kbd { } .task-list-item { + position: relative; list-style-type: none; + height: fit-content; + min-height: 25px; } .task-list-item + .task-list-item { @@ -393,16 +535,13 @@ kbd { vertical-align: middle; } +.task-list-item input[type=checkbox] { + position: absolute; + top: 6px; +} + hr { - box-sizing: content-box; - background: transparent; - overflow: hidden; - background-color: #e1e4e8; - border: 0; - height: 0.25em; - margin: 24px 0; - padding: 0; - border-bottom: 1px solid #eee; + border-bottom-color: #eee; } .dark p code, @@ -410,22 +549,25 @@ hr { { background-color: rgba(255,255,255,0.12); border-color: rgba(255,255,255,0.3); - color: #03C588; + color: #5580DC; } + .auto p code, .auto li code { background-color: #F9F9F9; border-color: rgba(0,0,0,0.26); - color: #03C588; + color: #5580DC; } + .light p code, .light li code { background-color: #F9F9F9; border-color: rgba(0,0,0,0.26); - color: #03C588; + color: #5580DC; } + .sepia p code, .sepia li code { @@ -433,6 +575,7 @@ hr { border-color: rgba(0,0,0,0.26); color: #F77942; } + .solarizedDark p code, .solarizedDark li code { @@ -444,7 +587,9 @@ hr { function loadPreviewStyle() { const previewStyle = localLiteStorage.getItem(previewStyleKey) - if (previewStyle == null) return defaultPreviewStyle + if (previewStyle == null) { + return defaultCustomizablePreviewStyle + } return previewStyle } @@ -453,7 +598,6 @@ function savePreviewStyle(style: string) { } const initialPreviewStyle = loadPreviewStyle() - function usePreviewStyleStore() { const [previewStyle, setPreviewStyle] = useState(initialPreviewStyle) diff --git a/src/mobile/components/pages/DocEditPage.tsx b/src/mobile/components/pages/DocEditPage.tsx index 77123bb5ff..f0865d9e89 100644 --- a/src/mobile/components/pages/DocEditPage.tsx +++ b/src/mobile/components/pages/DocEditPage.tsx @@ -31,7 +31,6 @@ import { } from '../../../cloud/lib/utils/events' import { ScrollSync, scrollSyncer } from '../../../cloud/lib/editor/scrollSync' import CodeMirrorEditor from '../../../cloud/lib/editor/components/CodeMirrorEditor' -import MarkdownView from '../../../cloud/components/MarkdownView' import { useToast } from '../../../design/lib/stores/toast' import EditorSelectionStatus from '../../../cloud/components/Editor/EditorSelectionStatus' import EditorThemeSelect from '../../../cloud/components/Editor/EditorThemeSelect' @@ -54,6 +53,7 @@ import { freePlanUploadSizeMb, paidPlanUploadSizeMb, } from '../../../cloud/lib/subscription' +import CustomizedMarkdownPreviewer from '../../../cloud/components/MarkdownView/CustomizedMarkdownPreviewer' interface EditorProps { doc: SerializedDocWithBookmark @@ -560,7 +560,7 @@ const Editor = ({ -
{realtimeContent !== '' ? ( -