@use-kona/editor 0.1.24-dnd.7 → 0.1.25

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.
@@ -1,9 +1,13 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { useStore } from "@nanostores/react";
2
3
  import is_hotkey from "is-hotkey";
3
- import react, { isValidElement, useCallback } from "react";
4
- import { Transforms } from "slate";
5
- import { Editable, useReadOnly } from "slate-react";
4
+ import { atom } from "nanostores";
5
+ import react, { isValidElement, useCallback, useState } from "react";
6
+ import { Editor, Element, Path, Transforms } from "slate";
7
+ import { Editable, ReactEditor, useReadOnly } from "slate-react";
6
8
  import { BaseElement } from "../elements/BaseElement.js";
9
+ import { Block } from "../elements/Block.js";
10
+ import { EditorProvider } from "../provider.js";
7
11
  import { deserialize } from "./deserialize.js";
8
12
  import styles_module from "./styles.module.js";
9
13
  const SUPPORTED_HANDLERS = [
@@ -13,6 +17,8 @@ const SUPPORTED_HANDLERS = [
13
17
  'onPaste'
14
18
  ];
15
19
  const createEditable = (editor, plugins)=>({ readOnly })=>{
20
+ const [selectedNodes] = useState(()=>atom([]));
21
+ const $selectedNodes = useStore(selectedNodes);
16
22
  const renderElement = (props)=>{
17
23
  let result = null;
18
24
  for (const plugin of plugins)for (const element of plugin.blocks || []){
@@ -24,13 +30,20 @@ const createEditable = (editor, plugins)=>({ readOnly })=>{
24
30
  ...props,
25
31
  children: result || props.children
26
32
  });
27
- return result || /*#__PURE__*/ jsx(BaseElement, {
33
+ if (Editor.isInline(editor, props.element)) return result || /*#__PURE__*/ jsx(BaseElement, {
28
34
  ...props
29
35
  });
36
+ return /*#__PURE__*/ jsx(Block, {
37
+ element: props.element,
38
+ attributes: props.attributes,
39
+ children: result || /*#__PURE__*/ jsx(BaseElement, {
40
+ ...props
41
+ })
42
+ });
30
43
  };
31
44
  const renderLeaf = (props)=>{
32
45
  let result;
33
- for (const plugin of plugins)plugin.leafs?.map((element)=>{
46
+ for (const plugin of plugins)plugin.leafs?.forEach((element)=>{
34
47
  const newElement = element.render({
35
48
  ...props,
36
49
  children: result || props.children
@@ -41,6 +54,121 @@ const createEditable = (editor, plugins)=>({ readOnly })=>{
41
54
  };
42
55
  const handleHotkey = (event)=>{
43
56
  if ('Tab' === event.key) event.preventDefault();
57
+ if ('Escape' === event.key) {
58
+ const currentBlock = Editor.above(editor, {
59
+ at: editor.selection?.focus,
60
+ match: (n)=>Element.isElement(n) && Editor.isBlock(editor, n)
61
+ });
62
+ if (!currentBlock) return;
63
+ if (0 === $selectedNodes.length) selectedNodes.set([
64
+ currentBlock[0]
65
+ ]);
66
+ else {
67
+ selectedNodes.set([]);
68
+ ReactEditor.blur(editor);
69
+ }
70
+ } else if ('ArrowDown' === event.key) {
71
+ const currentBlock = Editor.above(editor, {
72
+ at: editor.selection?.focus,
73
+ match: (n)=>Element.isElement(n)
74
+ });
75
+ if (!currentBlock) return;
76
+ const nextBlock = Editor.next(editor, {
77
+ at: currentBlock[1],
78
+ match: (n)=>Element.isElement(n),
79
+ voids: true
80
+ });
81
+ if (nextBlock) {
82
+ if (Editor.isVoid(editor, nextBlock[0])) {
83
+ event.preventDefault();
84
+ selectedNodes.set([
85
+ nextBlock[0]
86
+ ]);
87
+ } else selectedNodes.set([]);
88
+ if (Editor.isVoid(editor, currentBlock[0]) || Editor.isVoid(editor, nextBlock[0])) {
89
+ event.preventDefault();
90
+ Transforms.select(editor, nextBlock[1]);
91
+ Transforms.collapse(editor, {
92
+ edge: 'start'
93
+ });
94
+ }
95
+ }
96
+ } else if ('ArrowUp' === event.key) {
97
+ const currentBlock = Editor.above(editor, {
98
+ at: editor.selection?.focus,
99
+ match: (n)=>Element.isElement(n)
100
+ });
101
+ if (!currentBlock) return;
102
+ const prevBlock = Editor.previous(editor, {
103
+ at: currentBlock[1],
104
+ match: (n)=>Element.isElement(n),
105
+ voids: true
106
+ });
107
+ if (prevBlock) {
108
+ if (Editor.isVoid(editor, prevBlock[0])) {
109
+ event.preventDefault();
110
+ selectedNodes.set([
111
+ prevBlock[0]
112
+ ]);
113
+ } else selectedNodes.set([]);
114
+ if (Editor.isVoid(editor, currentBlock[0]) || Editor.isVoid(editor, prevBlock[0])) {
115
+ event.preventDefault();
116
+ Transforms.select(editor, prevBlock[1]);
117
+ Transforms.collapse(editor, {
118
+ edge: 'end'
119
+ });
120
+ }
121
+ }
122
+ } else if ('Backspace' === event.key) {
123
+ const currentBlock = Editor.above(editor, {
124
+ at: editor.selection?.focus,
125
+ match: (n)=>Element.isElement(n) && Editor.isBlock(editor, n)
126
+ });
127
+ if (!currentBlock) return;
128
+ if (editor.selection?.focus.offset !== 0) return;
129
+ const prevBlock = Editor.previous(editor, {
130
+ at: currentBlock[1],
131
+ match: (n)=>Element.isElement(n),
132
+ voids: true
133
+ });
134
+ if (prevBlock && Editor.isVoid(editor, prevBlock[0])) {
135
+ event.preventDefault();
136
+ Transforms.removeNodes(editor, {
137
+ at: currentBlock[1]
138
+ });
139
+ Transforms.select(editor, prevBlock[1]);
140
+ selectedNodes.set([
141
+ prevBlock[0]
142
+ ]);
143
+ Transforms.collapse(editor, {
144
+ edge: 'start'
145
+ });
146
+ }
147
+ } else if ('Enter' === event.key) {
148
+ const currentBlock = Editor.above(editor, {
149
+ at: editor.selection?.focus,
150
+ match: (n)=>Element.isElement(n)
151
+ });
152
+ if (!currentBlock) return;
153
+ if (Editor.isVoid(editor, currentBlock[0])) {
154
+ event.preventDefault();
155
+ Transforms.insertNodes(editor, {
156
+ type: 'paragraph',
157
+ children: [
158
+ {
159
+ text: ''
160
+ }
161
+ ]
162
+ }, {
163
+ at: Path.next(currentBlock[1])
164
+ });
165
+ selectedNodes.set([]);
166
+ Transforms.select(editor, Path.next(currentBlock[1]));
167
+ Transforms.collapse(editor, {
168
+ edge: 'start'
169
+ });
170
+ }
171
+ }
44
172
  for (const plugin of plugins)if (plugin.hotkeys) {
45
173
  for (const hotkey of plugin.hotkeys)if (is_hotkey(hotkey[0], event) && hotkey[1]) {
46
174
  event.preventDefault();
@@ -95,7 +223,7 @@ const createEditable = (editor, plugins)=>({ readOnly })=>{
95
223
  },
96
224
  children: [
97
225
  ui.map((p, index)=>/*#__PURE__*/ jsx(react.Fragment, {
98
- children: p.ui({
226
+ children: p.ui?.({
99
227
  readOnly,
100
228
  children,
101
229
  editor
@@ -111,15 +239,18 @@ const createEditable = (editor, plugins)=>({ readOnly })=>{
111
239
  editor,
112
240
  plugins.filter
113
241
  ]);
114
- return /*#__PURE__*/ jsx("div", {
115
- className: styles_module.root,
116
- children: /*#__PURE__*/ jsx(Ui, {
117
- children: /*#__PURE__*/ jsx(Editable, {
118
- decorate: decorate,
119
- renderElement: renderElement,
120
- renderLeaf: renderLeaf,
121
- readOnly: readOnly,
122
- ...handlers
242
+ return /*#__PURE__*/ jsx(EditorProvider, {
243
+ selectedNodes: selectedNodes,
244
+ children: /*#__PURE__*/ jsx("div", {
245
+ className: styles_module.root,
246
+ children: /*#__PURE__*/ jsx(Ui, {
247
+ children: /*#__PURE__*/ jsx(Editable, {
248
+ decorate: decorate,
249
+ renderElement: renderElement,
250
+ renderLeaf: renderLeaf,
251
+ readOnly: readOnly,
252
+ ...handlers
253
+ })
123
254
  })
124
255
  })
125
256
  });
@@ -136,7 +136,7 @@ const createEditor_createEditor = (plugins)=>()=>{
136
136
  const hasOnBeforeDelete = match?.onBeforeDelete;
137
137
  if (!hasOnBeforeDelete) return void deleteFragment(options);
138
138
  {
139
- const result = await match.onBeforeDelete([
139
+ const result = await match.onBeforeDelete?.([
140
140
  node
141
141
  ]);
142
142
  if (!result) return;
@@ -2,6 +2,8 @@ import "./styles_module.css";
2
2
  const styles_module = {
3
3
  root: "root-e1gdll",
4
4
  ui: "ui-I68yAA",
5
- editor: "editor-K2itFp"
5
+ editor: "editor-K2itFp",
6
+ block: "block-sBBbL1",
7
+ selected: "selected-dNsAIz"
6
8
  };
7
9
  export { styles_module as default };
@@ -33,3 +33,16 @@
33
33
  }
34
34
  }
35
35
 
36
+ .block-sBBbL1 {
37
+ transition: background-color .2s ease-in-out;
38
+
39
+ &.selected-dNsAIz {
40
+ background-color: var(--kona-editor-selected-color);
41
+
42
+ & > * {
43
+ transition: transform .2s ease-in-out;
44
+ transform: scale(.98);
45
+ }
46
+ }
47
+ }
48
+
@@ -0,0 +1,25 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import { useStore } from "@nanostores/react";
3
+ import { Editor } from "slate";
4
+ import { useSlateStatic } from "slate-react";
5
+ import styles_module from "../core/styles.module.js";
6
+ import { useEditorContext } from "../provider.js";
7
+ const Block = (props)=>{
8
+ const editor = useSlateStatic();
9
+ const { selectedNodes } = useEditorContext();
10
+ const $selectedNodes = useStore(selectedNodes);
11
+ const isSelected = $selectedNodes.some((node)=>node === props.element);
12
+ const classes = isSelected ? `${styles_module.block} ${styles_module.selected}` : styles_module.block;
13
+ const handleClick = ()=>{
14
+ if (Editor.isVoid(editor, props.element)) selectedNodes.set([
15
+ props.element
16
+ ]);
17
+ else selectedNodes.set([]);
18
+ };
19
+ return /*#__PURE__*/ jsx("div", {
20
+ className: classes,
21
+ onClick: handleClick,
22
+ children: props.children
23
+ });
24
+ };
25
+ export { Block };
package/dist/index.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { deserialize } from "./core/deserialize.js";
2
2
  import { serialize } from "./core/serialize.js";
3
3
  import { defaultValue } from "./defaultValue.js";
4
+ import { useEditorContext } from "./provider.js";
4
5
  import { isEmpty } from "./utils.js";
5
6
  export * from "./editor.js";
6
7
  export * from "./plugins/index.js";
7
- export { defaultValue, deserialize, isEmpty, serialize };
8
+ export { defaultValue, deserialize, isEmpty, serialize, useEditorContext };
@@ -78,7 +78,7 @@ class CodeBlockPlugin {
78
78
  if ('a' === event.key && (event.ctrlKey || event.metaKey) && node && node.type === CodeBlockPlugin.CODE_ELEMENT) {
79
79
  event.preventDefault();
80
80
  const entry = Editor.above(editor, {
81
- match: (n)=>!Editor.isEditor(n) && Element.isElement(n) && n.type === CodeBlockPlugin.CODE_ELEMENT
81
+ match: (n)=>Element.isElement(n) && n.type === CodeBlockPlugin.CODE_ELEMENT
82
82
  });
83
83
  if (entry) {
84
84
  const [, codeBlockPath] = entry;
@@ -1,38 +1,20 @@
1
+ import { jsx } from "react/jsx-runtime";
1
2
  import { useStore } from "@nanostores/react";
2
- import { nanoid } from "nanoid";
3
- import { map } from "nanostores";
4
3
  import { useEffect, useRef, useState } from "react";
5
4
  import { useDrag, useDrop } from "react-dnd";
6
5
  import { NativeTypes } from "react-dnd-html5-backend";
7
6
  import { Editor, Element, Path, Transforms } from "slate";
8
7
  import { ReactEditor, useReadOnly, useSlate } from "slate-react";
8
+ import { useEditorContext } from "../../provider.js";
9
9
  import { getNodesByNodeIds } from "./utils.js";
10
10
  class DnDPlugin {
11
11
  options;
12
- store;
13
- editors;
14
12
  constructor(options){
15
13
  this.options = options;
16
- this.store = map({
17
- selected: new Set()
18
- });
19
- this.editors = new WeakMap();
20
14
  }
21
15
  init(editor) {
22
- this.editors.set(editor, nanoid());
23
16
  return editor;
24
17
  }
25
- getEditorId(editor) {
26
- return this.editors.get(editor);
27
- }
28
- handleToggleSelected = (nodeId)=>{
29
- const selected = new Set(this.store.get().selected);
30
- if (selected.has(nodeId)) selected.delete(nodeId);
31
- else selected.add(nodeId);
32
- this.store.set({
33
- selected
34
- });
35
- };
36
18
  static DND_BLOCK_ELEMENT = 'block';
37
19
  handlers = {
38
20
  onDrop: (event)=>{
@@ -41,7 +23,8 @@ class DnDPlugin {
41
23
  };
42
24
  renderBlock = (props)=>{
43
25
  const editor = useSlate();
44
- const $store = useStore(this.store);
26
+ const { selectedNodes } = useEditorContext();
27
+ const $selectedNodes = useStore(selectedNodes);
45
28
  const isCopying = useRef(false);
46
29
  const options = this.options;
47
30
  const generateId = this.options?.nodeIdPlugin?.generateId;
@@ -53,8 +36,8 @@ class DnDPlugin {
53
36
  const [, drag, preview] = useDrag({
54
37
  type: customType?.type || DnDPlugin.DND_BLOCK_ELEMENT,
55
38
  item: ()=>{
56
- const selected = Array.from($store.selected.values());
57
- const nodes = selected.length > 0 && selected.includes(currentElement.nodeId) ? getNodesByNodeIds(editor, selected) : [
39
+ const selected = $selectedNodes;
40
+ const nodes = selected.length > 0 && selected.find((node)=>node.nodeId === currentElement.nodeId) ? getNodesByNodeIds(editor, selected.map((node)=>node.nodeId)) : [
58
41
  structuredClone(props.element)
59
42
  ];
60
43
  const item = this.options.customTypes?.[currentElement.type]?.getData?.(currentElement);
@@ -69,7 +52,6 @@ class DnDPlugin {
69
52
  items,
70
53
  source: {
71
54
  editor,
72
- editorId: this.getEditorId(editor),
73
55
  documentId: options.documentId
74
56
  }
75
57
  };
@@ -77,7 +59,7 @@ class DnDPlugin {
77
59
  },
78
60
  canDrag: !isReadOnly,
79
61
  end: ()=>{
80
- $store.selected.clear();
62
+ selectedNodes.set([]);
81
63
  }
82
64
  });
83
65
  const allCustomTypes = Object.values(options.customTypes || {}).map((customType)=>customType.type);
@@ -108,7 +90,7 @@ class DnDPlugin {
108
90
  const itemType = monitor.getItemType();
109
91
  const sourceEditor = 'inner' === data.kind ? data.source?.editor : null;
110
92
  const isSameDocument = 'inner' === data.kind && data.source?.documentId === this.options.documentId && void 0 !== this.options.documentId;
111
- const isSameEditor = 'inner' === data.kind && data.source?.editorId === this.editors.get(editor) && void 0 !== this.editors.get(editor);
93
+ const isSameEditor = 'inner' === data.kind && data.source?.editor === editor;
112
94
  const sourceTo = ReactEditor.findPath(editor, props.element);
113
95
  if (!sourceTo) return;
114
96
  const insertAt = getDropPath(editor, sourceTo, dropPosition || 'bottom');
@@ -140,7 +122,7 @@ class DnDPlugin {
140
122
  insertNodes(editor, insertAt, clonedNodes);
141
123
  if (!isCopying.current && sourceEditor) removeNodes(sourceEditor, nodeIds);
142
124
  }
143
- $store.selected.clear();
125
+ selectedNodes.set([]);
144
126
  break;
145
127
  }
146
128
  }
@@ -159,19 +141,27 @@ class DnDPlugin {
159
141
  ]);
160
142
  if (this.options.ignoreNodes?.includes(props.element.type)) return props.children;
161
143
  if (editor.isInline(props.element)) return props.children;
162
- return options.renderBlock?.({
163
- props,
164
- dragRef: drag,
165
- dropRef: connectDropRef,
166
- previewRef: preview,
167
- position: dropPosition,
168
- selected: 'string' == typeof currentElement.nodeId ? $store.selected.has(currentElement.nodeId) : false,
169
- onToggleSelected: ()=>{
170
- if ('string' == typeof currentElement.nodeId) this.handleToggleSelected(currentElement.nodeId);
171
- },
172
- onNativeDrop: (event)=>{
173
- isCopying.current = event.altKey;
174
- }
144
+ return /*#__PURE__*/ jsx("div", {
145
+ children: options.renderBlock?.({
146
+ props,
147
+ dragRef: drag,
148
+ dropRef: connectDropRef,
149
+ previewRef: preview,
150
+ position: dropPosition,
151
+ onNativeDrop: (event)=>{
152
+ isCopying.current = event.altKey;
153
+ },
154
+ onToggleSelected: (event)=>{
155
+ event.stopPropagation();
156
+ if ($selectedNodes.includes(props.element)) selectedNodes.set([
157
+ ...$selectedNodes
158
+ ].filter((element)=>element !== props.element));
159
+ else selectedNodes.set([
160
+ ...$selectedNodes,
161
+ props.element
162
+ ]);
163
+ }
164
+ })
175
165
  });
176
166
  };
177
167
  }
@@ -85,7 +85,7 @@ const isBlockActive = (editor, type)=>{
85
85
  if (!selection) return false;
86
86
  const [match] = Array.from(Editor.nodes(editor, {
87
87
  at: Editor.unhangRange(editor, selection),
88
- match: (node)=>!Editor.isEditor(node) && Element.isElement(node) && node.type === type
88
+ match: (node)=>Element.isElement(node) && node.type === type
89
89
  }));
90
90
  return !!match;
91
91
  };
@@ -195,7 +195,7 @@ class ListsPlugin {
195
195
  if (!selection) return false;
196
196
  const [match] = Array.from(Editor.nodes(editor, {
197
197
  at: Editor.unhangRange(editor, selection),
198
- match: (node)=>!Editor.isEditor(node) && Element.isElement(node) && node.type === type
198
+ match: (node)=>Element.isElement(node) && node.type === type
199
199
  }));
200
200
  return !!match;
201
201
  };
@@ -0,0 +1,15 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import { createContext, useContext } from "react";
3
+ const Context = /*#__PURE__*/ createContext(null);
4
+ const EditorProvider = ({ children, selectedNodes })=>/*#__PURE__*/ jsx(Context.Provider, {
5
+ value: {
6
+ selectedNodes
7
+ },
8
+ children: children
9
+ });
10
+ const useEditorContext = ()=>{
11
+ const context = useContext(Context);
12
+ if (!context) throw new Error('useEditorContext must be used within an EditorProvider');
13
+ return context;
14
+ };
15
+ export { EditorProvider, useEditorContext };
@@ -1,4 +1,4 @@
1
- import { type Editor } from 'slate';
1
+ import { Editor } from 'slate';
2
2
  import type { IPlugin } from '../types';
3
3
  type Props = {
4
4
  readOnly?: boolean;
@@ -0,0 +1,2 @@
1
+ import { type RenderElementProps } from 'slate-react';
2
+ export declare const Block: (props: RenderElementProps) => import("react/jsx-runtime").JSX.Element;
@@ -4,5 +4,6 @@ export { serialize } from './core/serialize';
4
4
  export { defaultValue } from './defaultValue';
5
5
  export * from './editor';
6
6
  export * from './plugins';
7
+ export { useEditorContext } from './provider';
7
8
  export type { EditorRef, IPlugin } from './types';
8
9
  export { isEmpty } from './utils';
@@ -1,20 +1,13 @@
1
- import { type MapStore } from 'nanostores';
2
1
  import { Editor } from 'slate';
3
2
  import { ReactEditor, type RenderElementProps } from 'slate-react';
4
3
  import type { IPlugin } from '../../types';
5
- import { type Options } from './types';
4
+ import type { Options } from './types';
6
5
  export declare class DnDPlugin implements IPlugin {
7
6
  private options;
8
- store: MapStore<{
9
- selected: Set<string>;
10
- }>;
11
- editors: WeakMap<Editor, string>;
12
7
  constructor(options: Options);
13
8
  init(editor: Editor): import("slate").BaseEditor & ReactEditor & {
14
9
  getCommands: <T extends IPlugin>(plugin: new () => T) => T["commands"];
15
10
  };
16
- getEditorId(editor: Editor): string | undefined;
17
- handleToggleSelected: (nodeId: string) => void;
18
11
  static DND_BLOCK_ELEMENT: string;
19
12
  handlers: {
20
13
  onDrop: (event: DragEvent) => void;
@@ -23,9 +23,8 @@ export type Options = {
23
23
  dropRef: ConnectDropTarget;
24
24
  previewRef: ConnectDragPreview;
25
25
  position: 'top' | 'bottom' | null;
26
- selected?: boolean;
27
- onToggleSelected: () => void;
28
26
  onNativeDrop?: (event: React.DragEvent<HTMLDivElement>) => void;
27
+ onToggleSelected?: (event: React.MouseEvent<HTMLDivElement>) => void;
29
28
  }) => React.ReactNode;
30
29
  ignoreNodes?: string[];
31
30
  customTypes?: {
@@ -0,0 +1,8 @@
1
+ import type { StoreValue } from 'nanostores';
2
+ import type React from 'react';
3
+ import type { EditorContext } from './types';
4
+ export declare const EditorProvider: ({ children, selectedNodes, }: {
5
+ children: React.ReactNode;
6
+ selectedNodes: StoreValue<string[]>;
7
+ }) => import("react/jsx-runtime").JSX.Element;
8
+ export declare const useEditorContext: () => EditorContext;
@@ -1,3 +1,4 @@
1
+ import type { StoreValue } from 'nanostores';
1
2
  import type { KeyboardEvent, ReactElement, ReactNode } from 'react';
2
3
  import type { DecoratedRange, Descendant, Editor, Location, NodeEntry, NodeMatch } from 'slate';
3
4
  import type { RenderElementProps, RenderLeafProps } from 'slate-react';
@@ -57,4 +58,7 @@ export type EditorRef = {
57
58
  };
58
59
  export type Serialize = (node: CustomElement | CustomText, children?: string) => string | undefined;
59
60
  export type Deserialize = (element: HTMLElement, children?: (string | Descendant)[]) => CustomElement | CustomText[] | undefined;
61
+ export type EditorContext = {
62
+ selectedNodes: StoreValue<string[]>;
63
+ };
60
64
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@use-kona/editor",
3
- "version": "0.1.24-dnd.7",
3
+ "version": "0.1.25",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./src/index.ts"
@@ -43,10 +43,11 @@
43
43
  "react-dnd": "^16.0.1",
44
44
  "react-dnd-html5-backend": "^16.0.1",
45
45
  "react-dom": "18",
46
- "slate": "^0.117.2",
46
+ "slate": "^0.126.0",
47
+ "slate-dom": "^0.126.0",
47
48
  "slate-history": "^0.113.1",
48
- "slate-hyperscript": "^0.100.0",
49
- "slate-react": "^0.117.3",
49
+ "slate-hyperscript": "^0.125.0",
50
+ "slate-react": "^0.126.0",
50
51
  "storybook": "^9.0.13",
51
52
  "storybook-addon-rslib": "^2.0.2",
52
53
  "storybook-react-rsbuild": "^2.0.2",
@@ -58,10 +59,11 @@
58
59
  "react-dnd": "^16.0.1",
59
60
  "react-dnd-html5-backend": "^16.0.1",
60
61
  "react-dom": "18",
61
- "slate": "^0.117.2",
62
+ "slate": "^0.126.0",
63
+ "slate-dom": "^0.126.0",
62
64
  "slate-history": "^0.113.1",
63
- "slate-hyperscript": "^0.100.0",
64
- "slate-react": "^0.117.3"
65
+ "slate-hyperscript": "^0.125.0",
66
+ "slate-react": "^0.126.0"
65
67
  },
66
68
  "dependencies": {
67
69
  "@nanostores/react": "^1.0.0",
@@ -69,7 +71,6 @@
69
71
  "escape-html": "^1.0.3",
70
72
  "is-hotkey": "^0.2.0",
71
73
  "is-url": "^1.2.4",
72
- "nanoid": "^5.1.16",
73
74
  "nanostores": "^1.0.1",
74
75
  "prismjs": "^1.30.0"
75
76
  }
@@ -1,4 +1,6 @@
1
+ import { useStore } from '@nanostores/react';
1
2
  import isHotkey from 'is-hotkey';
3
+ import { atom } from 'nanostores';
2
4
  import React, {
3
5
  type ClipboardEvent,
4
6
  isValidElement,
@@ -7,21 +9,28 @@ import React, {
7
9
  type ReactElement,
8
10
  type ReactNode,
9
11
  useCallback,
12
+ useState,
10
13
  } from 'react';
11
14
  import {
12
15
  type DecoratedRange,
13
16
  type Descendant,
14
- type Editor,
17
+ Editor,
18
+ Element,
15
19
  type NodeEntry,
20
+ Path,
16
21
  Transforms,
17
22
  } from 'slate';
18
23
  import {
19
24
  Editable,
25
+ ReactEditor,
20
26
  type RenderElementProps,
21
27
  type RenderLeafProps,
22
28
  useReadOnly,
23
29
  } from 'slate-react';
30
+ import type { CustomElement } from '../../types';
24
31
  import { BaseElement } from '../elements/BaseElement';
32
+ import { Block } from '../elements/Block';
33
+ import { EditorProvider } from '../provider';
25
34
  import type { IPlugin } from '../types';
26
35
  import { deserialize } from './deserialize';
27
36
  import styles from './styles.module.css';
@@ -35,6 +44,10 @@ type Props = {
35
44
  export const createEditable =
36
45
  (editor: Editor, plugins: IPlugin[]) =>
37
46
  ({ readOnly }: Props) => {
47
+ const [selectedNodes] = useState(() => atom<CustomElement[]>([]));
48
+
49
+ const $selectedNodes = useStore(selectedNodes);
50
+
38
51
  const renderElement = (props: RenderElementProps): ReactElement => {
39
52
  let result: ReactElement | null = null;
40
53
 
@@ -56,13 +69,21 @@ export const createEditable =
56
69
  }
57
70
  }
58
71
 
59
- return result || <BaseElement {...props} />;
72
+ if (Editor.isInline(editor, props.element)) {
73
+ return result || <BaseElement {...props} />;
74
+ }
75
+
76
+ return (
77
+ <Block element={props.element} attributes={props.attributes}>
78
+ {result || <BaseElement {...props} />}
79
+ </Block>
80
+ );
60
81
  };
61
82
 
62
83
  const renderLeaf = (props: RenderLeafProps): JSX.Element => {
63
84
  let result: ReactNode;
64
85
  for (const plugin of plugins) {
65
- plugin.leafs?.map((element) => {
86
+ plugin.leafs?.forEach((element) => {
66
87
  const newElement = element.render(
67
88
  {
68
89
  ...props,
@@ -84,6 +105,132 @@ export const createEditable =
84
105
  event.preventDefault();
85
106
  }
86
107
 
108
+ if (event.key === 'Escape') {
109
+ const currentBlock = Editor.above<CustomElement>(editor, {
110
+ at: editor.selection?.focus,
111
+ match: (n) => Element.isElement(n) && Editor.isBlock(editor, n),
112
+ });
113
+
114
+ if (!currentBlock) {
115
+ return;
116
+ }
117
+
118
+ if ($selectedNodes.length === 0) {
119
+ selectedNodes.set([currentBlock[0]]);
120
+ } else {
121
+ selectedNodes.set([]);
122
+ ReactEditor.blur(editor);
123
+ }
124
+ } else if (event.key === 'ArrowDown') {
125
+ const currentBlock = Editor.above(editor, {
126
+ at: editor.selection?.focus,
127
+ match: (n) => Element.isElement(n),
128
+ });
129
+
130
+ if (!currentBlock) return;
131
+
132
+ const nextBlock = Editor.next(editor, {
133
+ at: currentBlock[1],
134
+ match: (n) => Element.isElement(n),
135
+ voids: true,
136
+ });
137
+
138
+ if (nextBlock) {
139
+ if (Editor.isVoid(editor, nextBlock[0])) {
140
+ event.preventDefault();
141
+ selectedNodes.set([nextBlock[0]]);
142
+ } else {
143
+ selectedNodes.set([]);
144
+ }
145
+ if (
146
+ Editor.isVoid(editor, currentBlock[0]) ||
147
+ Editor.isVoid(editor, nextBlock[0])
148
+ ) {
149
+ event.preventDefault();
150
+ Transforms.select(editor, nextBlock[1]);
151
+ Transforms.collapse(editor, { edge: 'start' });
152
+ }
153
+ }
154
+ } else if (event.key === 'ArrowUp') {
155
+ const currentBlock = Editor.above(editor, {
156
+ at: editor.selection?.focus,
157
+ match: (n) => Element.isElement(n),
158
+ });
159
+
160
+ if (!currentBlock) return;
161
+
162
+ const prevBlock = Editor.previous(editor, {
163
+ at: currentBlock[1],
164
+ match: (n) => Element.isElement(n),
165
+ voids: true,
166
+ });
167
+
168
+ if (prevBlock) {
169
+ if (Editor.isVoid(editor, prevBlock[0])) {
170
+ event.preventDefault();
171
+ selectedNodes.set([prevBlock[0]]);
172
+ } else {
173
+ selectedNodes.set([]);
174
+ }
175
+ if (
176
+ Editor.isVoid(editor, currentBlock[0]) ||
177
+ Editor.isVoid(editor, prevBlock[0])
178
+ ) {
179
+ event.preventDefault();
180
+ Transforms.select(editor, prevBlock[1]);
181
+ Transforms.collapse(editor, { edge: 'end' });
182
+ }
183
+ }
184
+ } else if (event.key === 'Backspace') {
185
+ const currentBlock = Editor.above(editor, {
186
+ at: editor.selection?.focus,
187
+ match: (n) => Element.isElement(n) && Editor.isBlock(editor, n),
188
+ });
189
+
190
+ if (!currentBlock) return;
191
+ if (editor.selection?.focus.offset !== 0) return;
192
+
193
+ const prevBlock = Editor.previous(editor, {
194
+ at: currentBlock[1],
195
+ match: (n) => Element.isElement(n),
196
+ voids: true,
197
+ });
198
+
199
+ if (prevBlock && Editor.isVoid(editor, prevBlock[0])) {
200
+ event.preventDefault();
201
+ Transforms.removeNodes(editor, {
202
+ at: currentBlock[1],
203
+ });
204
+ Transforms.select(editor, prevBlock[1]);
205
+ selectedNodes.set([prevBlock[0]]);
206
+ Transforms.collapse(editor, { edge: 'start' });
207
+ }
208
+ } else if (event.key === 'Enter') {
209
+ const currentBlock = Editor.above(editor, {
210
+ at: editor.selection?.focus,
211
+ match: (n) => Element.isElement(n),
212
+ });
213
+
214
+ if (!currentBlock) return;
215
+
216
+ if (Editor.isVoid(editor, currentBlock[0])) {
217
+ event.preventDefault();
218
+ Transforms.insertNodes(
219
+ editor,
220
+ {
221
+ type: 'paragraph',
222
+ children: [{ text: '' }],
223
+ },
224
+ {
225
+ at: Path.next(currentBlock[1]),
226
+ },
227
+ );
228
+ selectedNodes.set([]);
229
+ Transforms.select(editor, Path.next(currentBlock[1]));
230
+ Transforms.collapse(editor, { edge: 'start' });
231
+ }
232
+ }
233
+
87
234
  for (const plugin of plugins) {
88
235
  if (plugin.hotkeys) {
89
236
  for (const hotkey of plugin.hotkeys) {
@@ -154,12 +301,15 @@ export const createEditable =
154
301
  }
155
302
 
156
303
  return (
157
- <div className={styles.ui} onClick={(event) => {
158
- event.stopPropagation();
159
- }}>
304
+ <div
305
+ className={styles.ui}
306
+ onClick={(event) => {
307
+ event.stopPropagation();
308
+ }}
309
+ >
160
310
  {ui.map((p, index) => (
161
311
  <React.Fragment key={index}>
162
- {p.ui!({ readOnly, children, editor })}
312
+ {p.ui?.({ readOnly, children, editor })}
163
313
  </React.Fragment>
164
314
  ))}
165
315
  <div className={styles.editor}>{children}</div>
@@ -170,16 +320,18 @@ export const createEditable =
170
320
  );
171
321
 
172
322
  return (
173
- <div className={styles.root}>
174
- <Ui>
175
- <Editable
176
- decorate={decorate}
177
- renderElement={renderElement}
178
- renderLeaf={renderLeaf}
179
- readOnly={readOnly}
180
- {...handlers}
181
- />
182
- </Ui>
183
- </div>
323
+ <EditorProvider selectedNodes={selectedNodes}>
324
+ <div className={styles.root}>
325
+ <Ui>
326
+ <Editable
327
+ decorate={decorate}
328
+ renderElement={renderElement}
329
+ renderLeaf={renderLeaf}
330
+ readOnly={readOnly}
331
+ {...handlers}
332
+ />
333
+ </Ui>
334
+ </div>
335
+ </EditorProvider>
184
336
  );
185
337
  };
@@ -219,7 +219,7 @@ export const createEditor = (plugins: IPlugin[]) => () => {
219
219
  const hasOnBeforeDelete = match?.onBeforeDelete;
220
220
 
221
221
  if (hasOnBeforeDelete) {
222
- const result = await match.onBeforeDelete!([node]);
222
+ const result = await match.onBeforeDelete?.([node]);
223
223
 
224
224
  if (result) {
225
225
  Transforms.removeNodes(editorWithPlugins, {
@@ -32,3 +32,15 @@
32
32
  outline: none;
33
33
  }
34
34
  }
35
+
36
+ .block {
37
+ transition: background-color 0.2s ease-in-out;
38
+
39
+ &.selected {
40
+ background-color: var(--kona-editor-selected-color);
41
+ & > * {
42
+ transition: transform 0.2s ease-in-out;
43
+ transform: scale(0.98);
44
+ }
45
+ }
46
+ }
package/src/editor.tsx CHANGED
@@ -1,5 +1,6 @@
1
+ import { atom } from 'nanostores';
1
2
  import { forwardRef, useCallback, useImperativeHandle, useState } from 'react';
2
- import {type Descendant, Editor, type NodeMatch, Transforms} from 'slate';
3
+ import { type Descendant, Editor, type NodeMatch, Transforms } from 'slate';
3
4
  import { ReactEditor, Slate } from 'slate-react';
4
5
  import type { CustomElement } from '../types';
5
6
  import { createEditable } from './core/createEditable';
@@ -54,8 +55,8 @@ export const KonaEditor = forwardRef<EditorRef, KonaEditorProps>(
54
55
  },
55
56
  focus: (mode?: 'end') => {
56
57
  if (mode === 'end') {
57
- const endPoint = Editor.end(editor, [])
58
- Transforms.select(editor, endPoint)
58
+ const endPoint = Editor.end(editor, []);
59
+ Transforms.select(editor, endPoint);
59
60
  }
60
61
 
61
62
  ReactEditor.focus(editor);
@@ -0,0 +1,31 @@
1
+ import { useStore } from '@nanostores/react';
2
+ import { Editor } from 'slate';
3
+ import { type RenderElementProps, useSlateStatic } from 'slate-react';
4
+ import styles from '../core/styles.module.css';
5
+ import { useEditorContext } from '../provider';
6
+
7
+ export const Block = (props: RenderElementProps) => {
8
+ const editor = useSlateStatic();
9
+ const { selectedNodes } = useEditorContext();
10
+ const $selectedNodes = useStore(selectedNodes);
11
+
12
+ const isSelected = $selectedNodes.some((node) => node === props.element);
13
+
14
+ const classes = isSelected
15
+ ? `${styles.block} ${styles.selected}`
16
+ : styles.block;
17
+
18
+ const handleClick = () => {
19
+ if (Editor.isVoid(editor, props.element)) {
20
+ selectedNodes.set([props.element]);
21
+ } else {
22
+ selectedNodes.set([]);
23
+ }
24
+ };
25
+
26
+ return (
27
+ <div className={classes} onClick={handleClick}>
28
+ {props.children}
29
+ </div>
30
+ );
31
+ };
package/src/index.ts CHANGED
@@ -4,5 +4,6 @@ export { serialize } from './core/serialize';
4
4
  export { defaultValue } from './defaultValue';
5
5
  export * from './editor';
6
6
  export * from './plugins';
7
+ export { useEditorContext } from './provider';
7
8
  export type { EditorRef, IPlugin } from './types';
8
9
  export { isEmpty } from './utils';
@@ -117,9 +117,7 @@ export class CodeBlockPlugin implements IPlugin {
117
117
  // Find the code block node and its path
118
118
  const entry = Editor.above(editor, {
119
119
  match: (n) =>
120
- !Editor.isEditor(n) &&
121
- Element.isElement(n) &&
122
- n.type === CodeBlockPlugin.CODE_ELEMENT,
120
+ Element.isElement(n) && n.type === CodeBlockPlugin.CODE_ELEMENT,
123
121
  });
124
122
 
125
123
  if (entry) {
@@ -1,6 +1,4 @@
1
1
  import { useStore } from '@nanostores/react';
2
- import { nanoid } from 'nanoid';
3
- import { type MapStore, map } from 'nanostores';
4
2
  import { useEffect, useRef, useState } from 'react';
5
3
  import { type ConnectDropTarget, useDrag, useDrop } from 'react-dnd';
6
4
  import { NativeTypes } from 'react-dnd-html5-backend';
@@ -12,49 +10,18 @@ import {
12
10
  useSlate,
13
11
  } from 'slate-react';
14
12
  import type { CustomElement } from '../../../types';
13
+ import { useEditorContext } from '../../provider';
15
14
  import type { IPlugin } from '../../types';
16
- import {
17
- type EditorDragItem,
18
- isNodeWithId,
19
- type NodeWithId,
20
- type Options,
21
- } from './types';
15
+ import type { EditorDragItem, NodeWithId, Options } from './types';
22
16
  import { getNodesByNodeIds } from './utils';
23
17
 
24
18
  export class DnDPlugin implements IPlugin {
25
- store: MapStore<{ selected: Set<string> }>;
26
- editors: WeakMap<Editor, string>;
27
-
28
- constructor(private options: Options) {
29
- this.store = map({
30
- selected: new Set(),
31
- });
32
- this.editors = new WeakMap();
33
- }
19
+ constructor(private options: Options) {}
34
20
 
35
21
  init(editor: Editor) {
36
- this.editors.set(editor, nanoid());
37
-
38
22
  return editor;
39
23
  }
40
24
 
41
- getEditorId(editor: Editor) {
42
- return this.editors.get(editor);
43
- }
44
-
45
- handleToggleSelected = (nodeId: string) => {
46
- const selected = new Set(this.store.get().selected);
47
- if (selected.has(nodeId)) {
48
- selected.delete(nodeId);
49
- } else {
50
- selected.add(nodeId);
51
- }
52
-
53
- this.store.set({
54
- selected,
55
- });
56
- };
57
-
58
25
  static DND_BLOCK_ELEMENT = 'block';
59
26
 
60
27
  handlers = {
@@ -66,7 +33,8 @@ export class DnDPlugin implements IPlugin {
66
33
  renderBlock = (props: RenderElementProps) => {
67
34
  const editor = useSlate();
68
35
 
69
- const $store = useStore(this.store);
36
+ const { selectedNodes } = useEditorContext();
37
+ const $selectedNodes = useStore(selectedNodes);
70
38
 
71
39
  const isCopying = useRef(false);
72
40
  const options = this.options;
@@ -85,7 +53,7 @@ export class DnDPlugin implements IPlugin {
85
53
  const [, drag, preview] = useDrag({
86
54
  type: customType?.type || DnDPlugin.DND_BLOCK_ELEMENT,
87
55
  item: () => {
88
- const selected = Array.from($store.selected.values());
56
+ const selected: Array<NodeWithId> = $selectedNodes;
89
57
 
90
58
  /**
91
59
  * We copy nodes that the user is dragging.
@@ -93,8 +61,12 @@ export class DnDPlugin implements IPlugin {
93
61
  * we copy selected nodes. Otherwise, we copy only the current node.
94
62
  */
95
63
  const nodes =
96
- selected.length > 0 && selected.includes(currentElement.nodeId)
97
- ? getNodesByNodeIds(editor, selected)
64
+ selected.length > 0 &&
65
+ selected.find((node) => node.nodeId === currentElement.nodeId)
66
+ ? getNodesByNodeIds(
67
+ editor,
68
+ selected.map((node) => node.nodeId),
69
+ )
98
70
  : [structuredClone(props.element as NodeWithId)];
99
71
 
100
72
  const item =
@@ -118,7 +90,6 @@ export class DnDPlugin implements IPlugin {
118
90
  items,
119
91
  source: {
120
92
  editor,
121
- editorId: this.getEditorId(editor),
122
93
  documentId: options.documentId,
123
94
  },
124
95
  };
@@ -127,7 +98,7 @@ export class DnDPlugin implements IPlugin {
127
98
  },
128
99
  canDrag: !isReadOnly,
129
100
  end: () => {
130
- $store.selected.clear();
101
+ selectedNodes.set([]);
131
102
  },
132
103
  });
133
104
 
@@ -179,9 +150,7 @@ export class DnDPlugin implements IPlugin {
179
150
  data.source?.documentId === this.options.documentId &&
180
151
  this.options.documentId !== undefined;
181
152
  const isSameEditor =
182
- data.kind === 'inner' &&
183
- data.source?.editorId === this.editors.get(editor) &&
184
- this.editors.get(editor) !== undefined;
153
+ data.kind === 'inner' && data.source?.editor === editor;
185
154
 
186
155
  const sourceTo = ReactEditor.findPath(editor, props.element);
187
156
 
@@ -248,7 +217,7 @@ export class DnDPlugin implements IPlugin {
248
217
  removeNodes(sourceEditor, nodeIds);
249
218
  }
250
219
  }
251
- $store.selected.clear();
220
+ selectedNodes.set([]);
252
221
  break;
253
222
  }
254
223
  }
@@ -276,25 +245,32 @@ export class DnDPlugin implements IPlugin {
276
245
  return props.children;
277
246
  }
278
247
 
279
- return options.renderBlock?.({
280
- props,
281
- dragRef: drag,
282
- dropRef: connectDropRef,
283
- previewRef: preview,
284
- position: dropPosition,
285
- selected:
286
- typeof currentElement.nodeId === 'string'
287
- ? $store.selected.has(currentElement.nodeId)
288
- : false,
289
- onToggleSelected: () => {
290
- if (typeof currentElement.nodeId === 'string') {
291
- this.handleToggleSelected(currentElement.nodeId);
292
- }
293
- },
294
- onNativeDrop: (event) => {
295
- isCopying.current = event.altKey;
296
- },
297
- });
248
+ return (
249
+ <div>
250
+ {options.renderBlock?.({
251
+ props,
252
+ dragRef: drag,
253
+ dropRef: connectDropRef,
254
+ previewRef: preview,
255
+ position: dropPosition,
256
+ onNativeDrop: (event) => {
257
+ isCopying.current = event.altKey;
258
+ },
259
+ onToggleSelected: (event) => {
260
+ event.stopPropagation();
261
+ if ($selectedNodes.includes(props.element)) {
262
+ selectedNodes.set(
263
+ [...$selectedNodes].filter(
264
+ (element) => element !== props.element,
265
+ ),
266
+ );
267
+ } else {
268
+ selectedNodes.set([...$selectedNodes, props.element]);
269
+ }
270
+ },
271
+ })}
272
+ </div>
273
+ );
298
274
  };
299
275
  }
300
276
 
@@ -1,4 +1,3 @@
1
- import { type MapStore, map } from 'nanostores';
2
1
  import type React from 'react';
3
2
  import type {
4
3
  ConnectDragPreview,
@@ -10,11 +9,6 @@ import type { RenderElementProps } from 'slate-react';
10
9
  import type { CustomElement, CustomText } from '../../../types';
11
10
  import type { NodeIdPlugin } from '../NodeIdPlugin';
12
11
 
13
- type DropParams = {
14
- editor: Editor;
15
- item: unknown;
16
- };
17
-
18
12
  export type NodeWithId = Omit<CustomElement, 'children'> & {
19
13
  nodeId: string;
20
14
  children: (NodeWithId | CustomText)[];
@@ -38,9 +32,8 @@ export type Options = {
38
32
  dropRef: ConnectDropTarget;
39
33
  previewRef: ConnectDragPreview;
40
34
  position: 'top' | 'bottom' | null;
41
- selected?: boolean;
42
- onToggleSelected: () => void;
43
35
  onNativeDrop?: (event: React.DragEvent<HTMLDivElement>) => void;
36
+ onToggleSelected?: (event: React.MouseEvent<HTMLDivElement>) => void;
44
37
  }) => React.ReactNode;
45
38
  ignoreNodes?: string[];
46
39
  customTypes?: {
@@ -124,8 +124,7 @@ const isBlockActive = (editor, type) => {
124
124
  const [match] = Array.from(
125
125
  Editor.nodes(editor, {
126
126
  at: Editor.unhangRange(editor, selection),
127
- match: (node) =>
128
- !Editor.isEditor(node) && Element.isElement(node) && node.type === type,
127
+ match: (node) => Element.isElement(node) && node.type === type,
129
128
  }),
130
129
  );
131
130
 
@@ -96,7 +96,10 @@ export class ListsPlugin implements IPlugin {
96
96
  }
97
97
 
98
98
  /* Merge adjacent lists */
99
- if (this.isList(editor, node as CustomElement) && Path.hasPrevious(path)) {
99
+ if (
100
+ this.isList(editor, node as CustomElement) &&
101
+ Path.hasPrevious(path)
102
+ ) {
100
103
  const prevPath = Path.previous(path);
101
104
  const prevNode = Node.get(editor, prevPath) as CustomElement;
102
105
 
@@ -331,10 +334,7 @@ export class ListsPlugin implements IPlugin {
331
334
  const [match] = Array.from(
332
335
  Editor.nodes(editor, {
333
336
  at: Editor.unhangRange(editor, selection),
334
- match: (node) =>
335
- !Editor.isEditor(node) &&
336
- Element.isElement(node) &&
337
- node.type === type,
337
+ match: (node) => Element.isElement(node) && node.type === type,
338
338
  }),
339
339
  );
340
340
 
@@ -0,0 +1,28 @@
1
+ import type { StoreValue } from 'nanostores';
2
+ import type React from 'react';
3
+ import { createContext, useContext } from 'react';
4
+ import type { EditorContext } from './types';
5
+
6
+ const Context = createContext<EditorContext | null>(null);
7
+
8
+ export const EditorProvider = ({
9
+ children,
10
+ selectedNodes,
11
+ }: {
12
+ children: React.ReactNode;
13
+ selectedNodes: StoreValue<string[]>;
14
+ }) => {
15
+ return (
16
+ <Context.Provider value={{ selectedNodes }}>{children}</Context.Provider>
17
+ );
18
+ };
19
+
20
+ export const useEditorContext = () => {
21
+ const context = useContext(Context);
22
+
23
+ if (!context) {
24
+ throw new Error('useEditorContext must be used within an EditorProvider');
25
+ }
26
+
27
+ return context;
28
+ };
package/src/types.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { StoreValue } from 'nanostores';
1
2
  import type { KeyboardEvent, ReactElement, ReactNode } from 'react';
2
3
  import type {
3
4
  DecoratedRange,
@@ -98,3 +99,7 @@ export type Deserialize = (
98
99
  element: HTMLElement,
99
100
  children?: (string | Descendant)[],
100
101
  ) => CustomElement | CustomText[] | undefined;
102
+
103
+ export type EditorContext = {
104
+ selectedNodes: StoreValue<string[]>;
105
+ };