// // Copyright 2023 DXOS.org // import { type Extension } from '@codemirror/state'; import { EditorView } from '@codemirror/view'; import { mx } from '@dxos/ui-theme'; export type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6; // https://tailwindcss.com/docs/font-weight const headings: Record = { 1: { className: 'text-3xl', fontSize: 'var(--text-3xl)', lineHeight: 'var(--text-4xl--line-height)', }, 2: { className: 'text-2xl', fontSize: 'var(--text-2xl)', lineHeight: 'var(--text-3xl--line-height)', }, 3: { className: 'text-xl', fontSize: 'var(--text-xl)', lineHeight: 'var(--text-2xl--line-height)', }, 4: { className: 'text-lg', fontSize: 'var(--text-lg)', lineHeight: 'var(--text-xl--line-height)', }, 5: { className: 'text-base', fontSize: 'var(--text-base)', lineHeight: 'var(--text-lg--line-height)', }, 6: { className: 'text-base', fontSize: 'var(--text-base)', lineHeight: 'var(--text-base--line-height)', }, }; // Font families matching --font-body and --font-mono in theme.css. export const fontBody = '"Nunito Variable", "Inter Variable", ui-sans-serif, system-ui, sans-serif'; export const fontMono = '"JetBrains Mono Variable", ui-monospace, "Cascadia Code", "Source Code Pro", monospace'; export const markdownTheme = { code: 'font-mono! cm-code-inline', codeMark: 'font-mono! cm-code-mark', mark: 'font-mono!', heading: (level: HeadingLevel) => ({ className: mx(headings[level].className, 'font-light text-(--color-cm-heading-number)'), color: 'var(--color-cm-heading) !important', lineHeight: headings[level].lineHeight, fontSize: headings[level].fontSize, fontWeight: '100 !important', }), }; /** * Global base theme. * * NOTE: The base theme is GLOBAL and is applied to ALL editors. * NOTE: `light` and `dark` selectors are preprocessed by CodeMirror and can only be in the base theme. * NOTE: Use 'unset' to remove default CM style. * * Examples: * - https://codemirror.net/examples/styling * - https://github.com/codemirror/view/blob/main/src/theme.ts * - https://github.com/codemirror/theme-one-dark/blob/main/src/one-dark.ts * * Main layout: * - https://codemirror.net/examples/styling * - https://codemirror.net/docs/guide (DOM Structure). * *
*
*
*
*
...
*
*
*
*
*
*
*
*
*
*
*
*
*
*/ export const baseTheme = EditorView.baseTheme({ /** * Outer frame. */ '&': {}, '&.cm-focused': { outline: 'none', }, /** * Scroller */ '.cm-scroller': { // Browser scroll-anchoring: see comment in `scrolling/crawler.ts`. `auto` lets the browser pin a // stable element near the viewport top so widget resizes (e.g. tool-block TogglePanel // open/close) don't jump the user's view. overflowAnchor: 'auto', }, // The strip is the thumb plus its inset at both ends, matching the ScrollArea overlay thumb // (8px thumb, 2px inset) so the two read identically side by side. '.cm-scroller::-webkit-scrollbar': { width: 'var(--scrollbar-size,12px)', height: 'var(--scrollbar-size,12px)', }, '.cm-scroller::-webkit-scrollbar-corner': { background: 'transparent', }, '.cm-scroller::-webkit-scrollbar-track': { background: 'transparent', }, // Matches the ScrollArea overlay thumb: a pill inset from the edge. The inset is a transparent // border with `background-clip: content-box`, since a scrollbar pseudo-element cannot be offset. '.cm-scroller::-webkit-scrollbar-thumb': { backgroundColor: 'transparent', borderRadius: 'var(--scrollbar-radius, 9999px)', border: 'var(--scrollbar-inset, 2px) solid transparent', backgroundClip: 'content-box', transition: 'background-color 0.15s', }, // `backgroundColor`, not the `background` shorthand, which would reset `background-clip` to // `border-box` and paint over the inset exactly when the thumb becomes visible. '&:hover .cm-scroller::-webkit-scrollbar-thumb': { backgroundColor: 'var(--color-scrollbar-thumb)', }, /** * Content * NOTE: Apply margins to content so that scrollbar is at the edge of the container. */ '.cm-content': { padding: 'unset', color: 'unset', }, /** * Gutters * NOTE: Gutters should have the same top margin as the content. */ '.cm-gutters': { background: 'transparent', borderRight: 'none', }, '.cm-gutter': {}, '.cm-gutter.cm-lineNumbers': { paddingRight: '4px', color: 'var(--color-fg-subtle)', }, '.cm-gutter.cm-lineNumbers .cm-gutterElement': { minWidth: '40px', }, /** * Height is set to match the corresponding line (which may have wrapped). */ '.cm-gutterElement': { lineHeight: '24px', fontSize: '12px', }, /** * Line. */ '.cm-line': { lineHeight: 1.5, paddingInline: 0, }, /** * Force all inline children to inherit line-height to prevent monospace font metrics * (JetBrains Mono ascent/descent) from inflating the line box beyond 24px. */ '.cm-line *': { lineHeight: 'inherit', }, '.cm-activeLine': { background: 'var(--color-cm-active-line)', }, /** * Cursor (layer). */ '.cm-cursor, .cm-dropCursor': { borderLeft: '2px solid var(--color-cm-cursor)', }, '.cm-placeholder': { color: 'var(--color-placeholder)', }, /** * Selection (layer). * NOTE: A browser dark-mode/reader extension can override `::selection` and reveal a second * (native) selection over this layer; that is an extension artifact, not an editor bug. */ '.cm-selectionBackground': { background: 'var(--color-cm-selection)', }, '&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground': { background: 'var(--color-cm-focused-selection)', }, /** * Search. * NOTE: Matches comment. */ '.cm-searchMatch': { margin: '0 -3px', padding: '3px', borderRadius: '3px', background: 'var(--color-cm-highlight-surface)', color: 'var(--color-cm-highlight)', }, '.cm-searchMatch-selected': { textDecoration: 'underline', }, /** * Link. */ '.cm-link': { textDecorationLine: 'underline', textDecorationThickness: '1px', textDecorationColor: 'var(--color-separator)', textUnderlineOffset: '2px', borderRadius: '.125rem', cursor: 'pointer', }, '.cm-link > span': { color: 'var(--color-accent-text)', }, '.cm-link > span:hover': { color: 'var(--color-accent-text-hover)', }, /** * Tooltip. */ '.cm-tooltip': { background: 'var(--color-modal-surface)', }, '.cm-tooltip-below': {}, '.cm-tooltip-hover': { background: 'var(--color-modal-surface)', border: '1px solid var(--color-separator)', borderRadius: '4px', overflow: 'hidden', }, /** * Autocomplete. * https://github.com/codemirror/autocomplete/blob/main/src/completion.ts */ '.cm-tooltip.cm-tooltip-autocomplete': { marginTop: '6px', marginLeft: '-10px', border: '2px solid var(--color-separator)', borderRadius: '4px', }, '.cm-tooltip.cm-tooltip-autocomplete > ul': { maxHeight: '20em', }, '.cm-tooltip.cm-tooltip-autocomplete > ul > li': { padding: '4px', }, '.cm-tooltip.cm-tooltip-autocomplete > ul > li[aria-selected]': { background: 'var(--color-current-surface)', color: 'var(--color-fg)', }, '.cm-tooltip.cm-tooltip-autocomplete > ul > completion-section': { paddingLeft: '4px !important', color: 'var(--color-fg)', }, /** * Completion info. */ '.cm-completionInfo': { width: '360px !important', margin: '-10px 1px 0 1px', padding: '8px !important', borderColor: 'var(--color-separator)', }, '.cm-completionIcon': { display: 'none', }, '.cm-completionLabel': { color: 'var(--color-fg-muted)', padding: '0 4px', }, '.cm-completionMatchedText': { color: 'var(--color-fg)', textDecoration: 'none !important', }, /** * Panels * https://github.com/codemirror/search/blob/main/src/search.ts#L745 * * Find/replace panel. *
...
*
...
*
* *
EditorView.theme({ // Main content. '.cm-scroller': { fontFamily: monospace ? fontMono : fontBody, }, // Maintain defaults for UI components. '.cm-content, .cm-gutters, .cm-panel': { fontFamily: 'inherit', fontSize: 'inherit', }, });