@dxos/react-ui-markdown 0.8.4-main.03d5cd7b56

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 (45) hide show
  1. package/LICENSE +8 -0
  2. package/README.md +1 -0
  3. package/dist/lib/browser/index.mjs +608 -0
  4. package/dist/lib/browser/index.mjs.map +7 -0
  5. package/dist/lib/browser/meta.json +1 -0
  6. package/dist/types/src/MarkdownBlock/MarkdownBlock.d.ts +15 -0
  7. package/dist/types/src/MarkdownBlock/MarkdownBlock.d.ts.map +1 -0
  8. package/dist/types/src/MarkdownBlock/MarkdownBlock.stories.d.ts +11 -0
  9. package/dist/types/src/MarkdownBlock/MarkdownBlock.stories.d.ts.map +1 -0
  10. package/dist/types/src/MarkdownBlock/index.d.ts +2 -0
  11. package/dist/types/src/MarkdownBlock/index.d.ts.map +1 -0
  12. package/dist/types/src/MarkdownStream/MarkdownStream.d.ts +101 -0
  13. package/dist/types/src/MarkdownStream/MarkdownStream.d.ts.map +1 -0
  14. package/dist/types/src/MarkdownStream/MarkdownStream.stories.d.ts +23 -0
  15. package/dist/types/src/MarkdownStream/MarkdownStream.stories.d.ts.map +1 -0
  16. package/dist/types/src/MarkdownStream/footer.d.ts +23 -0
  17. package/dist/types/src/MarkdownStream/footer.d.ts.map +1 -0
  18. package/dist/types/src/MarkdownStream/index.d.ts +4 -0
  19. package/dist/types/src/MarkdownStream/index.d.ts.map +1 -0
  20. package/dist/types/src/MarkdownStream/stream.d.ts +39 -0
  21. package/dist/types/src/MarkdownStream/stream.d.ts.map +1 -0
  22. package/dist/types/src/MarkdownStream/stream.test.d.ts +2 -0
  23. package/dist/types/src/MarkdownStream/stream.test.d.ts.map +1 -0
  24. package/dist/types/src/MarkdownStream/testing/index.d.ts +2 -0
  25. package/dist/types/src/MarkdownStream/testing/index.d.ts.map +1 -0
  26. package/dist/types/src/MarkdownStream/testing/testing.d.ts +16 -0
  27. package/dist/types/src/MarkdownStream/testing/testing.d.ts.map +1 -0
  28. package/dist/types/src/index.d.ts +3 -0
  29. package/dist/types/src/index.d.ts.map +1 -0
  30. package/dist/types/tsconfig.tsbuildinfo +1 -0
  31. package/package.json +82 -0
  32. package/src/MarkdownBlock/MarkdownBlock.stories.tsx +75 -0
  33. package/src/MarkdownBlock/MarkdownBlock.tsx +100 -0
  34. package/src/MarkdownBlock/index.ts +5 -0
  35. package/src/MarkdownStream/MarkdownStream.stories.tsx +215 -0
  36. package/src/MarkdownStream/MarkdownStream.tsx +444 -0
  37. package/src/MarkdownStream/footer.ts +119 -0
  38. package/src/MarkdownStream/index.ts +8 -0
  39. package/src/MarkdownStream/stream.test.ts +126 -0
  40. package/src/MarkdownStream/stream.ts +229 -0
  41. package/src/MarkdownStream/testing/index.ts +5 -0
  42. package/src/MarkdownStream/testing/testing.ts +56 -0
  43. package/src/MarkdownStream/testing/text.md +67 -0
  44. package/src/index.ts +6 -0
  45. package/src/typings.d.ts +8 -0
