@dxos/react-ui-editor 0.6.8-main.046e6cf → 0.6.8-staging.77f93a3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/dist/lib/browser/index.mjs +290 -293
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/types/src/TextEditor.stories.d.ts +5 -2
  5. package/dist/types/src/TextEditor.stories.d.ts.map +1 -1
  6. package/dist/types/src/defaults.d.ts +1 -1
  7. package/dist/types/src/defaults.d.ts.map +1 -1
  8. package/dist/types/src/extensions/doc.d.ts +3 -0
  9. package/dist/types/src/extensions/doc.d.ts.map +1 -1
  10. package/dist/types/src/extensions/factories.d.ts.map +1 -1
  11. package/dist/types/src/extensions/folding.d.ts.map +1 -1
  12. package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -1
  13. package/dist/types/src/extensions/markdown/decorate.d.ts.map +1 -1
  14. package/dist/types/src/extensions/markdown/formatting.d.ts +1 -1
  15. package/dist/types/src/extensions/markdown/formatting.d.ts.map +1 -1
  16. package/dist/types/src/extensions/markdown/highlight.d.ts +2 -1
  17. package/dist/types/src/extensions/markdown/highlight.d.ts.map +1 -1
  18. package/dist/types/src/extensions/markdown/link-paste.d.ts +3 -0
  19. package/dist/types/src/extensions/markdown/link-paste.d.ts.map +1 -1
  20. package/dist/types/src/extensions/markdown/link.d.ts +3 -1
  21. package/dist/types/src/extensions/markdown/link.d.ts.map +1 -1
  22. package/dist/types/src/extensions/state.d.ts +14 -14
  23. package/dist/types/src/extensions/state.d.ts.map +1 -1
  24. package/dist/types/src/extensions/util/react.d.ts +1 -1
  25. package/dist/types/src/extensions/util/react.d.ts.map +1 -1
  26. package/dist/types/src/hooks/useTextEditor.d.ts +5 -3
  27. package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -1
  28. package/dist/types/src/styles/markdown.d.ts +7 -17
  29. package/dist/types/src/styles/markdown.d.ts.map +1 -1
  30. package/dist/types/src/styles/theme.d.ts +2 -1
  31. package/dist/types/src/styles/theme.d.ts.map +1 -1
  32. package/dist/types/src/styles/tokens.d.ts +5 -7
  33. package/dist/types/src/styles/tokens.d.ts.map +1 -1
  34. package/package.json +24 -24
  35. package/src/TextEditor.stories.tsx +40 -27
  36. package/src/defaults.ts +2 -1
  37. package/src/extensions/doc.ts +3 -0
  38. package/src/extensions/factories.ts +3 -2
  39. package/src/extensions/folding.tsx +5 -7
  40. package/src/extensions/markdown/bundle.ts +1 -3
  41. package/src/extensions/markdown/decorate.ts +29 -23
  42. package/src/extensions/markdown/formatting.ts +3 -1
  43. package/src/extensions/markdown/highlight.ts +33 -19
  44. package/src/extensions/markdown/link-paste.ts +3 -0
  45. package/src/extensions/state.ts +41 -35
  46. package/src/extensions/util/react.tsx +3 -4
  47. package/src/hooks/useTextEditor.ts +24 -29
  48. package/src/styles/markdown.ts +17 -40
  49. package/src/styles/theme.ts +42 -49
  50. package/src/styles/tokens.ts +9 -7
@@ -7,6 +7,9 @@ import { type EditorState, Transaction } from '@codemirror/state';
7
7
  import { ViewPlugin, type ViewUpdate, type PluginValue } from '@codemirror/view';
8
8
  import { type SyntaxNode } from '@lezer/common';
9
9
 
