@dxos/react-ui-editor 0.9.1-main.c7dcc2e112 → 0.10.0

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 (44) hide show
  1. package/dist/lib/browser/index.mjs +32 -42
  2. package/dist/lib/browser/index.mjs.map +3 -3
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/lib/node-esm/index.mjs +32 -42
  5. package/dist/lib/node-esm/index.mjs.map +3 -3
  6. package/dist/lib/node-esm/meta.json +1 -1
  7. package/dist/types/src/components/Editor/Editor.d.ts +1 -1
  8. package/dist/types/src/components/Editor/Editor.d.ts.map +1 -1
  9. package/dist/types/src/components/Editor/Editor.stories.d.ts.map +1 -1
  10. package/dist/types/src/components/EditorMenuProvider/menu-presets.d.ts.map +1 -1
  11. package/dist/types/src/components/EditorPreviewProvider/EditorPreviewProvider.d.ts +4 -2
  12. package/dist/types/src/components/EditorPreviewProvider/EditorPreviewProvider.d.ts.map +1 -1
  13. package/dist/types/src/extensions/Assistant.stories.d.ts +2 -2
  14. package/dist/types/src/extensions/Assistant.stories.d.ts.map +1 -1
  15. package/dist/types/src/hooks/useBasicMarkdownExtensions.d.ts +3 -1
  16. package/dist/types/src/hooks/useBasicMarkdownExtensions.d.ts.map +1 -1
  17. package/dist/types/src/stories/EditorToolbar.stories.d.ts +2 -2
  18. package/dist/types/src/stories/EditorToolbar.stories.d.ts.map +1 -1
  19. package/dist/types/src/stories/Outliner.stories.d.ts +2 -2
  20. package/dist/types/src/stories/Outliner.stories.d.ts.map +1 -1
  21. package/dist/types/src/stories/Popover.stories.d.ts +2 -2
  22. package/dist/types/src/stories/Popover.stories.d.ts.map +1 -1
  23. package/dist/types/src/stories/Widgets.stories.d.ts +25 -0
  24. package/dist/types/src/stories/Widgets.stories.d.ts.map +1 -0
  25. package/dist/types/tsconfig.tsbuildinfo +1 -1
  26. package/package.json +53 -51
  27. package/src/components/Editor/Editor.stories.tsx +3 -2
  28. package/src/components/Editor/Editor.tsx +2 -2
  29. package/src/components/EditorMenuProvider/menu-presets.ts +29 -23
  30. package/src/components/EditorPreviewProvider/EditorPreviewProvider.tsx +4 -3
  31. package/src/extensions/Assistant.stories.tsx +4 -4
  32. package/src/hooks/useBasicMarkdownExtensions.ts +5 -2
  33. package/src/stories/Automerge.stories.tsx +8 -8
  34. package/src/stories/EditorToolbar.stories.tsx +2 -2
  35. package/src/stories/Outliner.stories.tsx +2 -2
  36. package/src/stories/Popover.stories.tsx +3 -3
  37. package/src/stories/Widgets.stories.tsx +241 -0
  38. package/src/stories/components/util.tsx +1 -1
  39. package/dist/types/src/stories/Preview.stories.d.ts +0 -26
  40. package/dist/types/src/stories/Preview.stories.d.ts.map +0 -1
  41. package/dist/types/src/stories/Tags.stories.d.ts +0 -16
  42. package/dist/types/src/stories/Tags.stories.d.ts.map +0 -1
  43. package/src/stories/Preview.stories.tsx +0 -253
  44. package/src/stories/Tags.stories.tsx +0 -104