package/package.json ADDED
@@ -0,0 +1,82 @@
1
+ {
2
+ "name": "@dxos/react-ui-markdown",
3
+ "version": "0.8.4-main.03d5cd7b56",
4
+ "description": "Markdown components.",
5
+ "homepage": "https://dxos.org",
6
+ "bugs": "https://github.com/dxos/dxos/issues",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/dxos/dxos"
10
+ },
11
+ "license": "MIT",
12
+ "author": "DXOS.org",
13
+ "type": "module",
14
+ "exports": {
15
+ ".": {
16
+ "source": "./src/index.ts",
17
+ "types": "./dist/types/src/index.d.ts",
18
+ "browser": "./dist/lib/browser/index.mjs"
19
+ }
20
+ },
21
+ "types": "dist/types/src/index.d.ts",
22
+ "files": [
23
+ "dist",
24
+ "src"
25
+ ],
26
+ "dependencies": {
27
+ "@codemirror/autocomplete": "^6.19.0",
28
+ "@codemirror/language": "^6.11.3",
29
+ "@codemirror/state": "^6.5.2",
30
+ "@codemirror/view": "^6.38.5",
31
+ "@lezer/common": "^1.2.2",
32
+ "@lezer/highlight": "^1.2.1",
33
+ "@lezer/markdown": "^1.3.1",
34
+ "@lezer/xml": "^1.0.6",
35
+ "@radix-ui/react-compose-refs": "1.1.1",
36
+ "@radix-ui/react-context": "1.1.1",
37
+ "effect": "3.20.0",
38
+ "json5": "^2.2.3",
39
+ "react-markdown": "^10.1.0",
40
+ "react-resize-detector": "^11.0.1",
41
+ "remark-gfm": "^4.0.1",
42
+ "@dxos/async": "0.8.4-main.03d5cd7b56",
43
+ "@dxos/echo": "0.8.4-main.03d5cd7b56",
44
+ "@dxos/effect": "0.8.4-main.03d5cd7b56",
45
+ "@dxos/invariant": "0.8.4-main.03d5cd7b56",
46
+ "@dxos/log": "0.8.4-main.03d5cd7b56",
47
+ "@dxos/react-ui": "0.8.4-main.03d5cd7b56",
48
+ "@dxos/react-ui-editor": "0.8.4-main.03d5cd7b56",
49
+ "@dxos/ui": "0.8.4-main.03d5cd7b56",
50
+ "@dxos/ui-editor": "0.8.4-main.03d5cd7b56",
51
+ "@dxos/ui-theme": "0.8.4-main.03d5cd7b56",
52
+ "@dxos/react-ui-syntax-highlighter": "0.8.4-main.03d5cd7b56",
53
+ "@dxos/util": "0.8.4-main.03d5cd7b56"
54
+ },
55
+ "devDependencies": {
56
+ "@effect/vitest": "0.29.0",
57
+ "@types/react": "~19.2.7",
58
+ "@types/react-dom": "~19.2.3",
59
+ "react": "~19.2.3",
60
+ "react-dom": "~19.2.3",
61
+ "vite": "^8.0.10",
62
+ "@dxos/echo": "0.8.4-main.03d5cd7b56",
63
+ "@dxos/keys": "0.8.4-main.03d5cd7b56",
64
+ "@dxos/random": "0.8.4-main.03d5cd7b56",
65
+ "@dxos/lit-ui": "0.8.4-main.03d5cd7b56",
66
+ "@dxos/react-ui": "0.8.4-main.03d5cd7b56",
67
+ "@dxos/react-client": "0.8.4-main.03d5cd7b56",
68
+ "@dxos/schema": "0.8.4-main.03d5cd7b56",
69
+ "@dxos/ui-theme": "0.8.4-main.03d5cd7b56",
70
+ "@dxos/storybook-utils": "0.8.4-main.03d5cd7b56"
71
+ },
72
+ "peerDependencies": {
73
+ "effect": "3.20.0",
74
+ "react": "~19.2.3",
75
+ "react-dom": "~19.2.3",
76
+ "@dxos/ui-theme": "0.8.4-main.03d5cd7b56",
77
+ "@dxos/react-ui": "0.8.4-main.03d5cd7b56"
78
+ },
79
+ "publishConfig": {
80
+ "access": "public"
81
+ }
82
+ }
@@ -0,0 +1,75 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { type Meta, type StoryObj } from '@storybook/react-vite';
6
+
7
+ import { random } from '@dxos/random';
8
+ import { withLayout, withTheme } from '@dxos/react-ui/testing';
9
+ import { trim } from '@dxos/util';
10
+
11
+ import { MarkdownBlock } from './MarkdownBlock';
12
+
13
+ random.seed(0);
14
+
15
+ const meta = {
16
+ title: 'ui/react-ui-markdown/MarkdownBlock',
17
+ component: MarkdownBlock,
18
+ decorators: [withTheme(), withLayout({ layout: 'column' })],
19
+ } satisfies Meta<typeof MarkdownBlock>;
20
+
21
+ export default meta;
22
+
23
+ type Story = StoryObj<typeof MarkdownBlock>;
24
+
25
+ const content = trim`
26
+ # Hello World!
27
+
28
+ > An example of the MarkdownBlock component.
29
+
30
+ ${random.lorem.paragraphs(1)}
31
+
32
+ Here's a JSON block:
33
+
34
+ ~~~json
35
+ {
36
+ "hello": "world"
37
+ }
38
+ ~~~
39
+
40
+ And some code:
41
+
42
+ ~~~tsx
43
+ import React from 'react'
44
+
45
+ const App = () => {
46
+ const title = 'Hello, world!'
47
+ return <div>{title}</div>
48
+ }
49
+ ~~~
50
+
51
+ ## Task lists
52
+
53
+ - [ ] Task one
54
+ - [x] Task two
55
+ - [ ] Task three
56
+
57
+ ## Tables
58
+
59
+ | Column 1 | Column 2 | Column 3 |
60
+ | -------- | -------- | -------- |
61
+ | Cell 1 | Cell 2 | Cell 3 |
62
+ | Cell 4 | Cell 5 | Cell 6 |
63
+ | Cell 7 | Cell 8 | Cell 9 |
64
+
65
+ ## Examples
66
+
67
+ ${random.lorem.paragraphs(1)}
68
+ `;
69
+
70
+ export const Default: Story = {
71
+ args: {
72
+ classNames: 'p-4 border border-border rounded-md overflow-y-auto bg-base-surface',
73
+ content,
74
+ },
75
+ };
@@ -0,0 +1,100 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import React, { type PropsWithChildren } from 'react';
6
+ import ReactMarkdown, { type Options as ReactMarkdownOptions } from 'react-markdown';
7
+ import remarkGfm from 'remark-gfm';
8
+
9
+ import { type ThemedClassName } from '@dxos/react-ui';
10
+ import { SyntaxHighlighter } from '@dxos/react-ui-syntax-highlighter';
11
+ import { mx } from '@dxos/ui-theme';
12
+
13
+ export type MarkdownBlockProps = ThemedClassName<
14
+ PropsWithChildren<{
15
+ content?: string;
16
+ components?: ReactMarkdownOptions['components'];
17
+ }>
18
+ >;
19
+
20
+ /**
21
+ * Transforms markdown text into react elements.
22
+ * https://github.com/remarkjs/react-markdown
23
+ * markdown -> remark -> [mdast -> remark plugins] -> [hast -> rehype plugins] -> components -> react elements.
24
+ * Consider using @dxos/react-ui-editor.
25
+ */
26
+ export const MarkdownBlock = ({ classNames, children, components, content = '' }: MarkdownBlockProps) => {
27
+ return (
28
+ <div className={mx(classNames)}>
29
+ <ReactMarkdown remarkPlugins={[remarkGfm]} skipHtml components={{ ...defaultComponents, ...components }}>
30
+ {content}
31
+ </ReactMarkdown>
32
+ {children}
33
+ </div>
34
+ );
35
+ };
36
+
37
+ const defaultComponents: ReactMarkdownOptions['components'] = {
38
+ h1: ({ children }) => {
39
+ return <h1 className='pt-1 pb-1 text-accent-text text-xl'>{children}</h1>;
40
+ },
41
+ h2: ({ children }) => {
42
+ return <h2 className='pt-1 pb-1 text-accent-text text-lg'>{children}</h2>;
43
+ },
44
+ h3: ({ children }) => {
45
+ return <h3 className='pt-1 pb-1 text-accent-text text-base'>{children}</h3>;
46
+ },
47
+ blockquote: ({ children, ...props }) => (
48
+ <blockquote className='my-2 py-2 ps-4 border-l-4 border-accent-text text-accent-text' {...props}>
49
+ {children}
50
+ </blockquote>
51
+ ),
52
+ p: ({ children }) => {
53
+ return <div className='pt-1 pb-1'>{children}</div>;
54
+ },
55
+ a: ({ children, href, ...props }) => (
56
+ <a
57
+ href={href}
58
+ className='text-primary-500 hover:text-primary-500' // TODO(burdon): Use link token.
59
+ target='_blank'
60
+ rel='noopener noreferrer'
61
+ {...props}
62
+ >
63
+ {children}
64
+ </a>
65
+ ),
66
+ ol: ({ children, ...props }) => (
67
+ <ol className='pt-1 pb-1 ps-6 leading-tight list-decimal' {...props}>
68
+ {children}
69
+ </ol>
70
+ ),
71
+ ul: ({ children, ...props }) => (
72
+ <ul className='pt-1 pb-1 ps-6 leading-tight list-disc' {...props}>
73
+ {children}
74
+ </ul>
75
+ ),
76
+ li: ({ children, ...props }) => (
77
+ <li className='' {...props}>
78
+ {children}
79
+ </li>
80
+ ),
81
+ pre: ({ children }) => children,
82
+ code: ({ children, className, node }) => {
83
+ const [, language] = /language-(\w+)/.exec(className || '') || [];
84
+ const inline = !className && node?.position?.start.line === node?.position?.end.line;
85
+ if (inline) {
86
+ return <code className='rounded-xs bg-group-surface px-1 py-0.5 text-sm text-info-text'>{children}</code>;
87
+ }
88
+
89
+ return (
90
+ <SyntaxHighlighter
91
+ language={language}
92
+ classNames='mt-2 mb-2 p-2 border border-separator rounded-xs text-sm bg-group-surface'
93
+ copyButton
94
+ PreTag='pre'
95
+ >
96
+ {children}
97
+ </SyntaxHighlighter>
98
+ );
99
+ },
100
+ };
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ export * from './MarkdownBlock';
@@ -0,0 +1,215 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { WidgetType } from '@codemirror/view';
6
+ import { type Meta, type StoryObj } from '@storybook/react-vite';
7
+ import React, { useCallback, useEffect, useState } from 'react';
8
+
9
+ import '@dxos/lit-ui';
10
+ import { PublicKey } from '@dxos/keys';
11
+ import { random } from '@dxos/random';
12
+ import { Input, Toolbar } from '@dxos/react-ui';
13
+ import { Panel } from '@dxos/react-ui';
14
+ import { withLayout, withTheme } from '@dxos/react-ui/testing';
15
+ import { Domino } from '@dxos/ui';
16
+ import { type XmlWidgetRegistry, getXmlTextChild } from '@dxos/ui-editor';
17
+ import { mx } from '@dxos/ui-theme';
18
+ import { keyToFallback, trim } from '@dxos/util';
19
+
20
+ import { MarkdownStream, type MarkdownStreamController, type MarkdownStreamProps } from './MarkdownStream';
21
+ import { type TextStreamOptions, textStream } from './testing';
22
+ import TEXT from './testing/text.md?raw';
23
+
24
+ random.seed(123);
25
+
26
+ const userHue = keyToFallback(PublicKey.random()).hue;
27
+
28
+ const defaultStreamOptions: TextStreamOptions = {
29
+ wordsPerChunk: 5,
30
+ chunkDelay: 200,
31
+ variance: 0.5,
32
+ };
33
+
34
+ class DOMWidget extends WidgetType {
35
+ constructor(private text: string) {
36
+ super();
37
+ }
38
+
39
+ override eq(other: this) {
40
+ return this.text === other.text;
41
+ }
42
+
43
+ override toDOM() {
44
+ return Domino.of('span').classNames(mx('flex m-2 p-2 border border-separator rounded')).text(this.text).root;
45
+ }
46
+ }
47
+
48
+ const ReactWidget = ({ children }: { children: string }) => {
49
+ return <div className='m-2 p-2 border border-separator rounded'>{children}</div>;
50
+ };
51
+
52
+ const registry: XmlWidgetRegistry = {
53
+ 'dom-widget': {
54
+ block: true,
55
+ streaming: true,
56
+ factory: ({ children }) => {
57
+ const text = getXmlTextChild(children);
58
+ return text ? new DOMWidget(text) : null;
59
+ },
60
+ },
61
+
62
+ 'react-widget': {
63
+ block: true,
64
+ streaming: true,
65
+ Component: ReactWidget,
66
+ },
67
+ };
68
+
69
+ type DefaultStoryProps = MarkdownStreamProps & {
70
+ initialContent?: string;
71
+ streamOptions?: TextStreamOptions;
72
+ };
73
+
74
+ const DefaultStory = ({
75
+ initialContent,
76
+ content,
77
+ streamOptions = defaultStreamOptions,
78
+ debug: debugProp,
79
+ ...props
80
+ }: DefaultStoryProps) => {
81
+ const [controller, setController] = useState<MarkdownStreamController | null>(null);
82
+ const [streaming, setStreaming] = useState(false);
83
+ const [debug, setDebug] = useState(debugProp);
84
+
85
+ useEffect(() => {
86
+ if (initialContent) {
87
+ void controller?.append(initialContent);
88
+ }
89
+ }, [controller, initialContent]);
90
+
91
+ useEffect(() => {
92
+ if (!controller || !streaming || !content) {
93
+ return;
94
+ }
95
+
96
+ let cancelled = false;
97
+ void (async () => {
98
+ for await (const chunk of textStream(content + '\n', streamOptions)) {
99
+ if (cancelled) {
100
+ break;
101
+ }
102
+
103
+ await controller.append(chunk);
104
+ }
105
+
106
+ setStreaming(false);
107
+ })();
108
+
109
+ return () => {
110
+ cancelled = true;
111
+ };
112
+ }, [controller, content, streaming]);
113
+
114
+ const handleReset = useCallback(() => {
115
+ setStreaming(false);
116
+ void controller?.setContent('');
117
+ }, [controller]);
118
+
119
+ const handleAppend = useCallback(() => {
120
+ void controller?.append(
121
+ [
122
+ random.lorem.paragraph(),
123
+ `<dom-widget>${random.lorem.paragraphs(3)}</dom-widget>`,
124
+ random.lorem.paragraph(),
125
+ `<react-widget>${random.lorem.paragraphs(3)}</react-widget>`,
126
+ '',
127
+ ].join('\n\n'),
128
+ );
129
+ }, [controller]);
130
+
131
+ return (
132
+ <Panel.Root data-hue={userHue}>
133
+ <Panel.Toolbar asChild>
134
+ <Toolbar.Root>
135
+ <Toolbar.IconButton
136
+ disabled={streaming}
137
+ icon='ph--play--regular'
138
+ iconOnly
139
+ label='Start'
140
+ onClick={() => setStreaming(true)}
141
+ />
142
+ <Toolbar.IconButton
143
+ disabled={!streaming}
144
+ icon='ph--stop--regular'
145
+ iconOnly
146
+ label='Stop'
147
+ onClick={() => setStreaming(false)}
148
+ />
149
+ <Toolbar.IconButton icon='ph--trash--regular' iconOnly label='Reset' onClick={handleReset} />
150
+ <Toolbar.IconButton
151
+ disabled={streaming}
152
+ icon='ph--plus--regular'
153
+ iconOnly
154
+ label='Append'
155
+ onClick={handleAppend}
156
+ />
157
+ <Toolbar.Separator />
158
+ <Input.Root>
159
+ <Input.Label classNames='pr-1'>Debug</Input.Label>
160
+ <Input.Switch checked={debug} onCheckedChange={setDebug} />
161
+ </Input.Root>
162
+ </Toolbar.Root>
163
+ </Panel.Toolbar>
164
+ <Panel.Content>
165
+ <MarkdownStream {...props} debug={debug} ref={setController} />
166
+ </Panel.Content>
167
+ </Panel.Root>
168
+ );
169
+ };
170
+
171
+ const meta = {
172
+ title: 'ui/react-ui-markdown/MarkdownStream',
173
+ render: DefaultStory,
174
+ decorators: [withTheme(), withLayout({ layout: 'column' })],
175
+ parameters: { layout: 'fullscreen' },
176
+ } satisfies Meta<typeof DefaultStory>;
177
+
178
+ export default meta;
179
+
180
+ type Story = StoryObj<typeof meta>;
181
+
182
+ export const Default: Story = {
183
+ args: {
184
+ initialContent: TEXT,
185
+ options: {
186
+ autoScroll: true,
187
+ },
188
+ },
189
+ };
190
+
191
+ export const Streaming: Story = {
192
+ args: {
193
+ registry,
194
+ content: TEXT,
195
+ options: {
196
+ autoScroll: true,
197
+ typewriter: true,
198
+ fader: true,
199
+ cursor: true,
200
+ },
201
+ },
202
+ };
203
+
204
+ export const Widgets: Story = {
205
+ args: {
206
+ registry,
207
+ initialContent: trim`
208
+ # DOM Widget
209
+ <dom-widget>${random.lorem.paragraph()}</dom-widget>
210
+
211
+ # React Widget
212
+ <react-widget>${random.lorem.paragraph()}</react-widget>
213
+ `,
214
+ },
215
+ };