@moldable-ai/editor 0.1.7 → 0.1.9

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 (43) hide show
  1. package/dist/components/markdown-editor.d.ts +12 -3
  2. package/dist/components/markdown-editor.d.ts.map +1 -1
  3. package/dist/components/markdown-editor.js +23 -9
  4. package/dist/index.d.ts +6 -2
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +6 -2
  7. package/dist/lib/lexical/editor-theme.d.ts +2 -0
  8. package/dist/lib/lexical/editor-theme.d.ts.map +1 -1
  9. package/dist/lib/lexical/editor-theme.js +2 -0
  10. package/dist/lib/lexical/file-reference-plugin.d.ts +9 -0
  11. package/dist/lib/lexical/file-reference-plugin.d.ts.map +1 -0
  12. package/dist/lib/lexical/file-reference-plugin.js +99 -0
  13. package/dist/lib/lexical/find-replace-plugin.d.ts +31 -0
  14. package/dist/lib/lexical/find-replace-plugin.d.ts.map +1 -0
  15. package/dist/lib/lexical/find-replace-plugin.js +205 -0
  16. package/dist/lib/lexical/headless-editor.d.ts.map +1 -1
  17. package/dist/lib/lexical/headless-editor.js +6 -0
  18. package/dist/lib/lexical/image-component.d.ts +8 -0
  19. package/dist/lib/lexical/image-component.d.ts.map +1 -0
  20. package/dist/lib/lexical/image-component.js +99 -0
  21. package/dist/lib/lexical/image-node.d.ts +36 -0
  22. package/dist/lib/lexical/image-node.d.ts.map +1 -0
  23. package/dist/lib/lexical/image-node.js +105 -0
  24. package/dist/lib/lexical/markdown-transformers.d.ts +4 -1
  25. package/dist/lib/lexical/markdown-transformers.d.ts.map +1 -1
  26. package/dist/lib/lexical/markdown-transformers.js +78 -0
  27. package/dist/lib/lexical/media-context.d.ts +7 -0
  28. package/dist/lib/lexical/media-context.d.ts.map +1 -0
  29. package/dist/lib/lexical/media-context.js +7 -0
  30. package/dist/lib/lexical/media-node.d.ts +32 -0
  31. package/dist/lib/lexical/media-node.d.ts.map +1 -0
  32. package/dist/lib/lexical/media-node.js +97 -0
  33. package/dist/lib/lexical/media-plugin.d.ts +12 -0
  34. package/dist/lib/lexical/media-plugin.d.ts.map +1 -0
  35. package/dist/lib/lexical/media-plugin.js +139 -0
  36. package/dist/lib/lexical/sync-plugin.d.ts +1 -0
  37. package/dist/lib/lexical/sync-plugin.d.ts.map +1 -1
  38. package/dist/lib/lexical/sync-plugin.js +13 -5
  39. package/package.json +19 -18
  40. package/src/styles/index.css +130 -0
  41. package/dist/lib/lexical/translation.test.d.ts +0 -2
  42. package/dist/lib/lexical/translation.test.d.ts.map +0 -1
  43. package/dist/lib/lexical/translation.test.js +0 -329