10
+ /**
11
+ * Formats pasted URLs as markdown links and images.
12
+ */
10
13
  export const linkPastePlugin = ViewPlugin.fromClass(
11
14
  class implements PluginValue {
12
15
  update(update: ViewUpdate) {
@@ -2,7 +2,7 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { type Extension, Transaction } from '@codemirror/state';
5
+ import { type Extension, Transaction, type TransactionSpec } from '@codemirror/state';
6
6
  import { EditorView, keymap } from '@codemirror/view';
7
7
 
8
8
  import { debounce } from '@dxos/async';
@@ -11,76 +11,82 @@ import { isNotFalsy } from '@dxos/util';
11
11
 
12
12
  import { documentId } from './doc';
13
13
 
14
- const scrollAnnotation = 'dxos.org/cm/scrolling';
14
+ const stateRestoreAnnotation = 'dxos.org/cm/state-restore';
15
15
 
16
- // NOTE: Serializable.
17
- export type SelectionState = {
18
- scrollTo: {
19
- from: number;
20
- };
21
- selection: {
22
- anchor: number;
23
- head?: number;
24
- };
16
+ export type EditorSelection = {
17
+ anchor: number;
18
+ head?: number;
19
+ };
20
+
21
+ export type EditorSelectionState = {
22
+ scrollTo?: number;
23
+ selection?: EditorSelection;
25
24
  };
26
25
 
27
- export type StateOptions = {
28
- setState: (id: string, state: SelectionState) => void;
29
- getState: (id: string) => SelectionState | undefined;
26
+ export type EditorStateOptions = {
27
+ setState: (id: string, state: EditorSelectionState) => void;
28
+ getState: (id: string) => EditorSelectionState | undefined;
30
29
  };
31
30
 
32
31
  const keyPrefix = 'dxos.org/react-ui-editor/state';
33
- export const localStorageStateStoreAdapter: StateOptions = {
34
- setState: (id, state) => {
35
- invariant(id);
36
- localStorage.setItem(`${keyPrefix}/${id}`, JSON.stringify(state));
37
- },
32
+ export const localStorageStateStoreAdapter: EditorStateOptions = {
38
33
  getState: (id) => {
39
34
  invariant(id);
40
35
  const state = localStorage.getItem(`${keyPrefix}/${id}`);
41
36
  return state ? JSON.parse(state) : undefined;
42
37
  },
38
+
39
+ setState: (id, state) => {
40
+ invariant(id);
41
+ localStorage.setItem(`${keyPrefix}/${id}`, JSON.stringify(state));
42
+ },
43
+ };
44
+
45
+ export const createEditorStateTransaction = ({ scrollTo, selection }: EditorSelectionState): TransactionSpec => {
46
+ return {
47
+ selection,
48
+ scrollIntoView: !scrollTo,
49
+ effects: scrollTo ? EditorView.scrollIntoView(scrollTo, { yMargin: 96 }) : undefined,
50
+ annotations: Transaction.userEvent.of(stateRestoreAnnotation),
51
+ };
43
52
  };
44
53
 
45
54
  /**
46
55
  * Track scrolling and selection state to be restored when switching to document.
47
56
  */
48
- export const state = ({ getState, setState }: Partial<StateOptions> = {}): Extension => {
57
+ export const state = ({ getState, setState }: Partial<EditorStateOptions> = {}): Extension => {
49
58
  const setStateDebounced = debounce(setState!, 1_000);
50
59
 
51
60
  return [
52
61
  // TODO(burdon): Track scrolling (currently only updates when cursor moves).
53
- EditorView.updateListener.of(({ view, changes, transactions }) => {
54
- // TODO(burdon): Don't react to initial scroll.
62
+ // EditorView.domEventHandlers({
63
+ // scroll: (event) => {
64
+ // setStateDebounced(id, {});
65
+ // },
66
+ // }),
67
+ EditorView.updateListener.of(({ view, transactions }) => {
55
68
  const id = view.state.facet(documentId);
56
- if (!id || transactions.some((tr) => tr.isUserEvent(scrollAnnotation))) {
69
+ if (!id || transactions.some((tr) => tr.isUserEvent(stateRestoreAnnotation))) {
57
70
  return;
58
71
  }
59
72
 
60
73
  if (setState) {
61
- const { top } = view.dom.getBoundingClientRect();
62
- const pos = view.posAtCoords({ x: 0, y: top });
74
+ const { scrollTop } = view.scrollDOM;
75
+ const pos = view.posAtCoords({ x: 0, y: scrollTop });
63
76
  if (pos !== null) {
64
77
  const { anchor, head } = view.state.selection.main;
65
- setStateDebounced(id, {
66
- scrollTo: { from: pos, yMargin: 0 },
67
- selection: { anchor, head },
68
- });
78
+ setStateDebounced(id, { scrollTo: pos, selection: { anchor, head } });
69
79
  }
70
80
  }
71
81
  }),
72
82
  getState &&
73
83
  keymap.of([
74
84
  {
75
- key: 'ctrl-r', // TODO(burdon): Setting to jump back to bookmark.
85
+ key: 'ctrl-r', // TODO(burdon): Setting to jump back to selection.
76
86
  run: (view) => {
77
87
  const state = getState(view.state.facet(documentId));
78
88
  if (state) {
79
- view.dispatch({
80
- effects: EditorView.scrollIntoView(state.scrollTo.from, { yMargin: 0 }),
81
- selection: state.selection,
82
- annotations: Transaction.userEvent.of(scrollAnnotation),
83
- });
89
+ view.dispatch(createEditorStateTransaction(state));
84
90
  }
85
91
  return true;
86
92
  },
@@ -8,8 +8,7 @@ import { createRoot } from 'react-dom/client';
8
8
  import { ThemeProvider } from '@dxos/react-ui';
9
9
  import { defaultTx } from '@dxos/react-ui-theme';
10
10
 
11
- export const renderRoot = (node: ReactNode) => {
12
- const el = document.createElement('div');
13
- createRoot(el).render(<ThemeProvider tx={defaultTx}>{node}</ThemeProvider>);
14
- return el;
11
+ export const renderRoot = (root: HTMLElement, node: ReactNode): HTMLElement => {
12
+ createRoot(root).render(<ThemeProvider tx={defaultTx}>{node}</ThemeProvider>);
13
+ return root;
15
14
  };
@@ -2,7 +2,7 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { EditorState, type EditorStateConfig, type StateEffect } from '@codemirror/state';
5
+ import { EditorState, type EditorStateConfig } from '@codemirror/state';
6
6
  import { EditorView } from '@codemirror/view';
7
7
  import { useFocusableGroup } from '@fluentui/react-tabster';
8
8
  import {
@@ -17,10 +17,9 @@ import {
17
17
  } from 'react';
18
18
 
19
19
  import { log } from '@dxos/log';
20
- import { useDefaultValue } from '@dxos/react-ui';
21
20
  import { isNotFalsy, type MaybeFunction } from '@dxos/util';
22
21
 
23
- import { documentId, editorInputMode } from '../extensions';
22
+ import { createEditorStateTransaction, documentId, editorInputMode, type EditorSelection } from '../extensions';
24
23
  import { logChanges } from '../util';
25
24
 
26
25
  export type UseTextEditor = {
@@ -41,12 +40,13 @@ export type CursorInfo = {
41
40
  after?: string;
42
41
  };
43
42
 
44
- export type UseTextEditorProps = Pick<EditorStateConfig, 'selection' | 'extensions'> & {
43
+ export type UseTextEditorProps = Pick<EditorStateConfig, 'extensions'> & {
45
44
  id?: string;
46
45
  initialValue?: string;
47
46
  className?: string;
48
47
  autoFocus?: boolean;
49
- scrollTo?: StateEffect<unknown>;
48
+ scrollTo?: number;
49
+ selection?: EditorSelection;
50
50
  moveToEndOfLine?: boolean;
51
51
  debug?: boolean;
52
52
  };
@@ -60,22 +60,14 @@ export const useTextEditor = (
60
60
  props: MaybeFunction<UseTextEditorProps> = {},
61
61
  deps: DependencyList = [],
62
62
  ): UseTextEditor => {
63
- const {
64
- id,
65
- initialValue,
66
- selection,
67
- extensions,
68
- autoFocus,
69
- scrollTo: _scrollTo,
70
- moveToEndOfLine,
71
- debug,
72
- } = useMemo<UseTextEditorProps>(() => {
73
- return typeof props === 'function' ? props() : props;
74
- }, deps ?? []);
63
+ const { id, initialValue, extensions, autoFocus, scrollTo, selection, moveToEndOfLine, debug } =
64
+ useMemo<UseTextEditorProps>(() => {
65
+ return typeof props === 'function' ? props() : props;
66
+ }, deps ?? []);
75
67
 
76
68
  // NOTE: Increments by 2 in strict mode.
77
69
  const [instanceId] = useState(() => `text-editor-${++instanceCount}`);
78
- const scrollTo = useDefaultValue(_scrollTo, EditorView.scrollIntoView(0, { yMargin: 0 }));
70
+ // Callback once view is created.
79
71
  const onUpdate = useRef<() => void>();
80
72
  const [view, setView] = useState<EditorView>();
81
73
  const parentRef = useRef<HTMLDivElement>(null);
@@ -85,8 +77,12 @@ export const useTextEditor = (
85
77
  if (parentRef.current) {
86
78
  log('create', { id, instanceId, doc: initialValue?.length ?? 0 });
87
79
 
88
- let initialSelection = selection;
89
- if (moveToEndOfLine && selection === undefined) {
80
+ let initialSelection;
81
+ if (selection?.anchor && initialValue?.length) {
82
+ if (selection.anchor <= initialValue.length && (selection?.head ?? 0) <= initialValue.length) {
83
+ initialSelection = selection;
84
+ }
85
+ } else if (moveToEndOfLine && selection === undefined) {
90
86
  const index = initialValue?.indexOf('\n');
91
87
  const anchor = !index || index === -1 ? 0 : index;
92
88
  initialSelection = { anchor };
@@ -105,7 +101,9 @@ export const useTextEditor = (
105
101
  }),
106
102
  extensions,
107
103
  EditorView.updateListener.of(() => {
108
- onUpdate.current?.();
104
+ setTimeout(() => {
105
+ onUpdate.current?.();
106
+ });
109
107
  }),
110
108
  ].filter(isNotFalsy),
111
109
  });
@@ -113,7 +111,6 @@ export const useTextEditor = (
113
111
  // https://codemirror.net/docs/ref/#view.EditorViewConfig
114
112
  view = new EditorView({
115
113
  parent: parentRef.current,
116
- scrollTo,
117
114
  selection: initialSelection,
118
115
  state,
119
116
  // NOTE: Uncomment to debug/monitor all transactions.
@@ -143,13 +140,11 @@ export const useTextEditor = (
143
140
 
144
141
  useEffect(() => {
145
142
  if (view) {
146
- // TODO(burdon): Set selection after first update (since content may rerender on focus)?
147
- if (scrollTo) {
148
- onUpdate.current = () => {
149
- onUpdate.current = undefined;
150
- view.dispatch({ effects: scrollTo && [scrollTo], scrollIntoView: !scrollTo });
151
- };
152
- }
143
+ // NOTE: Set selection after first update (since content may rerender on focus).
144
+ onUpdate.current = () => {
145
+ onUpdate.current = undefined;
146
+ view.dispatch(createEditorStateTransaction({ scrollTo, selection }));
147
+ };
153
148
 
154
149
  // Remove tabster attribute (rely on custom keymap).
155
150
  if (view.state.facet(editorInputMode).noTabster) {
@@ -6,48 +6,25 @@ import { mx } from '@dxos/react-ui-theme';
6
6
 
7
7
  export type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6;
8
8
 
9
- // TODO(burdon): Better way to align vertically than negative margin? Font-specific?
10
9
  // https://tailwindcss.com/docs/font-weight
11
10
  const headings: Record<HeadingLevel, string> = {
12
- 1: 'mbs-4 mbe-2 font-medium text-inherit no-underline text-4xl',
13
- 2: 'mbs-4 mbe-2 font-medium text-inherit no-underline text-3xl',
14
- 3: 'mbs-4 mbe-2 font-medium text-inherit no-underline text-2xl',
15
- 4: 'mbs-4 mbe-2 font-medium text-inherit no-underline text-xl',
16
- 5: 'mbs-4 mbe-2 font-medium text-inherit no-underline text-lg',
17
- 6: 'mbs-4 mbe-2 font-medium text-inherit no-underline',
11
+ 1: 'text-4xl',
12
+ 2: 'text-3xl',
13
+ 3: 'text-2xl',
14
+ 4: 'text-xl',
15
+ 5: 'text-lg',
16
+ 6: 'text-md',
18
17
  };
19
18
 
20
- // TODO(burdon): Themes.
21
- export const heading = (level: HeadingLevel) => {
22
- return mx(headings[level], 'dark:text-primary-400');
19
+ // TODO(burdon): Define theme as facet (used in multiple extensions).
20
+ // TODO(burdon): Organize theme styles for widgets.
21
+ export const theme = {
22
+ mark: 'opacity-50',
23
+ code: 'font-mono !no-underline text-neutral-700 dark:text-neutral-300',
24
+ codeMark: 'font-mono text-primary-500',
25
+ // TODO(burdon): Replace with widget.
26
+ blockquote: 'pl-1 mr-1 border-is-4 border-orange-500 dark:border-orange-500 dark:text-neutral-500',
27
+ heading: (level: HeadingLevel) => {
28
+ return mx(headings[level], 'dark:text-primary-400');
29
+ },
23
30
  };
24
-
25
- export const text = 'text-neutral-800 dark:text-neutral-200';
26
- export const light = 'text-neutral-200 dark:text-neutral-800';
27
-
28
- export const mark = mx('!font-normal !no-underline !text-inherit opacity-40', light);
29
-
30
- export const paragraph = 'mlb-1';
31
-
32
- export const bold = 'font-bold';
33
- export const italic = 'italic';
34
- export const strikethrough = 'line-through';
35
-
36
- export const code = 'font-mono !no-underline text-neutral-700 dark:text-neutral-300';
37
- export const codeMark = 'font-mono text-primary-500';
38
- export const codeBlock = 'mlb-2 font-mono bg-neutral-500/10 p-3 rounded';
39
-
40
- export const inlineUrl = mx(code, 'px-1');
41
-
42
- export const blockquote = mx('pl-1 mr-1 border-is-4 border-orange-500 dark:border-orange-500 text-transparent');
43
-
44
- export const horizontalRule =
45
- 'flex mlb-4 border-b text-neutral-100 dark:text-neutral-900 border-neutral-200 dark:border-neutral-800';
46
-
47
- // TODO(thure): Tailwind was not seeing `[&>li:before]:content-["•"]` as a utility class, but it would work if instead of `"•"` it was `"X"`… why?
48
- export const unorderedList =
49
- 'mlb-2 grid grid-cols-[min-content_1fr] [&>li:before]:content-[attr(marker)] [&>li:before]:mlb-1 [&>li:before]:mie-2';
50
- export const orderedList =
51
- 'mlb-2 grid grid-cols-[min-content_1fr] [&>li:before]:content-[counters(section,_".")_"._"] [counter-reset:section] [&>li:before]:mlb-1';
52
-
53
- export const listItem = 'contents before:[counter-increment:section]';
@@ -2,10 +2,13 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import get from 'lodash.get';
5
+ import { type StyleSpec } from 'style-mod';
6
6
 
7
- import { type ThemeStyles, tokens } from './tokens';
7
+ import { getToken } from './tokens';
8
8
 
9
+ export type ThemeStyles = Record<string, StyleSpec>;
10
+
11
+ // TODO(burdon): Factor out theme.
9
12
  // TODO(burdon): Can we use @apply and import css file?
10
13
  // https://tailwindcss.com/docs/reusing-styles#extracting-classes-with-apply?
11
14
 
@@ -52,8 +55,6 @@ export const defaultTheme: ThemeStyles = {
52
55
  // NOTE: See https://codemirror.net/docs/guide (DOM Structure).
53
56
  '.cm-scroller': {
54
57
  overflowY: 'auto',
55
- fontFamily: get(tokens, 'fontFamily.body', []).join(','),
56
- lineHeight: 1.5,
57
58
  },
58
59
 
59
60
  // Content.
@@ -61,12 +62,14 @@ export const defaultTheme: ThemeStyles = {
61
62
  padding: 'unset',
62
63
  // NOTE: Base font size (otherwise defined by HTML tag, which might be different for storybook).
63
64
  fontSize: '16px',
65
+ fontFamily: getToken('fontFamily.body'),
66
+ lineHeight: 1.5,
64
67
  },
65
68
  '&light .cm-content': {
66
- color: get(tokens, 'extend.semanticColors.base.fg.light', 'black'),
69
+ color: getToken('extend.semanticColors.base.fg.light', 'black'),
67
70
  },
68
71
  '&dark .cm-content': {
69
- color: get(tokens, 'extend.semanticColors.base.fg.dark', 'red'),
72
+ color: getToken('extend.semanticColors.base.fg.dark', 'white'),
70
73
  },
71
74
 
72
75
  //
@@ -79,10 +82,10 @@ export const defaultTheme: ThemeStyles = {
79
82
  borderLeft: '2px solid white',
80
83
  },
81
84
  '&light .cm-placeholder': {
82
- color: get(tokens, 'extend.semanticColors.description.light', 'rgba(0,0,0,.2)'),
85
+ color: getToken('extend.semanticColors.description.light', 'rgba(0,0,0,.2)'),
83
86
  },
84
87
  '&dark .cm-placeholder': {
85
- color: get(tokens, 'extend.semanticColors.description.dark', 'rgba(255,255,255,.2)'),
88
+ color: getToken('extend.semanticColors.description.dark', 'rgba(255,255,255,.2)'),
86
89
  },
87
90
 
88
91
  //
@@ -107,16 +110,16 @@ export const defaultTheme: ThemeStyles = {
107
110
  //
108
111
 
109
112
  '&light .cm-selectionBackground': {
110
- background: get(tokens, 'extend.colors.primary.100'),
113
+ background: getToken('extend.colors.primary.100'),
111
114
  },
112
115
  '&light.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground': {
113
- background: get(tokens, 'extend.colors.primary.200'),
116
+ background: getToken('extend.colors.primary.200'),
114
117
  },
115
118
  '&dark .cm-selectionBackground': {
116
- background: get(tokens, 'extend.colors.primary.700'),
119
+ background: getToken('extend.colors.primary.700'),
117
120
  },
118
121
  '&dark.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground': {
119
- background: get(tokens, 'extend.colors.primary.600'),
122
+ background: getToken('extend.colors.primary.600'),
120
123
  },
121
124
 
122
125
  //
@@ -124,22 +127,27 @@ export const defaultTheme: ThemeStyles = {
124
127
  //
125
128
 
126
129
  '&light .cm-searchMatch': {
127
- backgroundColor: get(tokens, 'extend.colors.yellow.100'),
130
+ backgroundColor: getToken('extend.colors.yellow.100'),
128
131
  },
129
132
  '&dark .cm-searchMatch': {
130
- backgroundColor: get(tokens, 'extend.colors.yellow.700'),
133
+ backgroundColor: getToken('extend.colors.yellow.700'),
131
134
  },
132
135
 
133
136
  //
134
137
  // link
135
138
  //
136
139
  '.cm-link': {
137
- color: get(tokens, 'extend.colors.primary.500'),
138
140
  textDecorationLine: 'underline',
139
141
  textDecorationThickness: '1px',
140
142
  textUnderlineOffset: '2px',
141
143
  borderRadius: '.125rem',
142
- fontFamily: get(tokens, 'fontFamily.body', []).join(','),
144
+ fontFamily: getToken('fontFamily.body'),
145
+ },
146
+ '&light .cm-link > span': {
147
+ color: getToken('extend.colors.primary.600'),
148
+ },
149
+ '&dark .cm-link > span': {
150
+ color: getToken('extend.colors.primary.400'),
143
151
  },
144
152
 
145
153
  //
@@ -147,10 +155,10 @@ export const defaultTheme: ThemeStyles = {
147
155
  //
148
156
  '.cm-tooltip': {},
149
157
  '&light .cm-tooltip': {
150
- background: `${get(tokens, 'extend.colors.neutral.100')} !important`,
158
+ background: `${getToken('extend.colors.neutral.100')} !important`,
151
159
  },
152
160
  '&dark .cm-tooltip': {
153
- background: `${get(tokens, 'extend.colors.neutral.900')} !important`,
161
+ background: `${getToken('extend.colors.neutral.900')} !important`,
154
162
  },
155
163
  '.cm-tooltip-below': {},
156
164
 
@@ -167,14 +175,13 @@ export const defaultTheme: ThemeStyles = {
167
175
  },
168
176
  '.cm-tooltip.cm-tooltip-autocomplete > ul > li': {},
169
177
  '.cm-tooltip.cm-tooltip-autocomplete > ul > li[aria-selected]': {},
170
- // TODO(burdon): Can we add a class prefix to avoid adding !important?
171
178
  '.cm-tooltip.cm-tooltip-autocomplete > ul > completion-section': {
172
179
  paddingLeft: '4px !important',
173
180
  borderBottom: 'none !important',
174
- color: get(tokens, 'extend.colors.primary.500'),
181
+ color: getToken('extend.colors.primary.500'),
175
182
  },
176
183
  '.cm-tooltip.cm-completionInfo': {
177
- border: get(tokens, 'extend.colors.neutral.500'),
184
+ border: getToken('extend.colors.neutral.500'),
178
185
  width: '360px !important',
179
186
  margin: '-10px 1px 0 1px',
180
187
  padding: '8px !important',
@@ -183,7 +190,7 @@ export const defaultTheme: ThemeStyles = {
183
190
  display: 'none',
184
191
  },
185
192
  '.cm-completionLabel': {
186
- fontFamily: get(tokens, 'fontFamily.body', []).join(','),
193
+ fontFamily: getToken('fontFamily.body'),
187
194
  },
188
195
  '.cm-completionMatchedText': {
189
196
  textDecoration: 'none !important',
@@ -194,14 +201,14 @@ export const defaultTheme: ThemeStyles = {
194
201
  // table
195
202
  //
196
203
  '.cm-table *': {
197
- fontFamily: `${get(tokens, 'fontFamily.mono', []).join(',')} !important`,
204
+ fontFamily: `${getToken('fontFamily.mono')} !important`,
198
205
  textDecoration: 'none !important',
199
206
  },
200
207
  '.cm-table-head': {
201
208
  padding: '2px 16px 2px 0px',
202
209
  textAlign: 'left',
203
- borderBottom: `1px solid ${get(tokens, 'extend.colors.primary.500')}`,
204
- color: get(tokens, 'extend.colors.neutral.500'),
210
+ borderBottom: `1px solid ${getToken('extend.colors.primary.500')}`,
211
+ color: getToken('extend.colors.neutral.500'),
205
212
  },
206
213
  '.cm-table-cell': {
207
214
  padding: '2px 16px 2px 0px',
@@ -220,20 +227,6 @@ export const defaultTheme: ThemeStyles = {
220
227
  borderBottom: '0.5rem solid transparent',
221
228
  },
222
229
 
223
- //
224
- // font size
225
- // TODO(thure): This appears to be the best or only way to set selection caret heights,
226
- // but it's far more verbose than it needs to be.
227
- //
228
- // ...Object.keys(get(tokens, 'extend.fontSize', {})).reduce((acc: Record<string, any>, fontSize) => {
229
- // const height = get(tokens, ['extend', 'fontSize', fontSize, 1, 'lineHeight']);
230
- //
231
- // acc[`& .text-${fontSize} + .cm-ySelectionCaret`] = { height };
232
- // acc[`& .text-${fontSize} + .cm-ySelection + .cm-ySelectionCaret`] = { height };
233
- // acc[`& .text-${fontSize} + .cm-widgetBuffer + .cm-ySelectionCaret`] = { height };
234
- // return acc;
235
- // }, {}),
236
-
237
230
  // TODO(burdon): Override vars --cm-background.
238
231
  // https://www.npmjs.com/package/codemirror-theme-vars
239
232
 
@@ -262,20 +255,20 @@ export const defaultTheme: ThemeStyles = {
262
255
  */
263
256
  '.cm-panels': {},
264
257
  '.cm-panel': {
265
- fontFamily: get(tokens, 'fontFamily.body', []).join(','),
258
+ fontFamily: getToken('fontFamily.body'),
266
259
  },
267
260
  '.cm-panel input[type=checkbox]': {
268
261
  marginRight: '0.4rem !important',
269
262
  },
270
263
  '&light .cm-panel': {
271
- background: get(tokens, 'extend.colors.neutral.50'),
264
+ background: getToken('extend.colors.neutral.50'),
272
265
  },
273
266
  '&dark .cm-panel': {
274
- background: get(tokens, 'extend.colors.neutral.850'),
267
+ background: getToken('extend.colors.neutral.850'),
275
268
  },
276
269
  '.cm-button': {
277
270
  margin: '4px',
278
- fontFamily: get(tokens, 'fontFamily.body', []).join(','),
271
+ fontFamily: getToken('fontFamily.body'),
279
272
  backgroundImage: 'none',
280
273
  border: 'none',
281
274
  '&:active': {
@@ -283,21 +276,21 @@ export const defaultTheme: ThemeStyles = {
283
276
  },
284
277
  },
285
278
  '&light .cm-button': {
286
- background: get(tokens, 'extend.colors.neutral.100'),
279
+ background: getToken('extend.colors.neutral.100'),
287
280
  '&:hover': {
288
- background: get(tokens, 'extend.colors.neutral.200'),
281
+ background: getToken('extend.colors.neutral.200'),
289
282
  },
290
283
  '&:active': {
291
- background: get(tokens, 'extend.colors.neutral.300'),
284
+ background: getToken('extend.colors.neutral.300'),
292
285
  },
293
286
  },
294
287
  '&dark .cm-button': {
295
- background: get(tokens, 'extend.colors.neutral.800'),
288
+ background: getToken('extend.colors.neutral.800'),
296
289
  '&:hover': {
297
- background: get(tokens, 'extend.colors.neutral.700'),
290
+ background: getToken('extend.colors.neutral.700'),
298
291
  },
299
292
  '&:active': {
300
- background: get(tokens, 'extend.colors.neutral.600'),
293
+ background: getToken('extend.colors.neutral.600'),
301
294
  },
302
295
  },
303
296
  };
@@ -3,14 +3,16 @@
3
3
  //
4
4
 
5
5
  import get from 'lodash.get';
6
- import type { StyleSpec } from 'style-mod';
7
6
 
8
7
  import { tailwindConfig, type TailwindConfig } from '@dxos/react-ui-theme';
9
8
 
10
- export type ThemeStyles = {
11
- [selector: string]: StyleSpec;
12
- };
13
-
14
- export const tokens: TailwindConfig['theme'] = tailwindConfig({}).theme;
9
+ const tokens: TailwindConfig['theme'] = tailwindConfig({}).theme;
15
10
 
16
- export const getToken = (path: string, defaultValue: any = undefined) => get(tokens, path, defaultValue);
11
+ /**
12
+ * @deprecated
13
+ * Replace with CSS vars.
14
+ */
15
+ export const getToken = (path: string, defaultValue?: string | string[]): string => {
16
+ const value = get(tokens, path, defaultValue);
17
+ return value?.toString() ?? '';
18
+ };