@dxos/react-ui-editor 0.8.1-staging.391c573 → 0.8.1-staging.9eaf14f
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.
- package/dist/lib/browser/index.mjs +383 -255
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node/index.cjs +415 -290
- package/dist/lib/node/index.cjs.map +4 -4
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +383 -255
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/types/src/InputMode.stories.d.ts +2 -2
- package/dist/types/src/TextEditor.stories.d.ts +5 -40
- package/dist/types/src/TextEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/EditorToolbar/EditorToolbar.d.ts.map +1 -1
- package/dist/types/src/defaults.d.ts +2 -0
- package/dist/types/src/defaults.d.ts.map +1 -1
- package/dist/types/src/extensions/command/command.d.ts +4 -2
- package/dist/types/src/extensions/command/command.d.ts.map +1 -1
- package/dist/types/src/extensions/command/hint.d.ts.map +1 -1
- package/dist/types/src/extensions/command/menu.d.ts +12 -0
- package/dist/types/src/extensions/command/menu.d.ts.map +1 -0
- package/dist/types/src/extensions/command/preview.d.ts +12 -0
- package/dist/types/src/extensions/command/preview.d.ts.map +1 -0
- package/dist/types/src/extensions/command/state.d.ts.map +1 -1
- package/dist/types/src/extensions/comments.d.ts +3 -3
- package/dist/types/src/extensions/comments.d.ts.map +1 -1
- package/dist/types/src/extensions/factories.d.ts +2 -1
- package/dist/types/src/extensions/factories.d.ts.map +1 -1
- package/dist/types/src/extensions/folding.d.ts +2 -8
- package/dist/types/src/extensions/folding.d.ts.map +1 -1
- package/dist/types/src/extensions/selection.d.ts +6 -1
- package/dist/types/src/extensions/selection.d.ts.map +1 -1
- package/dist/types/src/{styles/stack-item-content-class-names.d.ts → fragments.d.ts} +1 -1
- package/dist/types/src/fragments.d.ts.map +1 -0
- package/dist/types/src/index.d.ts +0 -1
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/styles/theme.d.ts.map +1 -1
- package/package.json +27 -27
- package/src/InputMode.stories.tsx +4 -4
- package/src/TextEditor.stories.tsx +183 -61
- package/src/components/EditorToolbar/EditorToolbar.tsx +4 -5
- package/src/defaults.ts +12 -0
- package/src/extensions/command/command.ts +21 -2
- package/src/extensions/command/hint.ts +3 -0
- package/src/extensions/command/menu.ts +100 -0
- package/src/extensions/command/preview.ts +79 -0
- package/src/extensions/command/state.ts +9 -4
- package/src/extensions/comments.ts +6 -10
- package/src/extensions/factories.ts +4 -3
- package/src/extensions/folding.tsx +30 -73
- package/src/extensions/selection.ts +41 -21
- package/src/{styles/stack-item-content-class-names.ts → fragments.ts} +4 -2
- package/src/index.ts +0 -4
- package/src/styles/theme.ts +6 -1
- package/src/util/debug.ts +1 -1
- package/dist/types/src/styles/stack-item-content-class-names.d.ts.map +0 -1
@@ -4,12 +4,13 @@
|
|
4
4
|
|
5
5
|
import '@dxos-theme';
|
6
6
|
|
7
|
+
import { type Completion } from '@codemirror/autocomplete';
|
7
8
|
import { javascript } from '@codemirror/lang-javascript';
|
8
9
|
import { markdown } from '@codemirror/lang-markdown';
|
9
10
|
import { openSearchPanel } from '@codemirror/search';
|
10
11
|
import { type Extension } from '@codemirror/state';
|
11
12
|
import { type EditorView } from '@codemirror/view';
|
12
|
-
import {
|
13
|
+
import { X } from '@phosphor-icons/react';
|
13
14
|
import { effect, useSignal } from '@preact/signals-react';
|
14
15
|
import defaultsDeep from 'lodash.defaultsdeep';
|
15
16
|
import React, { useEffect, useState, type FC, type KeyboardEvent } from 'react';
|
@@ -22,9 +23,9 @@ import { create } from '@dxos/live-object';
|
|
22
23
|
import { log } from '@dxos/log';
|
23
24
|
import { faker } from '@dxos/random';
|
24
25
|
import { createDocAccessor, createObject } from '@dxos/react-client/echo';
|
25
|
-
import { Button, Input, useThemeContext } from '@dxos/react-ui';
|
26
|
-
import { baseSurface, getSize, mx } from '@dxos/react-ui-theme';
|
27
|
-
import { withLayout, withTheme } from '@dxos/storybook-utils';
|
26
|
+
import { Button, Icon, IconButton, Input, ThemeProvider, useThemeContext } from '@dxos/react-ui';
|
27
|
+
import { baseSurface, defaultTx, getSize, mx } from '@dxos/react-ui-theme';
|
28
|
+
import { type Meta, withLayout, withTheme } from '@dxos/storybook-utils';
|
28
29
|
|
29
30
|
import { editorContent, editorGutter, editorMonospace } from './defaults';
|
30
31
|
import {
|
@@ -202,7 +203,7 @@ const text = str(
|
|
202
203
|
'=== LAST LINE ===',
|
203
204
|
);
|
204
205
|
|
205
|
-
const links = [
|
206
|
+
const links: Completion[] = [
|
206
207
|
{ label: 'DXOS', apply: '[DXOS](https://dxos.org)' },
|
207
208
|
{ label: 'GitHub', apply: '[DXOS GitHub](https://github.com/dxos)' },
|
208
209
|
{ label: 'Automerge', apply: '[Automerge](https://automerge.org/)' },
|
@@ -215,16 +216,6 @@ const names = ['adam', 'alice', 'alison', 'bob', 'carol', 'charlie', 'sayuri', '
|
|
215
216
|
const hover =
|
216
217
|
'rounded-sm text-baseText text-primary-600 hover:text-primary-500 dark:text-primary-300 hover:dark:text-primary-200';
|
217
218
|
|
218
|
-
const renderLinkTooltip = (el: Element, url: string) => {
|
219
|
-
const web = new URL(url);
|
220
|
-
createRoot(el).render(
|
221
|
-
<a href={url} target='_blank' rel='noreferrer' className={hover}>
|
222
|
-
{web.origin}
|
223
|
-
<ArrowSquareOut weight='bold' className={mx(getSize(4), 'inline-block leading-none mis-1')} />
|
224
|
-
</a>,
|
225
|
-
);
|
226
|
-
};
|
227
|
-
|
228
219
|
const Key: FC<{ char: string }> = ({ char }) => (
|
229
220
|
<span className='flex justify-center items-center w-[24px] h-[24px] rounded text-xs bg-neutral-200 text-black'>
|
230
221
|
{char}
|
@@ -244,11 +235,25 @@ const onCommentsHover: CommentsOptions['onHover'] = (el, shortcut) => {
|
|
244
235
|
);
|
245
236
|
};
|
246
237
|
|
238
|
+
const renderLinkTooltip = (el: Element, url: string) => {
|
239
|
+
const web = new URL(url);
|
240
|
+
createRoot(el).render(
|
241
|
+
<ThemeProvider tx={defaultTx}>
|
242
|
+
<a href={url} target='_blank' rel='noreferrer' className={mx(hover, 'flex items-center gap-2')}>
|
243
|
+
{web.origin}
|
244
|
+
<Icon icon='ph--arrow-square-out--regular' size={4} />
|
245
|
+
</a>
|
246
|
+
</ThemeProvider>,
|
247
|
+
);
|
248
|
+
};
|
249
|
+
|
247
250
|
const renderLinkButton = (el: Element, url: string) => {
|
248
251
|
createRoot(el).render(
|
249
|
-
<
|
250
|
-
<
|
251
|
-
|
252
|
+
<ThemeProvider tx={defaultTx}>
|
253
|
+
<a href={url} target='_blank' rel='noreferrer' className={mx(hover)}>
|
254
|
+
<Icon icon='ph--arrow-square-out--regular' size={4} classNames='inline-block mis-1 mb-[3px]' />
|
255
|
+
</a>
|
256
|
+
</ThemeProvider>,
|
252
257
|
);
|
253
258
|
};
|
254
259
|
|
@@ -262,7 +267,7 @@ type StoryProps = {
|
|
262
267
|
id?: string;
|
263
268
|
debug?: DebugMode;
|
264
269
|
text?: string;
|
265
|
-
|
270
|
+
readOnly?: boolean;
|
266
271
|
placeholder?: string;
|
267
272
|
lineNumbers?: boolean;
|
268
273
|
onReady?: (view: EditorView) => void;
|
@@ -273,7 +278,7 @@ const DefaultStory = ({
|
|
273
278
|
debug,
|
274
279
|
text,
|
275
280
|
extensions,
|
276
|
-
|
281
|
+
readOnly,
|
277
282
|
placeholder = 'New document.',
|
278
283
|
scrollTo,
|
279
284
|
selection,
|
@@ -289,7 +294,7 @@ const DefaultStory = ({
|
|
289
294
|
initialValue: text,
|
290
295
|
extensions: [
|
291
296
|
createDataExtensions({ id, text: createDocAccessor(object, ['content']) }),
|
292
|
-
createBasicExtensions({
|
297
|
+
createBasicExtensions({ readOnly, placeholder, lineNumbers, scrollPastEnd: true }),
|
293
298
|
createMarkdownExtensions({ themeMode }),
|
294
299
|
createThemeExtensions({
|
295
300
|
themeMode,
|
@@ -335,26 +340,22 @@ const DefaultStory = ({
|
|
335
340
|
);
|
336
341
|
};
|
337
342
|
|
338
|
-
|
343
|
+
const meta: Meta<typeof DefaultStory> = {
|
339
344
|
title: 'ui/react-ui-editor/TextEditor',
|
340
345
|
decorators: [withTheme, withLayout({ fullscreen: true })],
|
341
346
|
render: DefaultStory,
|
342
347
|
parameters: { translations, layout: 'fullscreen' },
|
343
348
|
};
|
344
349
|
|
350
|
+
export default meta;
|
351
|
+
|
345
352
|
const defaultExtensions: Extension[] = [
|
346
|
-
autocomplete({
|
347
|
-
onSearch: (text) => links.filter(({ label }) => label.toLowerCase().includes(text.toLowerCase())),
|
348
|
-
}),
|
349
353
|
decorateMarkdown({ renderLinkButton, selectionChangeDelay: 100 }),
|
350
354
|
formattingKeymap(),
|
351
355
|
linkTooltip(renderLinkTooltip),
|
352
356
|
];
|
353
357
|
|
354
358
|
const allExtensions: Extension[] = [
|
355
|
-
autocomplete({
|
356
|
-
onSearch: (text) => links.filter(({ label }) => label.toLowerCase().includes(text.toLowerCase())),
|
357
|
-
}),
|
358
359
|
decorateMarkdown({ numberedHeadings: { from: 2, to: 4 }, renderLinkButton, selectionChangeDelay: 100 }),
|
359
360
|
formattingKeymap(),
|
360
361
|
linkTooltip(renderLinkTooltip),
|
@@ -363,26 +364,50 @@ const allExtensions: Extension[] = [
|
|
363
364
|
folding(),
|
364
365
|
];
|
365
366
|
|
367
|
+
//
|
368
|
+
// Default
|
369
|
+
//
|
370
|
+
|
366
371
|
export const Default = {
|
367
372
|
render: () => <DefaultStory text={text} extensions={defaultExtensions} />,
|
368
373
|
};
|
369
374
|
|
375
|
+
//
|
376
|
+
// Everything
|
377
|
+
//
|
378
|
+
|
370
379
|
export const Everything = {
|
371
380
|
render: () => <DefaultStory text={text} extensions={allExtensions} selection={{ anchor: 99, head: 110 }} />,
|
372
381
|
};
|
373
382
|
|
383
|
+
//
|
384
|
+
// Empty
|
385
|
+
//
|
386
|
+
|
374
387
|
export const Empty = {
|
375
388
|
render: () => <DefaultStory extensions={defaultExtensions} />,
|
376
389
|
};
|
377
390
|
|
391
|
+
//
|
392
|
+
// Readonly
|
393
|
+
//
|
394
|
+
|
378
395
|
export const Readonly = {
|
379
|
-
render: () => <DefaultStory text={text} extensions={defaultExtensions}
|
396
|
+
render: () => <DefaultStory text={text} extensions={defaultExtensions} readOnly />,
|
380
397
|
};
|
381
398
|
|
399
|
+
//
|
400
|
+
// No Extensions
|
401
|
+
//
|
402
|
+
|
382
403
|
export const NoExtensions = {
|
383
404
|
render: () => <DefaultStory text={text} />,
|
384
405
|
};
|
385
406
|
|
407
|
+
//
|
408
|
+
// Vim
|
409
|
+
//
|
410
|
+
|
386
411
|
export const Vim = {
|
387
412
|
render: () => (
|
388
413
|
<DefaultStory
|
@@ -409,14 +434,22 @@ const headings = str(
|
|
409
434
|
.flat(),
|
410
435
|
);
|
411
436
|
|
412
|
-
const global
|
437
|
+
const global = new Map<string, EditorSelectionState>();
|
413
438
|
|
414
439
|
export const Folding = {
|
415
440
|
render: () => <DefaultStory text={text} extensions={[folding()]} />,
|
416
441
|
};
|
417
442
|
|
418
443
|
export const Scrolling = {
|
419
|
-
render: () =>
|
444
|
+
render: () => (
|
445
|
+
<DefaultStory
|
446
|
+
text={str('# Large Document', '', longText)}
|
447
|
+
extensions={selectionState({
|
448
|
+
setState: (id, state) => global.set(id, state),
|
449
|
+
getState: (id) => global.get(id),
|
450
|
+
})}
|
451
|
+
/>
|
452
|
+
),
|
420
453
|
};
|
421
454
|
|
422
455
|
export const ScrollingWithImages = {
|
@@ -503,6 +536,10 @@ export const Table = {
|
|
503
536
|
render: () => <DefaultStory text={str(content.table, content.footer)} extensions={[decorateMarkdown(), table()]} />,
|
504
537
|
};
|
505
538
|
|
539
|
+
//
|
540
|
+
// Commented out
|
541
|
+
//
|
542
|
+
|
506
543
|
export const CommentedOut = {
|
507
544
|
render: () => (
|
508
545
|
<DefaultStory
|
@@ -516,6 +553,10 @@ export const CommentedOut = {
|
|
516
553
|
),
|
517
554
|
};
|
518
555
|
|
556
|
+
//
|
557
|
+
// Typescript
|
558
|
+
//
|
559
|
+
|
519
560
|
export const Typescript = {
|
520
561
|
render: () => (
|
521
562
|
<DefaultStory
|
@@ -537,13 +578,19 @@ export const Autocomplete = {
|
|
537
578
|
extensions={[
|
538
579
|
decorateMarkdown({ renderLinkButton }),
|
539
580
|
autocomplete({
|
540
|
-
onSearch: (text) =>
|
581
|
+
onSearch: (text) => {
|
582
|
+
return links.filter(({ label }) => label.toLowerCase().includes(text.toLowerCase()));
|
583
|
+
},
|
541
584
|
}),
|
542
585
|
]}
|
543
586
|
/>
|
544
587
|
),
|
545
588
|
};
|
546
589
|
|
590
|
+
//
|
591
|
+
// Mention
|
592
|
+
//
|
593
|
+
|
547
594
|
export const Mention = {
|
548
595
|
render: () => (
|
549
596
|
<DefaultStory
|
@@ -557,6 +604,10 @@ export const Mention = {
|
|
557
604
|
),
|
558
605
|
};
|
559
606
|
|
607
|
+
//
|
608
|
+
// Search
|
609
|
+
//
|
610
|
+
|
560
611
|
export const Search = {
|
561
612
|
render: () => (
|
562
613
|
<DefaultStory
|
@@ -567,10 +618,65 @@ export const Search = {
|
|
567
618
|
),
|
568
619
|
};
|
569
620
|
|
621
|
+
//
|
622
|
+
// Command
|
623
|
+
//
|
624
|
+
|
625
|
+
export const Command = {
|
626
|
+
render: () => (
|
627
|
+
<DefaultStory
|
628
|
+
text={str('# Command', '', '', '[Test](dxn:queue:data:123)', '', '', '')}
|
629
|
+
extensions={[
|
630
|
+
command({
|
631
|
+
onHint: () => 'Press / for commands.',
|
632
|
+
onRenderMenu: (el, onClick) => {
|
633
|
+
renderRoot(
|
634
|
+
el,
|
635
|
+
<ThemeProvider tx={defaultTx}>
|
636
|
+
<Button classNames='p-1 aspect-square' onClick={onClick}>
|
637
|
+
<Icon icon={'ph--sparkle--regular'} size={5} />
|
638
|
+
</Button>
|
639
|
+
</ThemeProvider>,
|
640
|
+
);
|
641
|
+
},
|
642
|
+
onRenderDialog: (el, onClose) => {
|
643
|
+
renderRoot(el, <CommandDialog onClose={onClose} />);
|
644
|
+
},
|
645
|
+
onRenderPreview: (el, { text }) => {
|
646
|
+
faker.seed(text.length);
|
647
|
+
const data = Array.from({ length: 2 }, () => faker.lorem.sentences(2));
|
648
|
+
renderRoot(
|
649
|
+
el,
|
650
|
+
<ThemeProvider tx={defaultTx}>
|
651
|
+
<div className='flex flex-col gap-2'>
|
652
|
+
<div className='flex items-center gap-4'>
|
653
|
+
<div className='grow truncate'>
|
654
|
+
<span className='text-xs text-subdued mie-2'>Prompt</span>
|
655
|
+
{text}
|
656
|
+
</div>
|
657
|
+
<div className='flex gap-1'>
|
658
|
+
<IconButton classNames='text-green-500' label='Apply' icon={'ph--check--regular'} />
|
659
|
+
<IconButton classNames='text-red-500' label='Cancel' icon={'ph--x--regular'} />
|
660
|
+
</div>
|
661
|
+
</div>
|
662
|
+
<div>{data.join('\n\n')}</div>
|
663
|
+
</div>
|
664
|
+
</ThemeProvider>,
|
665
|
+
);
|
666
|
+
},
|
667
|
+
}),
|
668
|
+
]}
|
669
|
+
/>
|
670
|
+
),
|
671
|
+
};
|
672
|
+
|
570
673
|
const CommandDialog = ({ onClose }: { onClose: (action?: CommandAction) => void }) => {
|
571
674
|
const [text, setText] = useState('');
|
572
675
|
const handleInsert = () => {
|
573
|
-
|
676
|
+
// TODO(burdon): Use queue ref.
|
677
|
+
const link = `[${text}](dxn:queue:data:123)`;
|
678
|
+
console.log({ link });
|
679
|
+
onClose(text.length ? { insert: link } : undefined);
|
574
680
|
};
|
575
681
|
|
576
682
|
const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
|
@@ -587,38 +693,34 @@ const CommandDialog = ({ onClose }: { onClose: (action?: CommandAction) => void
|
|
587
693
|
};
|
588
694
|
|
589
695
|
return (
|
590
|
-
<div className=
|
591
|
-
<
|
592
|
-
|
593
|
-
|
594
|
-
|
595
|
-
|
596
|
-
|
597
|
-
|
598
|
-
|
599
|
-
|
600
|
-
|
601
|
-
|
602
|
-
|
696
|
+
<div className='flex w-full justify-center'>
|
697
|
+
<div
|
698
|
+
className={mx(
|
699
|
+
'flex w-full p-2 gap-2 items-center border border-separator rounded-md',
|
700
|
+
editorContent,
|
701
|
+
baseSurface,
|
702
|
+
)}
|
703
|
+
>
|
704
|
+
<Input.Root>
|
705
|
+
<Input.TextInput
|
706
|
+
autoFocus={true}
|
707
|
+
placeholder='Ask a question...'
|
708
|
+
value={text}
|
709
|
+
onChange={({ target: { value } }) => setText(value)}
|
710
|
+
onKeyDown={handleKeyDown}
|
711
|
+
/>
|
712
|
+
</Input.Root>
|
713
|
+
<Button variant='ghost' classNames='pli-0' onClick={() => onClose()}>
|
714
|
+
<X className={getSize(5)} />
|
715
|
+
</Button>
|
716
|
+
</div>
|
603
717
|
</div>
|
604
718
|
);
|
605
719
|
};
|
606
720
|
|
607
|
-
|
608
|
-
|
609
|
-
|
610
|
-
text={str('# Command', '')}
|
611
|
-
extensions={[
|
612
|
-
command({
|
613
|
-
onRender: (el, onClose) => {
|
614
|
-
renderRoot(el, <CommandDialog onClose={onClose} />);
|
615
|
-
},
|
616
|
-
onHint: () => 'Press / for commands.',
|
617
|
-
}),
|
618
|
-
]}
|
619
|
-
/>
|
620
|
-
),
|
621
|
-
};
|
721
|
+
//
|
722
|
+
// Comments
|
723
|
+
//
|
622
724
|
|
623
725
|
export const Comments = {
|
624
726
|
render: () => {
|
@@ -660,12 +762,20 @@ export const Comments = {
|
|
660
762
|
},
|
661
763
|
};
|
662
764
|
|
765
|
+
//
|
766
|
+
// Annotations
|
767
|
+
//
|
768
|
+
|
663
769
|
export const Annotations = {
|
664
770
|
render: () => (
|
665
771
|
<DefaultStory text={str('# Annotations', '', longText)} extensions={[annotations({ match: /volup/gi })]} />
|
666
772
|
),
|
667
773
|
};
|
668
774
|
|
775
|
+
//
|
776
|
+
// DND
|
777
|
+
//
|
778
|
+
|
669
779
|
export const DND = {
|
670
780
|
render: () => (
|
671
781
|
<DefaultStory
|
@@ -681,6 +791,10 @@ export const DND = {
|
|
681
791
|
),
|
682
792
|
};
|
683
793
|
|
794
|
+
//
|
795
|
+
// Listener
|
796
|
+
//
|
797
|
+
|
684
798
|
export const Listener = {
|
685
799
|
render: () => (
|
686
800
|
<DefaultStory
|
@@ -699,6 +813,10 @@ export const Listener = {
|
|
699
813
|
),
|
700
814
|
};
|
701
815
|
|
816
|
+
//
|
817
|
+
// Typewriter
|
818
|
+
//
|
819
|
+
|
702
820
|
const typewriterItems = localStorage.getItem('dxos.org/plugin/markdown/typewriter')?.split(',');
|
703
821
|
|
704
822
|
export const Typewriter = {
|
@@ -710,6 +828,10 @@ export const Typewriter = {
|
|
710
828
|
),
|
711
829
|
};
|
712
830
|
|
831
|
+
//
|
832
|
+
// Blast
|
833
|
+
//
|
834
|
+
|
713
835
|
export const Blast = {
|
714
836
|
render: () => (
|
715
837
|
<DefaultStory
|
@@ -7,11 +7,11 @@ import React, { useCallback } from 'react';
|
|
7
7
|
import { type NodeArg } from '@dxos/app-graph';
|
8
8
|
import { ElevationProvider } from '@dxos/react-ui';
|
9
9
|
import {
|
10
|
-
ToolbarMenu,
|
11
|
-
MenuProvider,
|
12
10
|
type MenuActionHandler,
|
13
|
-
|
11
|
+
MenuProvider,
|
12
|
+
ToolbarMenu,
|
14
13
|
createGapSeparator,
|
14
|
+
useMenuActions,
|
15
15
|
} from '@dxos/react-ui-menu';
|
16
16
|
import { textBlockWidth } from '@dxos/react-ui-theme';
|
17
17
|
|
@@ -27,7 +27,7 @@ import {
|
|
27
27
|
editorToolbarSearch,
|
28
28
|
} from './util';
|
29
29
|
import { createViewMode } from './viewMode';
|
30
|
-
import { stackItemContentToolbarClassNames } from '../../
|
30
|
+
import { stackItemContentToolbarClassNames } from '../../fragments';
|
31
31
|
|
32
32
|
const createToolbar = ({
|
33
33
|
state,
|
@@ -86,7 +86,6 @@ const createToolbar = ({
|
|
86
86
|
|
87
87
|
const useEditorToolbarActionGraph = ({ onAction, ...props }: EditorToolbarProps) => {
|
88
88
|
const menuCreator = useCallback(() => createToolbar(props), [props]);
|
89
|
-
|
90
89
|
const { resolveGroupItems } = useMenuActions(menuCreator);
|
91
90
|
|
92
91
|
return { resolveGroupItems, onAction: onAction as MenuActionHandler };
|
package/src/defaults.ts
CHANGED
@@ -41,3 +41,15 @@ export const editorMonospace = EditorView.theme({
|
|
41
41
|
|
42
42
|
export const editorWithToolbarLayout =
|
43
43
|
'grid grid-cols-1 grid-rows-[min-content_1fr] data-[toolbar=disabled]:grid-rows-[1fr] justify-center content-start overflow-hidden';
|
44
|
+
|
45
|
+
export const stackItemContentEditorClassNames = (role?: string) =>
|
46
|
+
mx(
|
47
|
+
'attention-surface dx-focus-ring-inset data-[toolbar=disabled]:pbs-2',
|
48
|
+
role === 'section' ? '[&_.cm-scroller]:overflow-hidden [&_.cm-scroller]:min-bs-24' : 'min-bs-0',
|
49
|
+
);
|
50
|
+
|
51
|
+
export const stackItemContentToolbarClassNames = (role?: string) =>
|
52
|
+
mx(
|
53
|
+
'attention-surface is-full border-be !border-separator',
|
54
|
+
role === 'section' && 'sticky block-start-0 z-[1] -mbe-px min-is-0',
|
55
|
+
);
|
@@ -6,6 +6,8 @@ import { type Extension } from '@codemirror/state';
|
|
6
6
|
import { EditorView, keymap } from '@codemirror/view';
|
7
7
|
|
8
8
|
import { hintViewPlugin } from './hint';
|
9
|
+
import { floatingMenu } from './menu';
|
10
|
+
import { preview, type PreviewOptions } from './preview';
|
9
11
|
import { closeEffect, commandConfig, commandKeyBindings, commandState } from './state';
|
10
12
|
|
11
13
|
// TODO(burdon): Create knowledge base for CM notes and ideas.
|
@@ -13,23 +15,40 @@ import { closeEffect, commandConfig, commandKeyBindings, commandState } from './
|
|
13
15
|
// https://github.com/saminzadeh/codemirror-extension-inline-suggestion
|
14
16
|
// https://github.com/ChromeDevTools/devtools-frontend/blob/main/front_end/ui/components/text_editor/config.ts#L370
|
15
17
|
|
18
|
+
// TODO(burdon): Discriminated union.
|
16
19
|
export type CommandAction = {
|
17
20
|
insert?: string;
|
18
21
|
};
|
19
22
|
|
20
23
|
export type CommandOptions = {
|
21
|
-
onRender: (el: HTMLElement, cb: (action?: CommandAction) => void) => void;
|
22
24
|
onHint: () => string | undefined;
|
23
|
-
|
25
|
+
onRenderDialog: (el: HTMLElement, cb: (action?: CommandAction) => void) => void;
|
26
|
+
onRenderMenu: (el: HTMLElement, cb: () => void) => void;
|
27
|
+
} & Pick<PreviewOptions, 'onRenderPreview'>;
|
24
28
|
|
25
29
|
export const command = (options: CommandOptions): Extension => {
|
26
30
|
return [
|
27
31
|
commandConfig.of(options),
|
28
32
|
commandState,
|
29
33
|
keymap.of(commandKeyBindings),
|
34
|
+
preview(options),
|
35
|
+
floatingMenu(options),
|
30
36
|
hintViewPlugin(options),
|
31
37
|
EditorView.focusChangeEffect.of((_, focusing) => {
|
32
38
|
return focusing ? closeEffect.of(null) : null;
|
33
39
|
}),
|
40
|
+
EditorView.theme({
|
41
|
+
'.cm-tooltip': {
|
42
|
+
background: 'transparent',
|
43
|
+
},
|
44
|
+
'.cm-preview': {
|
45
|
+
marginLeft: '-1rem',
|
46
|
+
marginRight: '-1rem',
|
47
|
+
padding: '1rem',
|
48
|
+
borderRadius: '1rem',
|
49
|
+
background: 'var(--dx-modalSurface)',
|
50
|
+
border: '1px solid var(--dx-separator)',
|
51
|
+
},
|
52
|
+
}),
|
34
53
|
];
|
35
54
|
};
|
@@ -24,6 +24,7 @@ class CommandHint extends WidgetType {
|
|
24
24
|
} else {
|
25
25
|
wrap.setAttribute('aria-hidden', 'true');
|
26
26
|
}
|
27
|
+
|
27
28
|
return wrap;
|
28
29
|
}
|
29
30
|
|
@@ -32,12 +33,14 @@ class CommandHint extends WidgetType {
|
|
32
33
|
if (!rects.length) {
|
33
34
|
return null;
|
34
35
|
}
|
36
|
+
|
35
37
|
const style = window.getComputedStyle(dom.parentNode as HTMLElement);
|
36
38
|
const rect = flattenRect(rects[0], style.direction !== 'rtl');
|
37
39
|
const lineHeight = parseInt(style.lineHeight);
|
38
40
|
if (rect.bottom - rect.top > lineHeight * 1.5) {
|
39
41
|
return { left: rect.left, right: rect.right, top: rect.top, bottom: rect.top + lineHeight };
|
40
42
|
}
|
43
|
+
|
41
44
|
return rect;
|
42
45
|
}
|
43
46
|
|
@@ -0,0 +1,100 @@
|
|
1
|
+
//
|
2
|
+
// Copyright 2024 DXOS.org
|
3
|
+
//
|
4
|
+
|
5
|
+
import { type BlockInfo, type EditorView, ViewPlugin, type ViewUpdate } from '@codemirror/view';
|
6
|
+
|
7
|
+
import { type CommandOptions } from './command';
|
8
|
+
import { closeEffect, openCommand, openEffect } from './state';
|
9
|
+
|
10
|
+
// TODO(burdon): Trigger completion on click.
|
11
|
+
// TODO(burdon): Hide when dialog is open.
|
12
|
+
export const floatingMenu = (options: CommandOptions) =>
|
13
|
+
ViewPlugin.fromClass(
|
14
|
+
class {
|
15
|
+
button: HTMLElement;
|
16
|
+
view: EditorView;
|
17
|
+
rafId: number | null = null;
|
18
|
+
|
19
|
+
constructor(view: EditorView) {
|
20
|
+
this.view = view;
|
21
|
+
|
22
|
+
// Position context: scrollDOM
|
23
|
+
const container = view.scrollDOM;
|
24
|
+
if (getComputedStyle(container).position === 'static') {
|
25
|
+
container.style.position = 'relative';
|
26
|
+
}
|
27
|
+
|
28
|
+
// Render menu externally.
|
29
|
+
this.button = document.createElement('div');
|
30
|
+
this.button.style.position = 'absolute';
|
31
|
+
this.button.style.zIndex = '10';
|
32
|
+
this.button.style.display = 'none';
|
33
|
+
|
34
|
+
options.onRenderMenu(this.button, () => {
|
35
|
+
openCommand(view);
|
36
|
+
});
|
37
|
+
container.appendChild(this.button);
|
38
|
+
|
39
|
+
// Listen for scroll events.
|
40
|
+
container.addEventListener('scroll', this.scheduleUpdate);
|
41
|
+
this.scheduleUpdate();
|
42
|
+
}
|
43
|
+
|
44
|
+
update(update: ViewUpdate) {
|
45
|
+
// TODO(burdon): Timer to fade in/out.
|
46
|
+
if (update.transactions.some((tr) => tr.effects.some((effect) => effect.is(openEffect)))) {
|
47
|
+
this.button.style.display = 'none';
|
48
|
+
} else if (update.transactions.some((tr) => tr.effects.some((effect) => effect.is(closeEffect)))) {
|
49
|
+
this.button.style.display = 'block';
|
50
|
+
} else if (update.selectionSet || update.viewportChanged || update.docChanged || update.geometryChanged) {
|
51
|
+
this.scheduleUpdate();
|
52
|
+
}
|
53
|
+
}
|
54
|
+
|
55
|
+
scheduleUpdate() {
|
56
|
+
if (this.rafId != null) {
|
57
|
+
cancelAnimationFrame(this.rafId);
|
58
|
+
}
|
59
|
+
this.rafId = requestAnimationFrame(() => this.updateButtonPosition());
|
60
|
+
}
|
61
|
+
|
62
|
+
updateButtonPosition() {
|
63
|
+
const pos = this.view.state.selection.main.head;
|
64
|
+
const lineBlock: BlockInfo = this.view.lineBlockAt(pos);
|
65
|
+
const domInfo = this.view.domAtPos(lineBlock.from);
|
66
|
+
|
67
|
+
// Find nearest HTMLElement for the line block
|
68
|
+
let node: Node | null = domInfo.node;
|
69
|
+
while (node && !(node instanceof HTMLElement)) {
|
70
|
+
node = node.parentNode;
|
71
|
+
}
|
72
|
+
|
73
|
+
if (!node) {
|
74
|
+
this.button.style.display = 'none';
|
75
|
+
return;
|
76
|
+
}
|
77
|
+
|
78
|
+
const lineRect = (node as HTMLElement).getBoundingClientRect();
|
79
|
+
const containerRect = this.view.scrollDOM.getBoundingClientRect();
|
80
|
+
|
81
|
+
// Account for scroll and padding/margin in scrollDOM.
|
82
|
+
const offsetTop = lineRect.top - containerRect.top + this.view.scrollDOM.scrollTop;
|
83
|
+
const offsetLeft = this.view.scrollDOM.clientWidth + this.view.scrollDOM.scrollLeft - lineRect.x;
|
84
|
+
|
85
|
+
// TODO(burdon): Position is incorrect if cursor is in fenced code block.
|
86
|
+
// console.log('offsetTop', lineRect, containerRect);
|
87
|
+
|
88
|
+
this.button.style.top = `${offsetTop}px`;
|
89
|
+
this.button.style.left = `${offsetLeft}px`;
|
90
|
+
this.button.style.display = 'block';
|
91
|
+
}
|
92
|
+
|
93
|
+
destroy() {
|
94
|
+
this.button.remove();
|
95
|
+
if (this.rafId != null) {
|
96
|
+
cancelAnimationFrame(this.rafId);
|
97
|
+
}
|
98
|
+
}
|
99
|
+
},
|
100
|
+
);
|