@@ -0,0 +1,139 @@
1
+ 'use client';
2
+ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
3
+ import { $createParagraphNode, $createTextNode, $insertNodes, COMMAND_PRIORITY_HIGH, DROP_COMMAND, PASTE_COMMAND, } from 'lexical';
4
+ import { useEffect } from 'react';
5
+ import { $createImageNode } from './image-node';
6
+ import { $createMediaNode } from './media-node';
7
+ function dataUrlForFile(file) {
8
+ return new Promise((resolve, reject) => {
9
+ const reader = new FileReader();
10
+ reader.onload = () => {
11
+ if (typeof reader.result === 'string')
12
+ resolve(reader.result);
13
+ else
14
+ reject(new Error('Failed to read file'));
15
+ };
16
+ reader.onerror = () => reject(reader.error ?? new Error('Failed to read file'));
17
+ reader.readAsDataURL(file);
18
+ });
19
+ }
20
+ function kindForFile(file) {
21
+ if (file.type.startsWith('image/'))
22
+ return 'image';
23
+ if (file.type.startsWith('video/'))
24
+ return 'video';
25
+ if (file.type.startsWith('audio/'))
26
+ return 'audio';
27
+ return 'file';
28
+ }
29
+ function escapeAttribute(value) {
30
+ return value
31
+ .replaceAll('&', '&')
32
+ .replaceAll('"', '"')
33
+ .replaceAll('<', '&lt;')
34
+ .replaceAll('>', '&gt;');
35
+ }
36
+ function markdownForMedia(result, file) {
37
+ const kind = result.kind ?? kindForFile(file);
38
+ const altText = result.altText ?? file.name;
39
+ if (result.markdown)
40
+ return result.markdown;
41
+ if (kind === 'video') {
42
+ return `<video controls src="${escapeAttribute(result.src)}"></video>`;
43
+ }
44
+ if (kind === 'audio') {
45
+ return `<audio controls src="${escapeAttribute(result.src)}"></audio>`;
46
+ }
47
+ return `[${altText}](${result.src})`;
48
+ }
49
+ export function MediaPasteDropPlugin({ onMediaUpload, }) {
50
+ const [editor] = useLexicalComposerContext();
51
+ useEffect(() => {
52
+ async function uploadFile(file) {
53
+ if (onMediaUpload)
54
+ return onMediaUpload(file);
55
+ return {
56
+ altText: file.name,
57
+ kind: kindForFile(file),
58
+ src: await dataUrlForFile(file),
59
+ };
60
+ }
61
+ async function insertFiles(files) {
62
+ for (const file of files) {
63
+ const result = await uploadFile(file);
64
+ const kind = result.kind ?? kindForFile(file);
65
+ editor.update(() => {
66
+ if (kind === 'image') {
67
+ $insertNodes([
68
+ $createImageNode({
69
+ altText: result.altText ?? file.name,
70
+ src: result.src,
71
+ }),
72
+ ]);
73
+ return;
74
+ }
75
+ if (kind === 'video' || kind === 'audio') {
76
+ $insertNodes([$createMediaNode({ kind, src: result.src })]);
77
+ return;
78
+ }
79
+ const paragraph = $createParagraphNode();
80
+ paragraph.append($createTextNode(markdownForMedia(result, file)));
81
+ $insertNodes([paragraph]);
82
+ });
83
+ }
84
+ }
85
+ function filesFromDataTransfer(dataTransfer) {
86
+ return Array.from(dataTransfer?.files ?? []).filter((file) => file.size > 0);
87
+ }
88
+ return editor.registerCommand(PASTE_COMMAND, (event) => {
89
+ if (!('clipboardData' in event))
90
+ return false;
91
+ const files = filesFromDataTransfer(event.clipboardData);
92
+ if (files.length === 0)
93
+ return false;
94
+ event.preventDefault();
95
+ void insertFiles(files);
96
+ return true;
97
+ }, COMMAND_PRIORITY_HIGH);
98
+ }, [editor, onMediaUpload]);
99
+ useEffect(() => {
100
+ return editor.registerCommand(DROP_COMMAND, (event) => {
101
+ const files = Array.from(event.dataTransfer?.files ?? []).filter((file) => file.size > 0);
102
+ if (files.length === 0)
103
+ return false;
104
+ event.preventDefault();
105
+ void Promise.resolve().then(async () => {
106
+ for (const file of files) {
107
+ const result = onMediaUpload
108
+ ? await onMediaUpload(file)
109
+ : {
110
+ altText: file.name,
111
+ kind: kindForFile(file),
112
+ src: await dataUrlForFile(file),
113
+ };
114
+ const kind = result.kind ?? kindForFile(file);
115
+ editor.update(() => {
116
+ if (kind === 'image') {
117
+ $insertNodes([
118
+ $createImageNode({
119
+ altText: result.altText ?? file.name,
120
+ src: result.src,
121
+ }),
122
+ ]);
123
+ }
124
+ else if (kind === 'video' || kind === 'audio') {
125
+ $insertNodes([$createMediaNode({ kind, src: result.src })]);
126
+ }
127
+ else {
128
+ const paragraph = $createParagraphNode();
129
+ paragraph.append($createTextNode(markdownForMedia(result, file)));
130
+ $insertNodes([paragraph]);
131
+ }
132
+ });
133
+ }
134
+ });
135
+ return true;
136
+ }, COMMAND_PRIORITY_HIGH);
137
+ }, [editor, onMediaUpload]);
138
+ return null;
139
+ }
@@ -2,6 +2,7 @@ interface SyncPluginProps {
2
2
  value: string;
3
3
  initialValueRef: React.MutableRefObject<string>;
4
4
  }
