@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.
- package/dist/lib/browser/index.mjs +32 -42
- package/dist/lib/browser/index.mjs.map +3 -3
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +32 -42
- package/dist/lib/node-esm/index.mjs.map +3 -3
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/types/src/components/Editor/Editor.d.ts +1 -1
- package/dist/types/src/components/Editor/Editor.d.ts.map +1 -1
- package/dist/types/src/components/Editor/Editor.stories.d.ts.map +1 -1
- package/dist/types/src/components/EditorMenuProvider/menu-presets.d.ts.map +1 -1
- package/dist/types/src/components/EditorPreviewProvider/EditorPreviewProvider.d.ts +4 -2
- package/dist/types/src/components/EditorPreviewProvider/EditorPreviewProvider.d.ts.map +1 -1
- package/dist/types/src/extensions/Assistant.stories.d.ts +2 -2
- package/dist/types/src/extensions/Assistant.stories.d.ts.map +1 -1
- package/dist/types/src/hooks/useBasicMarkdownExtensions.d.ts +3 -1
- package/dist/types/src/hooks/useBasicMarkdownExtensions.d.ts.map +1 -1
- package/dist/types/src/stories/EditorToolbar.stories.d.ts +2 -2
- package/dist/types/src/stories/EditorToolbar.stories.d.ts.map +1 -1
- package/dist/types/src/stories/Outliner.stories.d.ts +2 -2
- package/dist/types/src/stories/Outliner.stories.d.ts.map +1 -1
- package/dist/types/src/stories/Popover.stories.d.ts +2 -2
- package/dist/types/src/stories/Popover.stories.d.ts.map +1 -1
- package/dist/types/src/stories/Widgets.stories.d.ts +25 -0
- package/dist/types/src/stories/Widgets.stories.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +53 -51
- package/src/components/Editor/Editor.stories.tsx +3 -2
- package/src/components/Editor/Editor.tsx +2 -2
- package/src/components/EditorMenuProvider/menu-presets.ts +29 -23
- package/src/components/EditorPreviewProvider/EditorPreviewProvider.tsx +4 -3
- package/src/extensions/Assistant.stories.tsx +4 -4
- package/src/hooks/useBasicMarkdownExtensions.ts +5 -2
- package/src/stories/Automerge.stories.tsx +8 -8
- package/src/stories/EditorToolbar.stories.tsx +2 -2
- package/src/stories/Outliner.stories.tsx +2 -2
- package/src/stories/Popover.stories.tsx +3 -3
- package/src/stories/Widgets.stories.tsx +241 -0
- package/src/stories/components/util.tsx +1 -1
- package/dist/types/src/stories/Preview.stories.d.ts +0 -26
- package/dist/types/src/stories/Preview.stories.d.ts.map +0 -1
- package/dist/types/src/stories/Tags.stories.d.ts +0 -16
- package/dist/types/src/stories/Tags.stories.d.ts.map +0 -1
- package/src/stories/Preview.stories.tsx +0 -253
- 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
|
+

|
|
193
|
+
|
|
194
|
+
It consists of [ECHO](echo:/echo), [HALO](echo:/halo), and [MESH](echo:/mesh).
|
|
195
|
+
|
|
196
|
+
## Deep dive
|
|
197
|
+
|
|
198
|
+

|
|
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
|
-

|
|
214
|
-
|
|
215
|
-
It consists of [ECHO](echo:/echo), [HALO](echo:/halo), and [MESH](echo:/mesh).
|
|
216
|
-
|
|
217
|
-
## Deep dive
|
|
218
|
-
|
|
219
|
-

|
|
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
|
-
};
|