@@ -0,0 +1,241 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ /**
6
+ * Stories demonstrating the xmlTags widget-portal mechanism in ui-editor.
7
+ *
8
+ * Both XmlTags and Preview stories use the same underlying pattern: a CodeMirror WidgetType
9
+ * creates a DOM placeholder, and React portals render content into it.
10
+ */
11
+
12
+ import { type Meta, type StoryObj } from '@storybook/react-vite';
13
+ import React, { useEffect, useMemo, useState } from 'react';
14
+ import { createPortal } from 'react-dom';
15
+
16
+ import { random } from '@dxos/random';
17
+ import { Card, Icon, Popover, useThemeContext } from '@dxos/react-ui';
18
+ import { withLayout, withTheme } from '@dxos/react-ui/testing';
19
+ import {
20
+ AnchorWidget,
21
+ type XmlWidgetProps,
22
+ type XmlWidgetRegistry,
23
+ type XmlWidgetState,
24
+ createBasicExtensions,
25
+ createThemeExtensions,
26
+ decorateMarkdown,
27
+ extendedMarkdown,
28
+ image,
29
+ xmlTags,
30
+ } from '@dxos/ui-editor';
31
+ import { type PreviewLinkRef, type PreviewLinkTarget } from '@dxos/ui-types';
32
+ import { safeParseInt, trim } from '@dxos/util';
33
+
34
+ import { EditorPreviewProvider, useEditorPreview } from '../components';
35
+ import { useTextEditor } from '../hooks';
36
+ import { EditorStory } from './components';
37
+
38
+ // ----------------------------------------------------------------------------
39
+ // XmlTags helpers
40
+ // ----------------------------------------------------------------------------
41
+
42
+ const xmlRegistry = {
43
+ test: {
44
+ block: true,
45
+ Component: ({ start = '0' }: { start?: string }) => {
46
+ const [count, setCount] = useState<number>(safeParseInt(start, 0));
47
+ useEffect(() => {
48
+ const interval = setInterval(() => {
49
+ setCount((prev) => {
50
+ if (prev >= 200) {
51
+ clearInterval(interval);
52
+ return prev;
53
+ }
54
+ return prev + 1;
55
+ });
56
+ }, 100);
57
+ return () => clearInterval(interval);
58
+ }, []);
59
+ return <div className='p-2 border border-separator rounded-sm'>Test {count}</div>;
60
+ },
61
+ },
62
+ } satisfies XmlWidgetRegistry;
63
+
64
+ const XmlTagsStory = ({ text }: { text?: string }) => {
65
+ const { themeMode } = useThemeContext();
66
+ const [widgets, setWidgets] = useState<XmlWidgetState[]>([]);
67
+ const { parentRef } = useTextEditor({
68
+ initialValue: text,
69
+ extensions: [
70
+ createThemeExtensions({ themeMode }),
71
+ createBasicExtensions({ lineWrapping: true }),
72
+ decorateMarkdown(),
73
+ extendedMarkdown({ registry: xmlRegistry }),
74
+ xmlTags({ registry: xmlRegistry, setWidgets }),
75
+ ],
76
+ });
77
+
78
+ return (
79
+ <>
80
+ <div ref={parentRef} className='w-full p-4' />
81
+ {widgets.map(({ id, root, Component, props }) => (
82
+ <div key={id}>{createPortal(<Component {...props} />, root)}</div>
83
+ ))}
84
+ </>
85
+ );
86
+ };
87
+
88
+ // ----------------------------------------------------------------------------
89
+ // Preview helpers
90
+ // ----------------------------------------------------------------------------
91
+
92
+ const handlePreviewLookup = async ({ dxn, label }: PreviewLinkRef): Promise<PreviewLinkTarget> => {
93
+ random.seed(dxn.split('').reduce((acc: number, char: string) => acc + char.charCodeAt(0), 1));
94
+ return { label };
95
+ };
96
+
97
+ const PreviewCard = () => {
98
+ const { target } = useEditorPreview('PreviewCard');
99
+ if (!target) {
100
+ return null;
101
+ }
102
+ return (
103
+ <Popover.Portal>
104
+ <Popover.Content onOpenAutoFocus={(event) => event.preventDefault()}>
105
+ <Popover.Viewport classNames='dx-card-popover-width'>
106
+ <Card.Root border={false}>
107
+ <Card.Header>
108
+ <Card.Block>
109
+ <Icon icon='ph--file-text--regular' />
110
+ </Card.Block>
111
+ <Card.Title>{target.label}</Card.Title>
112
+ <Popover.Close asChild>
113
+ <Card.ActionIconButton action='close' />
114
+ </Popover.Close>
115
+ </Card.Header>
116
+ <Card.Row>
117
+ <Card.Text variant='description'>{target.label}</Card.Text>
118
+ </Card.Row>
119
+ </Card.Root>
120
+ </Popover.Viewport>
121
+ <Popover.Arrow />
122
+ </Popover.Content>
123
+ </Popover.Portal>
124
+ );
125
+ };
126
+
127
+ const PreviewBlockCard = ({ dxn, label }: XmlWidgetProps<{ dxn: string; label: string }>) => {
128
+ const [text, setText] = useState<string | undefined>();
129
+ useEffect(() => {
130
+ random.seed(dxn.split('').reduce((acc: number, char: string) => acc + char.charCodeAt(0), 1));
131
+ setText(Array.from({ length: 2 }, () => random.lorem.paragraphs()).join('\n\n'));
132
+ }, [dxn]);
133
+ return (
134
+ <Card.Root>
135
+ <Card.Header>
136
+ <Card.Block>
137
+ <Icon icon='ph--arrow-square-up--regular' />
138
+ </Card.Block>
139
+ <Card.Title>{label}</Card.Title>
140
+ </Card.Header>
141
+ {text && (
142
+ <Card.Row>
143
+ <Card.Text className='text-description'>{text}</Card.Text>
144
+ </Card.Row>
145
+ )}
146
+ </Card.Root>
147
+ );
148
+ };
149
+
150
+ // ----------------------------------------------------------------------------
151
+ // Meta
152
+ // ----------------------------------------------------------------------------
153
+
154
+ const meta = {
155
+ title: 'ui/react-ui-editor/Widgets',
156
+ decorators: [withTheme(), withLayout({ layout: 'fullscreen' })],
157
+ parameters: { layout: 'fullscreen' },
158
+ } satisfies Meta;
159
+
160
+ export default meta;
161
+
162
+ type Story = StoryObj<typeof meta>;
163
+
164
+ // ----------------------------------------------------------------------------
165
+ // Stories
166
+ // ----------------------------------------------------------------------------
167
+
168
+ const xmlTagsText = trim`
169
+ # XML Tags
170
+
171
+ React widget below.
172
+
173
+ <test id="t-1" />
174
+
175
+ <test id="t-2" start="100" />
176
+
177
+ React widget above.
178
+ `;
179
+
180
+ /**
181
+ * XML tag names in the document trigger React components via xmlTags.
182
+ */
183
+ export const XmlTags: Story = {
184
+ render: () => <XmlTagsStory text={xmlTagsText} />,
185
+ };
186
+
187
+ const previewText = trim`
188
+ # Preview
189
+
190
+ This project is part of the [DXOS](echo:/123) SDK.
191
+
192
+ ![DXOS](echo:/123)
193
+
194
+ It consists of [ECHO](echo:/echo), [HALO](echo:/halo), and [MESH](echo:/mesh).
195
+
196
+ ## Deep dive
197
+
198
+ ![ECHO](echo:/echo)
199
+
200
+ `;
201
+
202
+ /**
203
+ * Markdown image/link URLs (echo:/…) trigger block widgets via xmlTags.
204
+ */
205
+ export const Preview: Story = {
206
+ render: () => {
207
+ const [widgets, setWidgets] = useState<XmlWidgetState[]>([]);
208
+ const extensions = useMemo(
209
+ () => [
210
+ image(),
211
+ xmlTags({
212
+ registry: {
213
+ 'dxn-preview': {
214
+ block: true,
215
+ urlSchemes: ['dxn:', 'echo:'],
216
+ Component: PreviewBlockCard,
217
+ },
218
+ 'link-preview': {
219
+ block: false,
220
+ urlSchemes: ['dxn:', 'echo:'],
221
+ factory: ({ label, dxn }: XmlWidgetProps<{ label: string; dxn: string }>) =>
222
+ label && dxn ? new AnchorWidget(label, dxn) : null,
223
+ },
224
+ },
225
+ setWidgets,
226
+ }),
227
+ ],
228
+ [],
229
+ );
230
+
231
+ return (
232
+ <EditorPreviewProvider onLookup={handlePreviewLookup}>
233
+ <EditorStory text={previewText} extensions={extensions} />
234
+ <PreviewCard />
235
+ {widgets.map(({ id, root, Component, props }) => (
236
+ <div key={id}>{createPortal(<Component {...props} />, root)}</div>
237
+ ))}
238
+ </EditorPreviewProvider>
239
+ );
240
+ },
241
+ };
@@ -211,7 +211,7 @@ const hover =
211
211
  export const renderLinkTooltip: RenderCallback<{ url: string }> = (el, { url }) => {
212
212
  el.appendChild(
213
213
  Domino.of('a')
214
- .attributes({ href: url, target: '_blank', rel: 'noreferrer', 'aria-label': 'Open link' })
214
+ .attributes({ 'href': url, 'target': '_blank', 'rel': 'noreferrer', 'aria-label': 'Open link' })
215
215
  .classNames(hover, 'flex items-center gap-2')
216
216
  .text(safeUrl(url)?.toString() ?? url)
217
217
  .append(Domino.svg('ph--arrow-square-out--regular')).root,
@@ -1,26 +0,0 @@
1
- import { type EditorView } from '@codemirror/view';
2
- import { type StoryObj } from '@storybook/react-vite';
3
- import React from 'react';
4
- import { type EditorController } from '../components';
5
- declare const meta: {
6
- title: string;
7
- component: React.ForwardRefExoticComponent<Pick<import("..").UseTextEditorProps, "extensions" | "id" | "scrollTo" | "selection"> & Pick<import("@dxos/ui-editor").ThemeExtensionsOptions, "slots"> & {
8
- debug?: import("./components").DebugMode;
9
- debugCustom?: (view: EditorView) => React.ReactNode;
10
- text?: string;
11
- object?: import("@dxos/echo/Obj").OfShape<import("@dxos/echo/testing").TestSchema.Expando>;
12
- readOnly?: boolean;
13
- placeholder?: string;
14
- lineNumbers?: boolean;
15
- monospace?: boolean;
16
- onReady?: (view: EditorView) => void;
17
- } & React.RefAttributes<EditorController>>;
18
- decorators: import("@storybook/react").Decorator[];
19
- parameters: {
20
- layout: string;
21
- };
22
- };
23
- export default meta;
24
- type Story = StoryObj<typeof meta>;
25
- export declare const Default: Story;
26
- //# sourceMappingURL=Preview.stories.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"Preview.stories.d.ts","sourceRoot":"","sources":["../../../../src/stories/Preview.stories.tsx"],"names":[],"mappings":"AAKA,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,EAAa,KAAK,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACjE,OAAO,KAAoD,MAAM,OAAO,CAAC;AAmBzE,OAAO,EAAE,KAAK,gBAAgB,EAA2C,MAAM,eAAe,CAAC;AAwK/F,QAAA,MAAM,IAAI;;;;;;;;;;;;;;;QAKN,MAAM;;CAE0B,CAAC;eAEtB,IAAI;AAEnB,KAAK,KAAK,GAAG,QAAQ,CAAC,OAAO,IAAI,CAAC,CAAC;AAiBnC,eAAO,MAAM,OAAO,EAAE,KA8BrB,CAAC"}
@@ -1,16 +0,0 @@
1
- import { type StoryObj } from '@storybook/react-vite';
2
- import React from 'react';
3
- declare const meta: {
4
- title: string;
5
- render: ({ text }: {
6
- text?: string;
7
- }) => React.JSX.Element;
8
- decorators: import("@storybook/react").Decorator[];
9
- parameters: {
10
- layout: string;
11
- };
12
- };
13
- export default meta;
14
- type Story = StoryObj<typeof meta>;
15
- export declare const Default: Story;
16
- //# sourceMappingURL=Tags.stories.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"Tags.stories.d.ts","sourceRoot":"","sources":["../../../../src/stories/Tags.stories.tsx"],"names":[],"mappings":"AAIA,OAAO,EAAa,KAAK,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACjE,OAAO,KAA8B,MAAM,OAAO,CAAC;AAiFnD,QAAA,MAAM,IAAI;;uBApCsB;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE;;;QAyC7C,MAAM;;CAE2B,CAAC;eAEvB,IAAI;AAEnB,KAAK,KAAK,GAAG,QAAQ,CAAC,OAAO,IAAI,CAAC,CAAC;AAEnC,eAAO,MAAM,OAAO,EAAE,KAIrB,CAAC"}
@@ -1,253 +0,0 @@
1
- //
2
- // Copyright 2023 DXOS.org
3
- //
4
-
5
- import { syntaxTree } from '@codemirror/language';
6
- import { type EditorView } from '@codemirror/view';
7
- import { type Meta, type StoryObj } from '@storybook/react-vite';
8
- import React, { useCallback, useEffect, useMemo, useState } from 'react';
9
- import { createPortal } from 'react-dom';
10
-
11
- import { invariant } from '@dxos/invariant';
12
- import { random } from '@dxos/random';
13
- import { Card, Icon, IconButton, Popover } from '@dxos/react-ui';
14
- import { Menu, createMenuAction } from '@dxos/react-ui-menu';
15
- import { withLayout, withTheme } from '@dxos/react-ui/testing';
16
- import {
17
- type PreviewBlock,
18
- type PreviewLinkRef,
19
- type PreviewLinkTarget,
20
- getLinkRef,
21
- image,
22
- preview,
23
- } from '@dxos/ui-editor';
24
- import { hoverableControls } from '@dxos/ui-theme';
25
- import { trim } from '@dxos/util';
26
-
27
- import { type EditorController, EditorPreviewProvider, useEditorPreview } from '../components';
28
- import { EditorStory } from './components';
29
-
30
- const handlePreviewLookup = async ({ dxn, label }: PreviewLinkRef): Promise<PreviewLinkTarget> => {
31
- // Random text.
32
- random.seed(dxn.split('').reduce((acc: number, char: string) => acc + char.charCodeAt(0), 1));
33
- const text = Array.from({ length: 2 }, () => random.lorem.paragraphs()).join('\n\n');
34
- return {
35
- label,
36
- text,
37
- };
38
- };
39
-
40
- // Async lookup.
41
- // TODO(burdon): Handle errors.
42
- const useRefTarget = (link: PreviewLinkRef): PreviewLinkTarget | undefined => {
43
- const [target, setTarget] = useState<PreviewLinkTarget | undefined>();
44
- useEffect(() => {
45
- void handlePreviewLookup(link).then((target) => setTarget(target ?? undefined));
46
- }, [link]);
47
-
48
- return target;
49
- };
50
-
51
- const PreviewCard = () => {
52
- const { target } = useEditorPreview('PreviewCard');
53
- if (!target) {
54
- return null;
55
- }
56
-
57
- return (
58
- <Popover.Portal>
59
- <Popover.Content onOpenAutoFocus={(event) => event.preventDefault()}>
60
- <Popover.Viewport classNames='dx-card-popover-width'>
61
- <Card.Root border={false}>
62
- <Card.Header>
63
- <Card.Block>
64
- <Icon icon='ph--file-text--regular' />
65
- </Card.Block>
66
- <Card.Title>{target.label}</Card.Title>
67
- <Popover.Close asChild>
68
- <Card.ActionIconButton action='close' />
69
- </Popover.Close>
70
- </Card.Header>
71
- <Card.Row>
72
- <Card.Text variant='description'>{target.text}</Card.Text>
73
- </Card.Row>
74
- </Card.Root>
75
- </Popover.Viewport>
76
- <Popover.Arrow />
77
- </Popover.Content>
78
- </Popover.Portal>
79
- );
80
- };
81
-
82
- type PreviewAction =
83
- | {
84
- type: 'insert';
85
- link: PreviewLinkRef;
86
- target: PreviewLinkTarget;
87
- }
88
- | {
89
- type: 'delete';
90
- link: PreviewLinkRef;
91
- };
92
-
93
- const PreviewBlockComponent = ({ link, el, view }: { link: PreviewLinkRef; el: HTMLElement; view?: EditorView }) => {
94
- const target = useRefTarget(link);
95
-
96
- const handleAction = useCallback(
97
- (action: PreviewAction) => {
98
- invariant(view, 'View not found');
99
- const pos = view.posAtDOM(el);
100
- const node = syntaxTree(view.state).resolve(pos + 1).node.parent;
101
- if (!node) {
102
- return;
103
- }
104
-
105
- const link = getLinkRef(view.state, node);
106
- if (link?.dxn !== action.link.dxn) {
107
- return;
108
- }
109
-
110
- switch (action.type) {
111
- // TODO(burdon): Should we dispatch to the view or mutate the document? (i.e., handle externally?)
112
- // Insert ref text.
113
- case 'insert': {
114
- view.dispatch({
115
- changes: {
116
- from: node.from,
117
- to: node.to,
118
- insert: action.target.text,
119
- },
120
- });
121
- break;
122
- }
123
- // Remove ref.
124
- case 'delete': {
125
- view.dispatch({
126
- changes: {
127
- from: node.from,
128
- to: node.to,
129
- },
130
- });
131
- break;
132
- }
133
- }
134
- },
135
- [view, el],
136
- );
137
-
138
- const handleDelete = useCallback(() => {
139
- handleAction({ type: 'delete', link });
140
- }, [handleAction, link]);
141
-
142
- const handleInsert = useCallback(() => {
143
- if (target) {
144
- handleAction({ type: 'insert', link, target });
145
- }
146
- }, [handleAction, link, target]);
147
-
148
- const menuItems = useMemo(
149
- () => [
150
- createMenuAction('delete', handleDelete, {
151
- label: link.suggest ? 'Discard' : 'Delete',
152
- icon: 'ph--x--regular',
153
- }),
154
- ...(target
155
- ? [
156
- createMenuAction('apply', handleInsert, {
157
- label: 'Apply',
158
- icon: 'ph--check--regular',
159
- }),
160
- ]
161
- : []),
162
- ],
163
- [handleDelete, handleInsert, link.suggest, target],
164
- );
165
-
166
- return createPortal(
167
- <Menu.Root>
168
- <Card.Root classNames={hoverableControls}>
169
- {!view?.state.readOnly && (
170
- <Card.Header>
171
- <Card.Block>
172
- <Icon icon='ph--bookmark--regular' />
173
- </Card.Block>
174
- <Card.Title>{link.label}</Card.Title>
175
- {/* TODO(wittjosiah): Reconcile with Card.Menu. */}
176
- <Card.Block end>
177
- <Menu.Trigger asChild disabled={!menuItems?.length}>
178
- <IconButton iconOnly variant='ghost' icon='ph--dots-three-vertical--regular' label='Menu' />
179
- </Menu.Trigger>
180
- </Card.Block>
181
- <Menu.Content items={menuItems} />
182
- </Card.Header>
183
- )}
184
- {target && (
185
- <Card.Row>
186
- <Card.Text className='text-description'>{target.text}</Card.Text>
187
- </Card.Row>
188
- )}
189
- </Card.Root>
190
- </Menu.Root>,
191
- el,
192
- );
193
- };
194
-
195
- const meta = {
196
- title: 'ui/react-ui-editor/Preview',
197
- component: EditorStory,
198
- decorators: [withTheme(), withLayout({ layout: 'fullscreen' })],
199
- parameters: {
200
- layout: 'fullscreen',
201
- },
202
- } satisfies Meta<typeof EditorStory>;
203
-
204
- export default meta;
205
-
206
- type Story = StoryObj<typeof meta>;
207
-
208
- const text = trim`
209
- # Preview
210
-
211
- This project is part of the [DXOS](echo:/123) SDK.
212
-
213
- ![DXOS](echo:/123)
214
-
215
- It consists of [ECHO](echo:/echo), [HALO](echo:/halo), and [MESH](echo:/mesh).
216
-
217
- ## Deep dive
218
-
219
- ![ECHO](echo:/echo)
220
-
221
- `;
222
-
223
- export const Default: Story = {
224
- render: () => {
225
- const [controller, setController] = useState<EditorController | null>(null);
226
- const [previewBlocks, setPreviewBlocks] = useState<PreviewBlock[]>([]);
227
- const extensions = useMemo(() => {
228
- return [
229
- image(),
230
- preview({
231
- addBlockContainer: (block) => {
232
- setPreviewBlocks((prev) => [...prev, block]);
233
- },
234
- removeBlockContainer: (block) => {
235
- setPreviewBlocks((prev) => prev.filter(({ link: prevLink }) => prevLink.dxn !== block.link.dxn));
236
- },
237
- }),
238
- ];
239
- }, []);
240
-
241
- // TODO(burdon): Migrate to Editor.Root.
242
- return (
243
- <EditorPreviewProvider onLookup={handlePreviewLookup}>
244
- <EditorStory ref={setController} text={text} extensions={extensions} />
245
- <PreviewCard />
246
- {controller?.view &&
247
- previewBlocks.map(({ link, el }) => (
248
- <PreviewBlockComponent key={link.dxn} link={link} el={el} view={controller.view!} />
249
- ))}
250
- </EditorPreviewProvider>
251
- );
252
- },
253
- };
@@ -1,104 +0,0 @@
1
- //
2
- // Copyright 2025 DXOS.org
3
- //
4
-
5
- import { type Meta, type StoryObj } from '@storybook/react-vite';
6
- import React, { useEffect, useState } from 'react';
7
- import { createPortal } from 'react-dom';
8
-
9
- import { useThemeContext } from '@dxos/react-ui';
10
- import { withLayout, withTheme } from '@dxos/react-ui/testing';
11
- import {
12
- type XmlWidgetRegistry,
13
- type XmlWidgetState,
14
- createBasicExtensions,
15
- createThemeExtensions,
16
- decorateMarkdown,
17
- extendedMarkdown,
18
- xmlTags,
19
- } from '@dxos/ui-editor';
20
- import { safeParseInt, trim } from '@dxos/util';
21
-
22
- import { useTextEditor } from '../hooks';
23
-
24
- const registry = {
25
- /**
26
- * Custom tag: <test/>
27
- */
28
- test: {
29
- block: true,
30
- Component: ({ start = '0' }) => {
31
- const [count, setCount] = useState<number>(safeParseInt(start, 0));
32
- useEffect(() => {
33
- const interval = setInterval(() => {
34
- setCount((prev) => {
35
- if (prev >= 200) {
36
- clearInterval(interval);
37
- return prev;
38
- }
39
-
40
- return prev + 1;
41
- });
42
- }, 100);
43
- return () => clearInterval(interval);
44
- }, []);
45
-
46
- return <div className='p-2 border border-separator rounded-sm'>Test {count}</div>;
47
- },
48
- },
49
- } satisfies XmlWidgetRegistry;
50
-
51
- const DefaultStory = ({ text }: { text?: string }) => {
52
- const { themeMode } = useThemeContext();
53
- const [widgets, setWidgets] = useState<XmlWidgetState[]>([]);
54
- const { parentRef } = useTextEditor({
55
- initialValue: text,
56
- extensions: [
57
- createThemeExtensions({ themeMode }),
58
- createBasicExtensions({ lineWrapping: true }),
59
- decorateMarkdown(),
60
- extendedMarkdown({ registry }),
61
- xmlTags({ registry, setWidgets }),
62
- ],
63
- });
64
-
65
- return (
66
- <>
67
- <div ref={parentRef} className='w-full p-4' />
68
- {widgets.map(({ id, root, Component, props }) => (
69
- <div key={id}>{createPortal(<Component {...props} />, root)}</div>
70
- ))}
71
- </>
72
- );
73
- };
74
-
75
- const text = trim`
76
- # Tags
77
-
78
- React widget below.
79
-
80
- <test id="t-1" />
81
-
82
- <test id="t-2" start="100" />
83
-
84
- React widget above.
85
- `;
86
-
87
- const meta = {
88
- title: 'ui/react-ui-editor/Tags',
89
- render: DefaultStory,
90
- decorators: [withTheme(), withLayout({ layout: 'fullscreen' })],
91
- parameters: {
92
- layout: 'fullscreen',
93
- },
94
- } satisfies Meta<typeof DefaultStory>;
95
-
96
- export default meta;
97
-
98
- type Story = StoryObj<typeof meta>;
99
-
100
- export const Default: Story = {
101
- args: {
102
- text,
103
- },
104
- };