5
+ export declare const EXTERNAL_MARKDOWN_SYNC_TAG = "moldable-external-markdown-sync";
5
6
  /**
6
7
  * Plugin that syncs external value changes with the editor.
7
8
  * Handles cases where the value prop changes from outside the editor.
@@ -1 +1 @@
1
- {"version":3,"file":"sync-plugin.d.ts","sourceRoot":"","sources":["../../../src/lib/lexical/sync-plugin.tsx"],"names":[],"mappings":"AAWA,UAAU,eAAe;IACvB,KAAK,EAAE,MAAM,CAAA;IACb,eAAe,EAAE,KAAK,CAAC,gBAAgB,CAAC,MAAM,CAAC,CAAA;CAChD;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,EAAE,KAAK,EAAE,eAAe,EAAE,EAAE,eAAe,GAAG,IAAI,CAsC5E"}
1
+ {"version":3,"file":"sync-plugin.d.ts","sourceRoot":"","sources":["../../../src/lib/lexical/sync-plugin.tsx"],"names":[],"mappings":"AAWA,UAAU,eAAe;IACvB,KAAK,EAAE,MAAM,CAAA;IACb,eAAe,EAAE,KAAK,CAAC,gBAAgB,CAAC,MAAM,CAAC,CAAA;CAChD;AAED,eAAO,MAAM,0BAA0B,oCAAoC,CAAA;AAE3E;;;GAGG;AACH,wBAAgB,UAAU,CAAC,EAAE,KAAK,EAAE,eAAe,EAAE,EAAE,eAAe,GAAG,IAAI,CAiD5E"}
@@ -1,15 +1,16 @@
1
1
  'use client';
2
2
  import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
3
- import { useEffect } from 'react';
3
+ import { useLayoutEffect } from 'react';
4
4
  import { $convertFromMarkdownString, $convertToMarkdownString, markdownTransformers, } from './markdown-transformers';
5
5
  import { $createParagraphNode, $getRoot } from 'lexical';
6
+ export const EXTERNAL_MARKDOWN_SYNC_TAG = 'moldable-external-markdown-sync';
6
7
  /**
7
8
  * Plugin that syncs external value changes with the editor.
8
9
  * Handles cases where the value prop changes from outside the editor.
9
10
  */
10
11
  export function SyncPlugin({ value, initialValueRef }) {
11
12
  const [editor] = useLexicalComposerContext();
12
- useEffect(() => {
13
+ useLayoutEffect(() => {
13
14
  // Skip if this is the initial render or if value matches what we have
14
15
  if (value === initialValueRef.current) {
15
16
  return;
@@ -19,10 +20,12 @@ export function SyncPlugin({ value, initialValueRef }) {
19
20
  editor.read(() => {
20
21
  currentMarkdown = $convertToMarkdownString({
21
22
  transformers: markdownTransformers,
23
+ shouldPreserveNewLines: false,
22
24
  });
23
25
  });
24
26
  // Only update if the value is actually different
25
27
  if (currentMarkdown !== value) {
28
+ initialValueRef.current = value;
26
29
  editor.update(() => {
27
30
  const root = $getRoot();
28
31
  root.clear();
@@ -31,15 +34,20 @@ export function SyncPlugin({ value, initialValueRef }) {
31
34
  markdown: value,
32
35
  transformers: markdownTransformers,
33
36
  node: root,
34
- shouldPreserveNewLines: true,
37
+ shouldPreserveNewLines: false,
35
38
  });
36
39
  }
37
40
  else {
38
41
  root.append($createParagraphNode());
39
42
  }
40
- });
41
- initialValueRef.current = value;
43
+ if (root.getChildrenSize() === 0) {
44
+ root.append($createParagraphNode());
45
+ }
46
+ root.selectEnd();
47
+ }, { discrete: true, tag: EXTERNAL_MARKDOWN_SYNC_TAG });
48
+ return;
42
49
  }
50
+ initialValueRef.current = value;
43
51
  }, [editor, value, initialValueRef]);
44
52
  return null;
45
53
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moldable-ai/editor",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "Rich text markdown editor for Moldable applications",
5
5
  "author": "Desiderata LLC",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -32,18 +32,28 @@
32
32
  "src/styles",
33
33
  "LICENSE"
34
34
  ],
