@peers-app/peers-ui 0.15.4 → 0.16.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/.github/workflows/publish.yml +3 -3
- package/dist/components/markdown-editor/editor.js +3 -1
- package/dist/components/markdown-editor/move-line-plugin.d.ts +18 -0
- package/dist/components/markdown-editor/move-line-plugin.js +161 -0
- package/dist/components/markdown-editor/select-line-boundary-plugin.d.ts +8 -0
- package/dist/components/markdown-editor/select-line-boundary-plugin.js +71 -0
- package/dist/screens/data-explorer/data-explorer.js +64 -16
- package/package.json +3 -3
- package/src/components/markdown-editor/editor.tsx +4 -0
- package/src/components/markdown-editor/move-line-plugin.tsx +186 -0
- package/src/components/markdown-editor/select-line-boundary-plugin.tsx +95 -0
- package/src/screens/data-explorer/data-explorer.tsx +271 -76
- package/docs/conversation-tab.md +0 -201
- package/docs/getting-started.md +0 -284
- package/docs/knowledge.md +0 -187
- package/docs/tabs-ui.md +0 -681
- package/docs/user-contacts-ui.md +0 -384
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { $isListItemNode, $isListNode } from "@lexical/list";
|
|
2
|
+
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
|
|
3
|
+
import type { Observable } from "@peers-app/peers-sdk";
|
|
4
|
+
import {
|
|
5
|
+
$createRangeSelection,
|
|
6
|
+
$getNodeByKey,
|
|
7
|
+
$getSelection,
|
|
8
|
+
$isElementNode,
|
|
9
|
+
$isRangeSelection,
|
|
10
|
+
$isRootOrShadowRoot,
|
|
11
|
+
$setSelection,
|
|
12
|
+
COMMAND_PRIORITY_HIGH,
|
|
13
|
+
KEY_DOWN_COMMAND,
|
|
14
|
+
type LexicalNode,
|
|
15
|
+
} from "lexical";
|
|
16
|
+
import { useEffect } from "react";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Walks up from `node` to find the reorderable block for keyboard shortcuts that
|
|
20
|
+
* operate on "lines" (blocks). Returns a list item when inside a list, or the
|
|
21
|
+
* top-level block directly under the root.
|
|
22
|
+
*/
|
|
23
|
+
export function $getMovableBlock(node: LexicalNode): LexicalNode | null {
|
|
24
|
+
let n: LexicalNode | null = node;
|
|
25
|
+
while (n !== null) {
|
|
26
|
+
const parent: LexicalNode | null = n.getParent();
|
|
27
|
+
if (parent === null) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
if ($isListNode(parent) && $isListItemNode(n)) {
|
|
31
|
+
return n;
|
|
32
|
+
}
|
|
33
|
+
if ($isRootOrShadowRoot(parent)) {
|
|
34
|
+
return n;
|
|
35
|
+
}
|
|
36
|
+
n = parent;
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Returns true when `node` sits at the absolute start of `block` — i.e. it is
|
|
43
|
+
* the block itself or lies along the first-child path from block down to node.
|
|
44
|
+
* Used to detect a selection endpoint that bled into the next block at offset 0.
|
|
45
|
+
*/
|
|
46
|
+
function $isFirstPositionInBlock(node: LexicalNode, block: LexicalNode): boolean {
|
|
47
|
+
let n: LexicalNode | null = node;
|
|
48
|
+
while (n !== null && n !== block) {
|
|
49
|
+
if (n.getPreviousSibling() !== null) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
n = n.getParent();
|
|
53
|
+
}
|
|
54
|
+
return n === block;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Registers Alt/Option + ArrowUp/ArrowDown to move the selected block(s) — or the
|
|
59
|
+
* block at the caret — past an adjacent sibling. Supports both collapsed cursors and
|
|
60
|
+
* multi-block selections: the single neighbor block is relocated to the other side of
|
|
61
|
+
* the selection range, which effectively shifts the selected blocks up or down by one.
|
|
62
|
+
* @param props.mentionsOpen — when true, the plugin defers so mention typeahead keeps keyboard focus.
|
|
63
|
+
*/
|
|
64
|
+
export function MoveLineWithAltArrowsPlugin(props: { mentionsOpen: Observable<boolean> }) {
|
|
65
|
+
const [editor] = useLexicalComposerContext();
|
|
66
|
+
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
return editor.registerCommand(
|
|
69
|
+
KEY_DOWN_COMMAND,
|
|
70
|
+
(event: KeyboardEvent) => {
|
|
71
|
+
if (props.mentionsOpen()) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
if (!event.altKey || event.metaKey || event.ctrlKey) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
const code = event.code;
|
|
78
|
+
if (code !== "ArrowUp" && code !== "ArrowDown") {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// KEY_DOWN_COMMAND runs inside Lexical's updateEditorSync; do not call
|
|
83
|
+
// editor.update() here — it would queue and run after this handler returns,
|
|
84
|
+
// so handled would stay false, preventDefault would never run, and the
|
|
85
|
+
// built-in arrow handler + browser would move the caret on top of our swap.
|
|
86
|
+
|
|
87
|
+
const selection = $getSelection();
|
|
88
|
+
if (!$isRangeSelection(selection)) {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const anchorBlock = $getMovableBlock(selection.anchor.getNode());
|
|
93
|
+
const focusBlock = $getMovableBlock(selection.focus.getNode());
|
|
94
|
+
if (anchorBlock === null || focusBlock === null) {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Both endpoints must share the same parent (same nesting level).
|
|
99
|
+
if (anchorBlock.getParent()?.getKey() !== focusBlock.getParent()?.getKey()) {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const anchorIdx = anchorBlock.getIndexWithinParent();
|
|
104
|
+
const focusIdx = focusBlock.getIndexWithinParent();
|
|
105
|
+
const firstBlock = anchorIdx <= focusIdx ? anchorBlock : focusBlock;
|
|
106
|
+
let lastBlock = anchorIdx <= focusIdx ? focusBlock : anchorBlock;
|
|
107
|
+
|
|
108
|
+
let anchorKey = selection.anchor.key;
|
|
109
|
+
let anchorOffset = selection.anchor.offset;
|
|
110
|
+
let anchorType = selection.anchor.type;
|
|
111
|
+
let focusKey = selection.focus.key;
|
|
112
|
+
let focusOffset = selection.focus.offset;
|
|
113
|
+
let focusType = selection.focus.type;
|
|
114
|
+
|
|
115
|
+
// When a selection extends to offset 0 of the next block (common when
|
|
116
|
+
// selecting to "end of line"), that trailing block has no selected
|
|
117
|
+
// content — exclude it so it isn't dragged along. Also clamp the
|
|
118
|
+
// trailing selection endpoint to the end of the new lastBlock so the
|
|
119
|
+
// restored highlight doesn't overextend into the excluded block.
|
|
120
|
+
if (lastBlock !== firstBlock) {
|
|
121
|
+
const lastPoint = anchorIdx > focusIdx ? selection.anchor : selection.focus;
|
|
122
|
+
if (lastPoint.offset === 0 && $isFirstPositionInBlock(lastPoint.getNode(), lastBlock)) {
|
|
123
|
+
const stepped = lastBlock.getPreviousSibling();
|
|
124
|
+
if (stepped === null) {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
lastBlock = stepped;
|
|
128
|
+
|
|
129
|
+
const lastDesc = $isElementNode(lastBlock) ? lastBlock.getLastDescendant() : null;
|
|
130
|
+
const clampKey = lastDesc ? lastDesc.getKey() : lastBlock.getKey();
|
|
131
|
+
const clampOffset = lastDesc
|
|
132
|
+
? lastDesc.getTextContentSize()
|
|
133
|
+
: $isElementNode(lastBlock)
|
|
134
|
+
? lastBlock.getChildrenSize()
|
|
135
|
+
: lastBlock.getTextContentSize();
|
|
136
|
+
const clampType: "text" | "element" = lastDesc
|
|
137
|
+
? "text"
|
|
138
|
+
: $isElementNode(lastBlock)
|
|
139
|
+
? "element"
|
|
140
|
+
: "text";
|
|
141
|
+
if (anchorIdx > focusIdx) {
|
|
142
|
+
anchorKey = clampKey;
|
|
143
|
+
anchorOffset = clampOffset;
|
|
144
|
+
anchorType = clampType;
|
|
145
|
+
} else {
|
|
146
|
+
focusKey = clampKey;
|
|
147
|
+
focusOffset = clampOffset;
|
|
148
|
+
focusType = clampType;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (code === "ArrowUp") {
|
|
154
|
+
const prev = firstBlock.getPreviousSibling();
|
|
155
|
+
if (prev === null) {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
lastBlock.insertAfter(prev, false);
|
|
159
|
+
} else {
|
|
160
|
+
const next = lastBlock.getNextSibling();
|
|
161
|
+
if (next === null) {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
firstBlock.insertBefore(next, false);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if ($getNodeByKey(anchorKey) !== null) {
|
|
168
|
+
const restored = $createRangeSelection();
|
|
169
|
+
restored.format = selection.format;
|
|
170
|
+
restored.style = selection.style;
|
|
171
|
+
restored.anchor.set(anchorKey, anchorOffset, anchorType);
|
|
172
|
+
restored.focus.set(focusKey, focusOffset, focusType);
|
|
173
|
+
$setSelection(restored);
|
|
174
|
+
} else {
|
|
175
|
+
lastBlock.selectEnd();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
event.preventDefault();
|
|
179
|
+
return true;
|
|
180
|
+
},
|
|
181
|
+
COMMAND_PRIORITY_HIGH,
|
|
182
|
+
);
|
|
183
|
+
}, [editor]);
|
|
184
|
+
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
|
|
2
|
+
import {
|
|
3
|
+
$createRangeSelection,
|
|
4
|
+
$getNearestNodeFromDOMNode,
|
|
5
|
+
$getSelection,
|
|
6
|
+
$isRangeSelection,
|
|
7
|
+
$isTextNode,
|
|
8
|
+
$setSelection,
|
|
9
|
+
COMMAND_PRIORITY_HIGH,
|
|
10
|
+
KEY_DOWN_COMMAND,
|
|
11
|
+
} from "lexical";
|
|
12
|
+
import { useEffect } from "react";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Intercepts Cmd+Shift+ArrowLeft/Right (Mac line-boundary selection) and explicitly
|
|
16
|
+
* sets anchor/focus so that Lexical's reconciliation cannot flip the selection direction.
|
|
17
|
+
* Uses the browser's native `Selection.modify` with `'lineboundary'` granularity to
|
|
18
|
+
* find the correct **visual** line boundary (respecting text wrapping), then converts
|
|
19
|
+
* the result back to Lexical coordinates.
|
|
20
|
+
*/
|
|
21
|
+
export function SelectToLineBoundaryPlugin() {
|
|
22
|
+
const [editor] = useLexicalComposerContext();
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
return editor.registerCommand(
|
|
26
|
+
KEY_DOWN_COMMAND,
|
|
27
|
+
(event: KeyboardEvent) => {
|
|
28
|
+
if (!event.shiftKey || !event.metaKey || event.altKey || event.ctrlKey) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
const code = event.code;
|
|
32
|
+
if (code !== "ArrowLeft" && code !== "ArrowRight") {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const selection = $getSelection();
|
|
37
|
+
if (!$isRangeSelection(selection)) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const savedAnchorKey = selection.anchor.key;
|
|
42
|
+
const savedAnchorOffset = selection.anchor.offset;
|
|
43
|
+
const savedAnchorType = selection.anchor.type;
|
|
44
|
+
|
|
45
|
+
const focusElement = editor.getElementByKey(selection.focus.key);
|
|
46
|
+
if (!focusElement) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
const focusDomNode =
|
|
50
|
+
selection.focus.type === "text" ? focusElement.firstChild : focusElement;
|
|
51
|
+
if (!focusDomNode) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const domSelection = window.getSelection();
|
|
56
|
+
if (!domSelection) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
domSelection.collapse(focusDomNode, selection.focus.offset);
|
|
61
|
+
domSelection.modify("move", code === "ArrowLeft" ? "left" : "right", "lineboundary");
|
|
62
|
+
|
|
63
|
+
const boundaryDomNode = domSelection.anchorNode;
|
|
64
|
+
const boundaryDomOffset = domSelection.anchorOffset;
|
|
65
|
+
if (!boundaryDomNode) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const boundaryLexicalNode = $getNearestNodeFromDOMNode(boundaryDomNode);
|
|
70
|
+
if (!boundaryLexicalNode) {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const newFocusKey = boundaryLexicalNode.getKey();
|
|
75
|
+
const newFocusOffset = boundaryDomOffset;
|
|
76
|
+
const newFocusType: "text" | "element" = $isTextNode(boundaryLexicalNode)
|
|
77
|
+
? "text"
|
|
78
|
+
: "element";
|
|
79
|
+
|
|
80
|
+
const restored = $createRangeSelection();
|
|
81
|
+
restored.format = selection.format;
|
|
82
|
+
restored.style = selection.style;
|
|
83
|
+
restored.anchor.set(savedAnchorKey, savedAnchorOffset, savedAnchorType);
|
|
84
|
+
restored.focus.set(newFocusKey, newFocusOffset, newFocusType);
|
|
85
|
+
$setSelection(restored);
|
|
86
|
+
|
|
87
|
+
event.preventDefault();
|
|
88
|
+
return true;
|
|
89
|
+
},
|
|
90
|
+
COMMAND_PRIORITY_HIGH,
|
|
91
|
+
);
|
|
92
|
+
}, [editor]);
|
|
93
|
+
|
|
94
|
+
return null;
|
|
95
|
+
}
|