35
+ "scripts": {
36
+ "build": "tsc",
37
+ "dev": "tsc --watch",
38
+ "test": "vitest run",
39
+ "test:watch": "vitest",
40
+ "lint": "eslint . --ext .ts,.tsx --max-warnings 0",
41
+ "format": "prettier --write . --ignore-path ../../.prettierignore",
42
+ "check-types": "tsc --noEmit"
43
+ },
35
44
  "dependencies": {
36
45
  "@lexical/clipboard": "^0.39.0",
37
46
  "@lexical/code": "^0.39.0",
38
47
  "@lexical/extension": "^0.39.0",
48
+ "@lexical/headless": "^0.39.0",
39
49
  "@lexical/html": "^0.39.0",
40
50
  "@lexical/link": "^0.39.0",
41
51
  "@lexical/list": "^0.39.0",
52
+ "@lexical/mark": "^0.39.0",
42
53
  "@lexical/markdown": "^0.39.0",
43
54
  "@lexical/react": "^0.39.0",
44
55
  "@lexical/rich-text": "^0.39.0",
45
56
  "@lexical/selection": "^0.39.0",
46
- "@lexical/headless": "^0.39.0",
47
57
  "@lexical/utils": "^0.39.0",
48
58
  "lexical": "^0.39.0",
49
59
  "lucide-react": "^0.473.0",
@@ -52,30 +62,21 @@
52
62
  "remark-gfm": "^4.0.1"
53
63
  },
54
64
  "devDependencies": {
65
+ "@moldable-ai/eslint-config": "workspace:*",
66
+ "@moldable-ai/prettier-config": "workspace:*",
67
+ "@moldable-ai/typescript-config": "workspace:*",
68
+ "@moldable-ai/ui": "workspace:^",
55
69
  "@types/jsdom": "^27.0.0",
56
70
  "@types/node": "^24.0.3",
57
71
  "@types/react": "^19.1.6",
58
72
  "@types/react-dom": "^19.1.6",
59
73
  "jsdom": "^27.4.0",
60
74
  "typescript": "^5.8.3",
61
- "vitest": "^4.0.16",
62
- "@moldable-ai/eslint-config": "0.1.4",
63
- "@moldable-ai/typescript-config": "0.1.3",
64
- "@moldable-ai/prettier-config": "0.1.3",
65
- "@moldable-ai/ui": "^0.2.7"
75
+ "vitest": "^4.0.16"
66
76
  },
67
77
  "peerDependencies": {
68
- "@moldable-ai/ui": ">=0.2.7",
78
+ "@moldable-ai/ui": ">=0.2.10",
69
79
  "react": "^19.0.0",
70
80
  "react-dom": "^19.0.0"
71
- },
72
- "scripts": {
73
- "build": "tsc",
74
- "dev": "tsc --watch",
75
- "test": "vitest run",
76
- "test:watch": "vitest",
77
- "lint": "eslint . --ext .ts,.tsx --max-warnings 0",
78
- "format": "prettier --write . --ignore-path ../../.prettierignore",
79
- "check-types": "tsc --noEmit"
80
81
  }
81
- }
82
+ }
@@ -80,6 +80,19 @@
80
80
  word-break: break-word;
81
81
  }
82
82
 
83
+ /* Find/replace highlights */
84
+ .lexical-mark {
85
+ border-radius: 0.2em;
86
+ background-color: color-mix(in oklch, var(--primary) 24%, transparent);
87
+ color: inherit;
88
+ padding: 0 0.05em;
89
+ }
90
+
91
+ .lexical-mark-overlap {
92
+ background-color: color-mix(in oklch, var(--primary) 48%, transparent);
93
+ box-shadow: 0 0 0 1px color-mix(in oklch, var(--primary) 45%, transparent);
94
+ }
95
+
83
96
  /* Mention styles */
84
97
  .mention {
85
98
  color: hsl(var(--primary));
@@ -88,3 +101,120 @@
88
101
  padding: 0 4px;
89
102
  border-radius: 4px;
90
103
  }
104
+
105
+ /* Rich media nodes */
106
+ .lexical-image-shell {
107
+ display: inline-block;
108
+ max-width: 100%;
109
+ position: relative;
110
+ vertical-align: top;
111
+ }
112
+
113
+ .lexical-image-shell-selected {
114
+ outline: 3px solid var(--primary);
115
+ outline-offset: 0;
116
+ }
117
+
118
+ .lexical-image {
119
+ border-radius: 0;
120
+ display: block;
121
+ height: auto;
122
+ max-width: 100%;
123
+ }
124
+
125
+ .lexical-image-resize-handle {
126
+ background: var(--primary);
127
+ border: 2px solid var(--background);
128
+ border-radius: 9999px;
129
+ bottom: -8px;
130
+ cursor: nwse-resize;
131
+ height: 16px;
132
+ position: absolute;
133
+ right: -8px;
134
+ width: 16px;
135
+ }
136
+
137
+ .lexical-media-shell {
138
+ display: block;
139
+ max-width: 100%;
140
+ width: 100%;
141
+ }
142
+
143
+ .lexical-media {
144
+ background: rgb(10 10 10);
145
+ border: 1px solid rgb(255 255 255 / 0.1);
146
+ border-radius: 0.5rem;
147
+ color-scheme: dark;
148
+ display: block;
149
+ margin: 0.75rem 0;
150
+ max-width: 100%;
151
+ }
152
+
153
+ .lexical-media-video {
154
+ max-height: min(520px, 62vh);
155
+ width: 100%;
156
+ }
157
+
158
+ .lexical-media-audio {
159
+ padding: 0.75rem;
160
+ width: min(100%, 40rem);
161
+ }
162
+
163
+ /* File reference typeahead */
164
+ .moldable-editor-reference-menu {
165
+ background: hsl(var(--popover));
166
+ border: 1px solid hsl(var(--border));
167
+ border-radius: 0.5rem;
168
+ box-shadow:
169
+ 0 16px 40px hsl(var(--foreground) / 0.14),
170
+ 0 2px 8px hsl(var(--foreground) / 0.08);
171
+ display: flex;
172
+ flex-direction: column;
173
+ gap: 0.125rem;
174
+ min-width: min(22rem, calc(100vw - 2rem));
175
+ padding: 0.25rem;
176
+ z-index: 50;
177
+ }
178
+
179
+ .moldable-editor-reference-option {
180
+ align-items: center;
181
+ background: transparent;
182
+ border: 0;
183
+ border-radius: 0.375rem;
184
+ color: hsl(var(--popover-foreground));
185
+ cursor: pointer;
186
+ display: grid;
187
+ gap: 0.125rem;
188
+ justify-items: stretch;
189
+ min-height: 2.75rem;
190
+ padding: 0.45rem 0.625rem;
191
+ text-align: left;
192
+ width: 100%;
193
+ }
194
+
195
+ .moldable-editor-reference-option:hover,
196
+ .moldable-editor-reference-option-selected {
197
+ background: hsl(var(--muted));
198
+ }
199
+
200
+ .moldable-editor-reference-title,
201
+ .moldable-editor-reference-path {
202
+ display: block;
203
+ min-width: 0;
204
+ overflow: hidden;
205
+ text-overflow: ellipsis;
206
+ white-space: nowrap;
207
+ }
208
+
209
+ .moldable-editor-reference-title {
210
+ color: hsl(var(--foreground));
211
+ font-size: 0.8125rem;
212
+ font-weight: 500;
213
+ line-height: 1.15;
214
+ }
215
+
216
+ .moldable-editor-reference-path {
217
+ color: hsl(var(--muted-foreground));
218
+ font-size: 0.71875rem;
219
+ line-height: 1.15;
220
+ }
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=translation.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"translation.test.d.ts","sourceRoot":"","sources":["../../../src/lib/lexical/translation.test.ts"],"names":[],"mappings":""}