docblocks-react-ui 0.0.0-stage → 0.1.1
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/CHANGELOG.md +107 -0
- package/LICENSE +21 -0
- package/README.md +592 -2
- package/dist/Editor-CiJY5R4v.mjs +26587 -0
- package/dist/Editor-DT7RkRXf.d.mts +32 -0
- package/dist/LoaderCircle-AGVFE4IP.mjs +19 -0
- package/dist/Skeleton-D922Ths5.mjs +13 -0
- package/dist/constants-D4FTL0ph.mjs +81 -0
- package/dist/fonts/OFL.txt +162 -0
- package/dist/fonts/inter-300-c.woff2 +0 -0
- package/dist/fonts/inter-300-l.woff2 +0 -0
- package/dist/fonts/inter-300i-c.woff2 +0 -0
- package/dist/fonts/inter-300i-l.woff2 +0 -0
- package/dist/fonts/inter-400-c.woff2 +0 -0
- package/dist/fonts/inter-400-l.woff2 +0 -0
- package/dist/fonts/inter-400i-c.woff2 +0 -0
- package/dist/fonts/inter-400i-l.woff2 +0 -0
- package/dist/fonts/inter-600-c.woff2 +0 -0
- package/dist/fonts/inter-600-l.woff2 +0 -0
- package/dist/fonts/inter-600i-c.woff2 +0 -0
- package/dist/fonts/inter-600i-l.woff2 +0 -0
- package/dist/fonts/jetbrains-mono-300-c.woff2 +0 -0
- package/dist/fonts/jetbrains-mono-300-l.woff2 +0 -0
- package/dist/fonts/jetbrains-mono-300i-c.woff2 +0 -0
- package/dist/fonts/jetbrains-mono-300i-l.woff2 +0 -0
- package/dist/fonts/jetbrains-mono-400-c.woff2 +0 -0
- package/dist/fonts/jetbrains-mono-400-l.woff2 +0 -0
- package/dist/fonts/jetbrains-mono-400i-c.woff2 +0 -0
- package/dist/fonts/jetbrains-mono-400i-l.woff2 +0 -0
- package/dist/fonts/jetbrains-mono-600-c.woff2 +0 -0
- package/dist/fonts/jetbrains-mono-600-l.woff2 +0 -0
- package/dist/fonts/jetbrains-mono-600i-c.woff2 +0 -0
- package/dist/fonts/jetbrains-mono-600i-l.woff2 +0 -0
- package/dist/fonts/lora-400-c.woff2 +0 -0
- package/dist/fonts/lora-400-l.woff2 +0 -0
- package/dist/fonts/lora-400i-c.woff2 +0 -0
- package/dist/fonts/lora-400i-l.woff2 +0 -0
- package/dist/fonts/lora-600-c.woff2 +0 -0
- package/dist/fonts/lora-600-l.woff2 +0 -0
- package/dist/fonts/lora-600i-c.woff2 +0 -0
- package/dist/fonts/lora-600i-l.woff2 +0 -0
- package/dist/fonts/lora-700-c.woff2 +0 -0
- package/dist/fonts/lora-700-l.woff2 +0 -0
- package/dist/fonts/lora-700i-c.woff2 +0 -0
- package/dist/fonts/lora-700i-l.woff2 +0 -0
- package/dist/fonts/open-sans-300-c.woff2 +0 -0
- package/dist/fonts/open-sans-300-l.woff2 +0 -0
- package/dist/fonts/open-sans-300i-c.woff2 +0 -0
- package/dist/fonts/open-sans-300i-l.woff2 +0 -0
- package/dist/fonts/open-sans-400-c.woff2 +0 -0
- package/dist/fonts/open-sans-400-l.woff2 +0 -0
- package/dist/fonts/open-sans-400i-c.woff2 +0 -0
- package/dist/fonts/open-sans-400i-l.woff2 +0 -0
- package/dist/fonts/open-sans-600-c.woff2 +0 -0
- package/dist/fonts/open-sans-600-l.woff2 +0 -0
- package/dist/fonts/open-sans-600i-c.woff2 +0 -0
- package/dist/fonts/open-sans-600i-l.woff2 +0 -0
- package/dist/fonts/pt-sans-400-c.woff2 +0 -0
- package/dist/fonts/pt-sans-400-l.woff2 +0 -0
- package/dist/fonts/pt-sans-400i-c.woff2 +0 -0
- package/dist/fonts/pt-sans-400i-l.woff2 +0 -0
- package/dist/fonts/pt-sans-700-c.woff2 +0 -0
- package/dist/fonts/pt-sans-700-l.woff2 +0 -0
- package/dist/fonts/pt-sans-700i-c.woff2 +0 -0
- package/dist/fonts/pt-sans-700i-l.woff2 +0 -0
- package/dist/fonts/pt-serif-400-c.woff2 +0 -0
- package/dist/fonts/pt-serif-400-l.woff2 +0 -0
- package/dist/fonts/pt-serif-400i-c.woff2 +0 -0
- package/dist/fonts/pt-serif-400i-l.woff2 +0 -0
- package/dist/fonts/pt-serif-700-c.woff2 +0 -0
- package/dist/fonts/pt-serif-700-l.woff2 +0 -0
- package/dist/fonts/pt-serif-700i-c.woff2 +0 -0
- package/dist/fonts/pt-serif-700i-l.woff2 +0 -0
- package/dist/index-D6_ysSmQ.d.mts +1075 -0
- package/dist/index-YJcgf8Hh.d.mts +39 -0
- package/dist/index.d.mts +388 -0
- package/dist/index.mjs +641 -0
- package/dist/mermaid/index.d.mts +10 -0
- package/dist/mermaid/index.mjs +252 -0
- package/dist/motion/index.d.mts +8 -0
- package/dist/motion/index.mjs +1009 -0
- package/dist/pdf/index.d.mts +5 -0
- package/dist/pdf/index.mjs +164 -0
- package/dist/pdf/jbig2.wasm +0 -0
- package/dist/pdf/jbig2_nowasm_fallback.js +15 -0
- package/dist/pdf/pdf.worker.min.mjs +28 -0
- package/dist/styles.css +4562 -0
- package/package.json +99 -4
- package/theme.css +128 -0
|
@@ -0,0 +1,1009 @@
|
|
|
1
|
+
import { $t as ENUM_OPTION_PALETTE, At as Plus, C as deriveFlat, D as visibleRows, E as isEmptyChainText, It as Button, M as EnumOptionsMotionSlot, Mt as GripVertical, S as collapseEmptyTextChains, St as Card, T as indentOf$1, _ as useMentionMenu, _n as ensureTrailingParagraph, a as BlockNodeView, an as Input, b as BlockContext, bt as ColorSwatches, d as FormatPaletteContext, f as useFormatToolbar, ht as PopoverContent, i as CanvasView, j as useSlashMenu, k as useEditorContext, kt as X, mn as editorRootOf, n as PresenceCaretOverlay, o as FlatBlockView, on as useBlock, p as FORMAT_PALETTE, pn as SlashMenuContext, pt as Popover, r as useEditing, v as MentionMenuContext, vn as moveSegmentInSequence, w as entrySequenceParent, x as useBlockContext, y as insertParagraphAfter, yn as segmentRange, yt as PopoverTrigger } from "../Editor-CiJY5R4v.mjs";
|
|
2
|
+
import { c as cn } from "../constants-D4FTL0ph.mjs";
|
|
3
|
+
import { useStore } from "zustand";
|
|
4
|
+
import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
5
|
+
import { BLOCK_REGISTRY, InvalidAttrsError, InvalidOperationError } from "docblocks-core";
|
|
6
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
+
import { AnimatePresence, Reorder, useDragControls } from "framer-motion";
|
|
8
|
+
//#region src/motion/constants.ts
|
|
9
|
+
/** The motion editor's constants: the enter/exit geometry (the spacing
|
|
10
|
+
* margin collapses on exit so a leaving row does not keep its gap), the
|
|
11
|
+
* enter/exit and layout-slide transition (one snappy ease — the
|
|
12
|
+
* between-rows reflow while a drag runs runs the layout arm of this
|
|
13
|
+
* transition, gated per-value: 0 while the slide is disarmed), the slide
|
|
14
|
+
* settle window (how long the layout slide stays armed after a structural
|
|
15
|
+
* render — the slide itself runs 0.18s), the drag affordance style (the
|
|
16
|
+
* handle shows the grab cursor in this view only — the rule is scoped to
|
|
17
|
+
* the root attribute, so the base editor keeps the stylesheet's plain
|
|
18
|
+
* pointer cursor), the text-row gutter types (the text types whose
|
|
19
|
+
* built-in renderer renders the row's gutter handle — the row-level one
|
|
20
|
+
* stands for every other type). */
|
|
21
|
+
/**
|
|
22
|
+
* The enter/exit geometry of a row. The spacing margin collapses on exit
|
|
23
|
+
* so a leaving row does not keep its gap.
|
|
24
|
+
*/
|
|
25
|
+
const ENTER = {
|
|
26
|
+
opacity: 0,
|
|
27
|
+
height: 0
|
|
28
|
+
};
|
|
29
|
+
const ANIMATE = {
|
|
30
|
+
opacity: 1,
|
|
31
|
+
height: "auto"
|
|
32
|
+
};
|
|
33
|
+
const EXIT = {
|
|
34
|
+
opacity: 0,
|
|
35
|
+
height: 0,
|
|
36
|
+
marginBottom: 0
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* The enter/exit and layout-slide transition (one snappy ease). The
|
|
40
|
+
* between-rows reflow while a drag runs (the rows slide into each crossed
|
|
41
|
+
* slot) runs the layout arm of this transition (gated per-value: 0 while
|
|
42
|
+
* the slide is disarmed).
|
|
43
|
+
*/
|
|
44
|
+
const TRANSITION = {
|
|
45
|
+
duration: .18,
|
|
46
|
+
ease: "easeOut"
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* The drag affordance: the handle shows the grab cursor in this view
|
|
50
|
+
* only — the rule is scoped to the root attribute below, so the base
|
|
51
|
+
* editor keeps the stylesheet's plain pointer cursor.
|
|
52
|
+
*/
|
|
53
|
+
const HANDLE_GRAB_STYLE = "[data-docblocks-motion] [data-slot=\"docblocks/gutter\"] { cursor: grab; }";
|
|
54
|
+
/** The text types whose built-in renderer renders the row's gutter handle (the row-level one stands for every other type). */
|
|
55
|
+
const TEXT_ROW_TYPES = /* @__PURE__ */ new Set([
|
|
56
|
+
"paragraph",
|
|
57
|
+
"quote",
|
|
58
|
+
"heading",
|
|
59
|
+
"callout",
|
|
60
|
+
"list-item",
|
|
61
|
+
"code"
|
|
62
|
+
]);
|
|
63
|
+
//#endregion
|
|
64
|
+
//#region src/motion/MotionEnumOptionsEditor.tsx
|
|
65
|
+
/** The motion's enum's options editor: the framer `Reorder`'s rows (the
|
|
66
|
+
* grip's press-drag stands the live reorder with the layout's slide — the
|
|
67
|
+
* rows' positions animate over the drag / the commit). The base's stand
|
|
68
|
+
* stands the plain editor (the pointer's session — no animation); this
|
|
69
|
+
* variant is provided over the `EnumOptionsMotionSlot`'s context by the
|
|
70
|
+
* `MotionEditor` (the framer's import stands this subpath only — the
|
|
71
|
+
* subpath's boundary's contract). */
|
|
72
|
+
/** One option's row as a `Reorder.Item` (the grip's press starts the
|
|
73
|
+
* framer's drag through the row's `dragControls` — the grip is the only
|
|
74
|
+
* drag trigger, the row's own drag is disarmed). */
|
|
75
|
+
function MotionEnumOptionRow({ option, colorOpen, onColorOpen, onSet, onRemove }) {
|
|
76
|
+
const dragControls = useDragControls();
|
|
77
|
+
const onGripPointerDown = (event) => {
|
|
78
|
+
if (event.button !== 0) return;
|
|
79
|
+
event.preventDefault();
|
|
80
|
+
dragControls.start(event);
|
|
81
|
+
};
|
|
82
|
+
const onSelectColor = (hex) => {
|
|
83
|
+
onSet({ color: hex });
|
|
84
|
+
onColorOpen(false);
|
|
85
|
+
};
|
|
86
|
+
const onSelectNone = () => {
|
|
87
|
+
onSet({ color: null });
|
|
88
|
+
onColorOpen(false);
|
|
89
|
+
};
|
|
90
|
+
const onNameChange = (event) => onSet({ name: event.target.value });
|
|
91
|
+
return /* @__PURE__ */ jsxs(Reorder.Item, {
|
|
92
|
+
as: "div",
|
|
93
|
+
"data-slot": "docblocks/database-enum-option-row",
|
|
94
|
+
value: option,
|
|
95
|
+
dragListener: false,
|
|
96
|
+
dragControls,
|
|
97
|
+
whileDrag: { zIndex: 10 },
|
|
98
|
+
layout: "position",
|
|
99
|
+
transition: { layout: TRANSITION },
|
|
100
|
+
className: "relative flex h-8 items-center gap-1",
|
|
101
|
+
children: [
|
|
102
|
+
/* @__PURE__ */ jsx("span", {
|
|
103
|
+
"data-slot": "docblocks/database-enum-option-grip",
|
|
104
|
+
onPointerDown: onGripPointerDown,
|
|
105
|
+
className: "\n flex size-5 shrink-0 cursor-grab touch-none items-center justify-center text-muted-foreground\n active:cursor-grabbing\n ",
|
|
106
|
+
children: /* @__PURE__ */ jsx(GripVertical, { className: "size-3.5" })
|
|
107
|
+
}),
|
|
108
|
+
/* @__PURE__ */ jsxs(Popover, {
|
|
109
|
+
open: colorOpen,
|
|
110
|
+
onOpenChange: onColorOpen,
|
|
111
|
+
children: [/* @__PURE__ */ jsx(PopoverTrigger, {
|
|
112
|
+
asChild: true,
|
|
113
|
+
children: /* @__PURE__ */ jsx(Button, {
|
|
114
|
+
"data-slot": "docblocks/database-enum-option-color",
|
|
115
|
+
variant: "ghost",
|
|
116
|
+
"data-color": option.color ?? "",
|
|
117
|
+
title: "Option's color",
|
|
118
|
+
className: "size-4 shrink-0 cursor-pointer rounded-full border border-border p-0 focus-visible:ring-0",
|
|
119
|
+
style: option.color !== null ? { backgroundColor: option.color } : void 0
|
|
120
|
+
})
|
|
121
|
+
}), /* @__PURE__ */ jsx(PopoverContent, {
|
|
122
|
+
className: "flex items-center p-2",
|
|
123
|
+
children: /* @__PURE__ */ jsx(ColorSwatches, {
|
|
124
|
+
colors: ENUM_OPTION_PALETTE,
|
|
125
|
+
current: option.color ?? void 0,
|
|
126
|
+
label: "option color",
|
|
127
|
+
allowNone: true,
|
|
128
|
+
noneActive: option.color === null,
|
|
129
|
+
onSelect: onSelectColor,
|
|
130
|
+
onSelectNone
|
|
131
|
+
})
|
|
132
|
+
})]
|
|
133
|
+
}),
|
|
134
|
+
/* @__PURE__ */ jsx(Input, {
|
|
135
|
+
size: "sm",
|
|
136
|
+
type: "text",
|
|
137
|
+
"data-slot": "docblocks/database-enum-option-name",
|
|
138
|
+
placeholder: "Option",
|
|
139
|
+
className: "min-w-0 flex-1 cursor-text text-foreground focus:bg-muted/60",
|
|
140
|
+
value: option.name,
|
|
141
|
+
onChange: onNameChange
|
|
142
|
+
}),
|
|
143
|
+
/* @__PURE__ */ jsx(Button, {
|
|
144
|
+
"data-slot": "docblocks/database-enum-option-delete",
|
|
145
|
+
variant: "ghost",
|
|
146
|
+
size: "icon",
|
|
147
|
+
className: "\n size-5 shrink-0 cursor-pointer text-muted-foreground\n hover:bg-transparent hover:text-foreground\n dark:hover:bg-transparent\n ",
|
|
148
|
+
onClick: onRemove,
|
|
149
|
+
children: /* @__PURE__ */ jsx(X, { className: "size-3.5" })
|
|
150
|
+
})
|
|
151
|
+
]
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
/** The motion's options' editor (the create / the edit's form's stand):
|
|
155
|
+
* the `Reorder.Group`'s live reorder over the draft (the release stands
|
|
156
|
+
* nothing extra — the draft IS the order; the form's commit stands the
|
|
157
|
+
* bridge's write over the system's table). */
|
|
158
|
+
function MotionEnumOptionsEditor({ options, onChange }) {
|
|
159
|
+
const [colorOpen, setColorOpen] = useState(null);
|
|
160
|
+
const setOption = (index, patch) => {
|
|
161
|
+
onChange(options.map((option, i) => i === index ? {
|
|
162
|
+
...option,
|
|
163
|
+
...patch
|
|
164
|
+
} : option));
|
|
165
|
+
};
|
|
166
|
+
const colorOpenChange = (index) => (open) => setColorOpen(open ? index : null);
|
|
167
|
+
const setOptionAt = (index) => (patch) => setOption(index, patch);
|
|
168
|
+
const optionRemove = (index) => () => onChange(options.filter((_, i) => i !== index));
|
|
169
|
+
const addOption = () => onChange([...options, {
|
|
170
|
+
name: "",
|
|
171
|
+
color: null,
|
|
172
|
+
uid: Math.random().toString(36).slice(2)
|
|
173
|
+
}]);
|
|
174
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
175
|
+
"data-slot": "docblocks/database-enum-options",
|
|
176
|
+
className: "flex flex-col gap-1",
|
|
177
|
+
children: [
|
|
178
|
+
/* @__PURE__ */ jsx("div", {
|
|
179
|
+
className: "text-sm text-foreground",
|
|
180
|
+
children: "Options"
|
|
181
|
+
}),
|
|
182
|
+
/* @__PURE__ */ jsx(Reorder.Group, {
|
|
183
|
+
axis: "y",
|
|
184
|
+
values: options,
|
|
185
|
+
onReorder: onChange,
|
|
186
|
+
className: "flex flex-col gap-1",
|
|
187
|
+
children: options.map((option, index) => /* @__PURE__ */ jsx(MotionEnumOptionRow, {
|
|
188
|
+
option,
|
|
189
|
+
colorOpen: colorOpen === index,
|
|
190
|
+
onColorOpen: colorOpenChange(index),
|
|
191
|
+
onSet: setOptionAt(index),
|
|
192
|
+
onRemove: optionRemove(index)
|
|
193
|
+
}, option.rowId ?? option.uid ?? `draft-${index}`))
|
|
194
|
+
}),
|
|
195
|
+
/* @__PURE__ */ jsx(Button, {
|
|
196
|
+
"data-slot": "docblocks/database-enum-option-add",
|
|
197
|
+
variant: "ghost",
|
|
198
|
+
size: "sm",
|
|
199
|
+
className: "h-auto cursor-pointer self-start rounded-sm px-1 py-0.5 font-normal text-muted-foreground",
|
|
200
|
+
onClick: addOption,
|
|
201
|
+
children: "+ Add option"
|
|
202
|
+
})
|
|
203
|
+
]
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
//#endregion
|
|
207
|
+
//#region src/motion/RowItem.tsx
|
|
208
|
+
/** The motion editor's flat row: a page block as a `Reorder.Item` (the
|
|
209
|
+
* gutter handle's drag trigger, the indent steps' padding, the
|
|
210
|
+
* enter/exit geometry, the position-only layout slide). */
|
|
211
|
+
/**
|
|
212
|
+
* One flat row: a page block as a `Reorder.Item`. The drag is pure framer
|
|
213
|
+
* (the row's `dragControls`, started from the gutter handle's pointerdown
|
|
214
|
+
* — the handle is the only drag trigger, so a press without movement
|
|
215
|
+
* stays the plain block selection). Every page row is a drag source: the
|
|
216
|
+
* text types' built-in renderer renders the handle inside its own
|
|
217
|
+
* wrapper, every other row carries the row-level handle. The row's left
|
|
218
|
+
* padding is the `indent`'s per-level steps (a list item's extra step is
|
|
219
|
+
* its marker gutter — the base view's list padding), so the nesting is
|
|
220
|
+
* the data, not a wrapper.
|
|
221
|
+
*/
|
|
222
|
+
const RowItem = memo(function RowItem({ node, layoutSlide, isDragged, animated, previewIndent, onDragStart, onDragEnd, onDragMove }) {
|
|
223
|
+
const dragControls = useDragControls();
|
|
224
|
+
const rowRef = useRef(null);
|
|
225
|
+
const { doc, store, selection: selectionRef, viewMode } = useEditorContext();
|
|
226
|
+
const blockContext = useBlockContext();
|
|
227
|
+
const isList = node.kind !== void 0;
|
|
228
|
+
const block = useBlock(node.id);
|
|
229
|
+
const selected = useStore(store, (state) => state.selection.kind === "blocks" && state.selection.ids.includes(node.id));
|
|
230
|
+
const caretRow = useStore(store, (state) => {
|
|
231
|
+
if (state.selection.kind !== "text") return false;
|
|
232
|
+
return (state.selection.to !== null ? state.selection.to : state.selection.from).blockId === node.id;
|
|
233
|
+
});
|
|
234
|
+
const needsRowGutter = block !== void 0 && !TEXT_ROW_TYPES.has(block.type) && !viewMode;
|
|
235
|
+
/** The gutter handle's pointerdown starts this row's framer drag. */
|
|
236
|
+
const onRowPointerDown = useCallback((event) => {
|
|
237
|
+
const target = event.target instanceof HTMLElement ? event.target : null;
|
|
238
|
+
if (target === null) return;
|
|
239
|
+
if (target.closest("[data-slot=\"docblocks/gutter\"]") === null) return;
|
|
240
|
+
dragControls.start(event);
|
|
241
|
+
}, [dragControls]);
|
|
242
|
+
/** Gutter click: a blocks selection on this row's block (Shift extends from the anchor). */
|
|
243
|
+
const onGutterClick = useCallback((event) => {
|
|
244
|
+
event.preventDefault();
|
|
245
|
+
const view = selectionRef.view;
|
|
246
|
+
if (view === null) return;
|
|
247
|
+
const id = node.id;
|
|
248
|
+
const current = view.get();
|
|
249
|
+
if (event.shiftKey && current.kind === "blocks") {
|
|
250
|
+
const siblings = doc.getChildren(blockContext.parentId);
|
|
251
|
+
const anchorIndex = siblings.indexOf(current.anchorId);
|
|
252
|
+
const hereIndex = siblings.indexOf(id);
|
|
253
|
+
if (anchorIndex !== -1 && hereIndex !== -1) {
|
|
254
|
+
const [low, high] = anchorIndex <= hereIndex ? [anchorIndex, hereIndex] : [hereIndex, anchorIndex];
|
|
255
|
+
view.set({
|
|
256
|
+
kind: "blocks",
|
|
257
|
+
ids: siblings.slice(low, high + 1),
|
|
258
|
+
anchorId: current.anchorId
|
|
259
|
+
});
|
|
260
|
+
} else view.set({
|
|
261
|
+
kind: "blocks",
|
|
262
|
+
ids: [id],
|
|
263
|
+
anchorId: id
|
|
264
|
+
});
|
|
265
|
+
} else view.set({
|
|
266
|
+
kind: "blocks",
|
|
267
|
+
ids: [id],
|
|
268
|
+
anchorId: id
|
|
269
|
+
});
|
|
270
|
+
rowRef.current?.focus();
|
|
271
|
+
}, [
|
|
272
|
+
blockContext,
|
|
273
|
+
doc,
|
|
274
|
+
node.id,
|
|
275
|
+
selectionRef
|
|
276
|
+
]);
|
|
277
|
+
/** Keep a mousedown on the handle from moving the text caret. */
|
|
278
|
+
const onHandleMouseDown = useCallback((event) => {
|
|
279
|
+
event.preventDefault();
|
|
280
|
+
}, []);
|
|
281
|
+
/** Gutter plus: an empty paragraph under this row's block (one transact), the caret at its start. */
|
|
282
|
+
const onGutterPlusClick = useCallback((event) => {
|
|
283
|
+
event.preventDefault();
|
|
284
|
+
const id = node.id;
|
|
285
|
+
insertParagraphAfter(doc, id, blockContext.parentId, indentOf$1(block), editorRootOf(rowRef.current ?? document));
|
|
286
|
+
}, [
|
|
287
|
+
block,
|
|
288
|
+
blockContext,
|
|
289
|
+
doc,
|
|
290
|
+
node.id,
|
|
291
|
+
rowRef
|
|
292
|
+
]);
|
|
293
|
+
return /* @__PURE__ */ jsxs(Reorder.Item, {
|
|
294
|
+
as: "div",
|
|
295
|
+
ref: rowRef,
|
|
296
|
+
"data-slot": "docblocks/row",
|
|
297
|
+
"data-selected": selected ? "true" : void 0,
|
|
298
|
+
value: node.id,
|
|
299
|
+
tabIndex: 0,
|
|
300
|
+
initial: animated ? ENTER : false,
|
|
301
|
+
animate: ANIMATE,
|
|
302
|
+
exit: EXIT,
|
|
303
|
+
transition: {
|
|
304
|
+
opacity: TRANSITION,
|
|
305
|
+
height: TRANSITION,
|
|
306
|
+
marginBottom: TRANSITION,
|
|
307
|
+
layout: layoutSlide && !caretRow ? TRANSITION : { duration: 0 }
|
|
308
|
+
},
|
|
309
|
+
layout: "position",
|
|
310
|
+
dragListener: false,
|
|
311
|
+
dragControls,
|
|
312
|
+
onDragStart,
|
|
313
|
+
onDragEnd,
|
|
314
|
+
onDrag: onDragMove,
|
|
315
|
+
onPointerDown: onRowPointerDown,
|
|
316
|
+
className: cn(needsRowGutter && "group", "outline-none"),
|
|
317
|
+
style: {
|
|
318
|
+
position: "relative",
|
|
319
|
+
paddingLeft: `calc(${previewIndent ?? node.indent} * var(--docblocks-nested-indent) + ${isList ? "var(--docblocks-marker-width)" : "0px"})`,
|
|
320
|
+
...isDragged ? { zIndex: 30 } : void 0
|
|
321
|
+
},
|
|
322
|
+
children: [
|
|
323
|
+
needsRowGutter && /* @__PURE__ */ jsx(Button, {
|
|
324
|
+
type: "button",
|
|
325
|
+
"data-slot": "docblocks/gutter",
|
|
326
|
+
"aria-label": "Select block",
|
|
327
|
+
variant: "ghost",
|
|
328
|
+
size: "icon",
|
|
329
|
+
onClick: onGutterClick,
|
|
330
|
+
onMouseDown: onHandleMouseDown,
|
|
331
|
+
className: cn("absolute top-[calc((var(--docblocks-line-box-height)-var(--docblocks-gutter-size))/2)]", "cursor-pointer rounded-full opacity-0 group-hover:opacity-100", "text-(--docblocks-gutter-color)", "size-5", selected && "opacity-100"),
|
|
332
|
+
style: { left: `calc(${previewIndent ?? node.indent} * var(--docblocks-nested-indent) + var(--docblocks-gutter-left))` },
|
|
333
|
+
children: /* @__PURE__ */ jsx(GripVertical, {})
|
|
334
|
+
}),
|
|
335
|
+
needsRowGutter && /* @__PURE__ */ jsx(Button, {
|
|
336
|
+
type: "button",
|
|
337
|
+
"data-slot": "docblocks/gutter-plus",
|
|
338
|
+
"aria-label": "Add block below",
|
|
339
|
+
variant: "ghost",
|
|
340
|
+
size: "icon",
|
|
341
|
+
onClick: onGutterPlusClick,
|
|
342
|
+
onMouseDown: onHandleMouseDown,
|
|
343
|
+
className: cn("absolute top-[calc((var(--docblocks-line-box-height)-var(--docblocks-gutter-plus-size))/2)]", "cursor-pointer rounded-full opacity-0 group-hover:opacity-100", "text-(--docblocks-gutter-color)", "size-5", selected && "opacity-100"),
|
|
344
|
+
style: { left: `calc(
|
|
345
|
+
${previewIndent ?? node.indent} * var(--docblocks-nested-indent) +
|
|
346
|
+
var(--docblocks-gutter-left) + var(--docblocks-gutter-size) + var(--docblocks-gutter-gap)
|
|
347
|
+
)` },
|
|
348
|
+
children: /* @__PURE__ */ jsx(Plus, {})
|
|
349
|
+
}),
|
|
350
|
+
/* @__PURE__ */ jsx(BlockNodeView, {
|
|
351
|
+
node,
|
|
352
|
+
flat: true,
|
|
353
|
+
renderBlock: FlatBlockView
|
|
354
|
+
})
|
|
355
|
+
]
|
|
356
|
+
});
|
|
357
|
+
}, (prev, next) => prev.node === next.node && prev.layoutSlide === next.layoutSlide && prev.isDragged === next.isDragged && prev.previewIndent === next.previewIndent);
|
|
358
|
+
//#endregion
|
|
359
|
+
//#region src/motion/RowList.tsx
|
|
360
|
+
/**
|
|
361
|
+
* The motion editor's row list: the `Reorder.Group` + the flat rows, the
|
|
362
|
+
* memo boundary that keeps framer's `ReorderContext` value (collected per
|
|
363
|
+
* Group render, its fresh closures un-memoizable by the consumer) from
|
|
364
|
+
* propagating into the rows on parent churn: a text-only change re-renders
|
|
365
|
+
* nothing here (the rows' props stand), so the framer fibers and the
|
|
366
|
+
* rows' context consumers stand as well.
|
|
367
|
+
*/
|
|
368
|
+
const RowList = memo(function RowList({ order, nodesById, layoutSlide, draggingKey, previewIndent, animated, onReorder, onDragStart, onDragMove, onDragEnd }) {
|
|
369
|
+
const handleReorder = (next) => onReorder(next);
|
|
370
|
+
const rowDragStart = (id) => () => onDragStart(id);
|
|
371
|
+
const rowDragEnd = (id) => (event) => onDragEnd(id, event);
|
|
372
|
+
const rowDragMove = (id) => (event) => onDragMove(id, event);
|
|
373
|
+
return /* @__PURE__ */ jsx(Reorder.Group, {
|
|
374
|
+
as: "div",
|
|
375
|
+
"data-slot": "docblocks/rows",
|
|
376
|
+
axis: "y",
|
|
377
|
+
values: order,
|
|
378
|
+
onReorder: handleReorder,
|
|
379
|
+
children: /* @__PURE__ */ jsx(AnimatePresence, {
|
|
380
|
+
initial: false,
|
|
381
|
+
mode: "sync",
|
|
382
|
+
children: order.map((id) => {
|
|
383
|
+
const node = nodesById.get(id);
|
|
384
|
+
if (node === void 0) return null;
|
|
385
|
+
return /* @__PURE__ */ jsx(RowItem, {
|
|
386
|
+
node,
|
|
387
|
+
layoutSlide,
|
|
388
|
+
isDragged: draggingKey === id,
|
|
389
|
+
animated,
|
|
390
|
+
previewIndent: draggingKey === id ? previewIndent : null,
|
|
391
|
+
onDragStart: rowDragStart(id),
|
|
392
|
+
onDragEnd: rowDragEnd(id),
|
|
393
|
+
onDragMove: rowDragMove(id)
|
|
394
|
+
}, id);
|
|
395
|
+
})
|
|
396
|
+
})
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
//#endregion
|
|
400
|
+
//#region src/motion/hooks.ts
|
|
401
|
+
/** The motion editor's imperative root attributes: the grip typing flag
|
|
402
|
+
* (hidden while the editor content is typed) and the enter-animation
|
|
403
|
+
* gate (rows that mount before the first text input stand up instantly).
|
|
404
|
+
*/
|
|
405
|
+
/**
|
|
406
|
+
* The grip typing flag: the editor root holds `data-docblocks-typing` from
|
|
407
|
+
* the first text keypress in its own content until the first pointer move
|
|
408
|
+
* or the focus leaving the editor — the stylesheet's unlayered rule hides
|
|
409
|
+
* the hover grip while the attribute stands (a selected block's grip is a
|
|
410
|
+
* selection indicator and stays visible). The attribute is imperative: a
|
|
411
|
+
* React state would re-render every row per keypress.
|
|
412
|
+
*/
|
|
413
|
+
function useTypingFlag(rootRef) {
|
|
414
|
+
useEffect(() => {
|
|
415
|
+
const root = rootRef.current;
|
|
416
|
+
if (root === null) return;
|
|
417
|
+
const setTyping = (typing) => {
|
|
418
|
+
if (typing) root.setAttribute("data-docblocks-typing", "");
|
|
419
|
+
else root.removeAttribute("data-docblocks-typing");
|
|
420
|
+
};
|
|
421
|
+
const onKeydown = (event) => {
|
|
422
|
+
const target = event.target instanceof Element ? event.target : null;
|
|
423
|
+
if (target === null || target.closest("[data-slot=\"docblocks/editor\"]") !== root) return;
|
|
424
|
+
if (target.closest("[data-slot=\"docblocks/block-content\"]") === null) return;
|
|
425
|
+
if (event.key.length !== 1 || event.key === " " || event.metaKey || event.ctrlKey || event.altKey) return;
|
|
426
|
+
setTyping(true);
|
|
427
|
+
};
|
|
428
|
+
const onPointerMove = () => setTyping(false);
|
|
429
|
+
const onFocusOut = () => {
|
|
430
|
+
const active = document.activeElement;
|
|
431
|
+
if (active instanceof Node && root.contains(active)) return;
|
|
432
|
+
setTyping(false);
|
|
433
|
+
};
|
|
434
|
+
document.addEventListener("keydown", onKeydown);
|
|
435
|
+
window.addEventListener("pointermove", onPointerMove);
|
|
436
|
+
document.addEventListener("focusout", onFocusOut);
|
|
437
|
+
return () => {
|
|
438
|
+
document.removeEventListener("keydown", onKeydown);
|
|
439
|
+
window.removeEventListener("pointermove", onPointerMove);
|
|
440
|
+
document.removeEventListener("focusout", onFocusOut);
|
|
441
|
+
};
|
|
442
|
+
}, [rootRef]);
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* The enter-animation gate: the latch stands on the first text input in
|
|
446
|
+
* this editor (a printable keydown or an input event in the editor's
|
|
447
|
+
* contenteditable) — rows that mount before it (the mount and the sync
|
|
448
|
+
* load) stand up instantly (`initial={false}`: the animate
|
|
449
|
+
* values, no height-growth animation); a row that appears after it is an
|
|
450
|
+
* insert in progress and runs the enter animation. The gate never clears
|
|
451
|
+
* (a load window is a start window, not a session state).
|
|
452
|
+
*/
|
|
453
|
+
function useEnterAnimationGate(rootRef) {
|
|
454
|
+
const gateRef = useRef(false);
|
|
455
|
+
useEffect(() => {
|
|
456
|
+
const root = rootRef.current;
|
|
457
|
+
if (root === null) return;
|
|
458
|
+
const inThisEditorText = (event) => {
|
|
459
|
+
const target = event.target instanceof Element ? event.target : null;
|
|
460
|
+
if (target === null) return false;
|
|
461
|
+
if (target.closest("[data-slot=\"docblocks/editor\"]") !== root) return false;
|
|
462
|
+
return target.closest("[data-slot=\"docblocks/block-content\"]") !== null;
|
|
463
|
+
};
|
|
464
|
+
const onKeydown = (event) => {
|
|
465
|
+
if (event.key.length !== 1 || event.metaKey || event.ctrlKey || event.altKey) return;
|
|
466
|
+
if (!inThisEditorText(event)) return;
|
|
467
|
+
gateRef.current = true;
|
|
468
|
+
};
|
|
469
|
+
const onInput = (event) => {
|
|
470
|
+
if (!inThisEditorText(event)) return;
|
|
471
|
+
gateRef.current = true;
|
|
472
|
+
};
|
|
473
|
+
document.addEventListener("keydown", onKeydown);
|
|
474
|
+
document.addEventListener("input", onInput);
|
|
475
|
+
return () => {
|
|
476
|
+
document.removeEventListener("keydown", onKeydown);
|
|
477
|
+
document.removeEventListener("input", onInput);
|
|
478
|
+
};
|
|
479
|
+
}, [rootRef]);
|
|
480
|
+
return gateRef;
|
|
481
|
+
}
|
|
482
|
+
//#endregion
|
|
483
|
+
//#region src/motion/reorder/seam.ts
|
|
484
|
+
function dropCandidateIndent(rows, pointerY, segmentIds, headId) {
|
|
485
|
+
const rest = rows.filter((row) => !segmentIds.has(row.id));
|
|
486
|
+
if (rest.length === 0) {
|
|
487
|
+
const head = rows.find((row) => row.id === headId);
|
|
488
|
+
return head === void 0 ? 0 : head.indent;
|
|
489
|
+
}
|
|
490
|
+
const over = rest.find((row) => pointerY >= row.top && pointerY < row.bottom);
|
|
491
|
+
if (over !== void 0) return over.indent;
|
|
492
|
+
const belowLocal = rows.find((row) => row.top >= pointerY);
|
|
493
|
+
const aboveLocal = [...rows].reverse().find((row) => row.bottom <= pointerY);
|
|
494
|
+
if (belowLocal !== void 0 && aboveLocal !== void 0) return (pointerY < (aboveLocal.bottom + belowLocal.top) / 2 ? belowLocal : aboveLocal).indent;
|
|
495
|
+
if (belowLocal !== void 0) return segmentIds.has(belowLocal.id) ? rest[0]?.indent ?? 0 : belowLocal.indent;
|
|
496
|
+
const last = aboveLocal?.indent ?? 0;
|
|
497
|
+
return aboveLocal !== void 0 && segmentIds.has(aboveLocal.id) ? rest[rest.length - 1]?.indent ?? 0 : last;
|
|
498
|
+
}
|
|
499
|
+
//#endregion
|
|
500
|
+
//#region src/motion/reorder/commit.ts
|
|
501
|
+
/** Same ids in the same order (order matters). */
|
|
502
|
+
function sameIds(a, b) {
|
|
503
|
+
return a.length === b.length && a.every((id, i) => id === b[i]);
|
|
504
|
+
}
|
|
505
|
+
/** The block's effective indent (absent is 0). */
|
|
506
|
+
function indentOf(doc, id) {
|
|
507
|
+
const block = doc.getBlock(id);
|
|
508
|
+
if (block === null || !("attrs" in block)) return 0;
|
|
509
|
+
const indent = block.attrs?.indent;
|
|
510
|
+
return typeof indent === "number" && Number.isInteger(indent) && indent >= 0 ? indent : 0;
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* A collapsed list item's hidden segment rows (in scope order): the
|
|
514
|
+
* contiguous stretch after the item with `indent` deeper than the item's
|
|
515
|
+
* own, up to the first block with `indent` <= the item's (the segment
|
|
516
|
+
* minus the item itself). Absent when the item is not in the scope. The
|
|
517
|
+
* scope is a superset of the item's segment (its followers are deeper than
|
|
518
|
+
* the item, hence inside the scope), so the scoped scan equals the
|
|
519
|
+
* full-sequence one.
|
|
520
|
+
*/
|
|
521
|
+
function hiddenSegmentRows(doc, sequence, itemId) {
|
|
522
|
+
const start = sequence.indexOf(itemId);
|
|
523
|
+
if (start === -1) return [];
|
|
524
|
+
const indent = indentOf(doc, itemId);
|
|
525
|
+
const rows = [];
|
|
526
|
+
for (let i = start + 1; i < sequence.length; i += 1) {
|
|
527
|
+
const id = sequence[i];
|
|
528
|
+
if (id === void 0) break;
|
|
529
|
+
if (indentOf(doc, id) <= indent) break;
|
|
530
|
+
rows.push(id);
|
|
531
|
+
}
|
|
532
|
+
return rows;
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* The commit of a flat-row drag (the scope and the visible-order mapping:
|
|
536
|
+
* the module header). `contextId` is the provider's entry root: the commit
|
|
537
|
+
* resolves its sequence container and runs over its SCOPE — the full
|
|
538
|
+
* sequence for a sequence-container entry root, the entry root's derived
|
|
539
|
+
* segment (in the container's sequence) for a page-block one — and issues
|
|
540
|
+
* the range move of the dragged block's derived subtree within the
|
|
541
|
+
* CONTAINER's sequence (the scope's indices translated by the scope's
|
|
542
|
+
* offset — the scope's membership stands, so a valid permutation of the
|
|
543
|
+
* scope always commits: the segment's internal adjacencies and the
|
|
544
|
+
* boundary neighbors are unchanged, and the indent-jump rule holds) plus
|
|
545
|
+
* the seam's indent assignment (the `candidateIndent`, the module
|
|
546
|
+
* header's seam rule).
|
|
547
|
+
*
|
|
548
|
+
* Returns true when the drag landed (a command issued or a no-op — the
|
|
549
|
+
* local order already matches the store); false when the commit was
|
|
550
|
+
* dropped (stale — the caller resyncs the local order to the store).
|
|
551
|
+
*/
|
|
552
|
+
function commitFlatDrag(doc, contextId, draggedId, visibleFinalOrder, collapsedIds, candidateIndent) {
|
|
553
|
+
const containerId = entrySequenceParent(doc, contextId);
|
|
554
|
+
const containerSequence = doc.getChildren(containerId);
|
|
555
|
+
let scopeOffset = 0;
|
|
556
|
+
let sequence = containerSequence;
|
|
557
|
+
if (containerId !== contextId) {
|
|
558
|
+
const entry = segmentRange(doc, containerId, contextId);
|
|
559
|
+
if (entry === null) return false;
|
|
560
|
+
scopeOffset = entry.start;
|
|
561
|
+
sequence = containerSequence.slice(entry.start, entry.end);
|
|
562
|
+
}
|
|
563
|
+
const start = sequence.indexOf(draggedId);
|
|
564
|
+
if (start === -1) return false;
|
|
565
|
+
const segment = segmentRange(doc, containerId, draggedId);
|
|
566
|
+
if (segment === null) return false;
|
|
567
|
+
if (segment.start < scopeOffset || segment.end > scopeOffset + sequence.length) return false;
|
|
568
|
+
const segmentLength = segment.end - segment.start;
|
|
569
|
+
const segmentRows = sequence.slice(start, start + segmentLength);
|
|
570
|
+
const segmentOthers = segmentRows.filter((id) => id !== draggedId);
|
|
571
|
+
const collapsed = new Set(collapsedIds);
|
|
572
|
+
const hiddenByItem = /* @__PURE__ */ new Map();
|
|
573
|
+
for (const id of sequence) {
|
|
574
|
+
if (id === draggedId) continue;
|
|
575
|
+
const block = doc.getBlock(id);
|
|
576
|
+
if (block === null || block.type !== "list-item") continue;
|
|
577
|
+
if (!collapsed.has(id)) continue;
|
|
578
|
+
const rows = hiddenSegmentRows(doc, sequence, id);
|
|
579
|
+
if (rows.length > 0) hiddenByItem.set(id, rows);
|
|
580
|
+
}
|
|
581
|
+
const hiddenRowSet = /* @__PURE__ */ new Set();
|
|
582
|
+
for (const rows of hiddenByItem.values()) for (const row of rows) hiddenRowSet.add(row);
|
|
583
|
+
const isFixed = (id) => doc.getBlock(id)?.type === "surface" || hiddenRowSet.has(id);
|
|
584
|
+
const scopeVisible = sequence.filter((id) => !isFixed(id));
|
|
585
|
+
const scopeVisibleSet = new Set(scopeVisible);
|
|
586
|
+
const headLanding = visibleFinalOrder.indexOf(draggedId);
|
|
587
|
+
if (headLanding === -1) return false;
|
|
588
|
+
if (visibleFinalOrder.length !== scopeVisible.length) return false;
|
|
589
|
+
if (!visibleFinalOrder.every((id) => scopeVisibleSet.has(id))) return false;
|
|
590
|
+
const segmentSet = new Set(segmentRows);
|
|
591
|
+
const restNow = [...sequence.slice(0, start), ...sequence.slice(start + segmentLength)];
|
|
592
|
+
const restVisible = restNow.filter((id) => !isFixed(id));
|
|
593
|
+
if (!sameIds(visibleFinalOrder.filter((id) => !segmentSet.has(id)), restVisible)) return false;
|
|
594
|
+
if (headLanding === scopeVisible.indexOf(draggedId)) return true;
|
|
595
|
+
let restVisibleBeforeHead = 0;
|
|
596
|
+
for (let i = 0; i < headLanding; i += 1) if (!segmentSet.has(visibleFinalOrder[i])) restVisibleBeforeHead += 1;
|
|
597
|
+
const restIndexOf = /* @__PURE__ */ new Map();
|
|
598
|
+
restNow.forEach((id, i) => restIndexOf.set(id, i));
|
|
599
|
+
const precedingVisible = restVisible[restVisibleBeforeHead - 1];
|
|
600
|
+
const spliceAt = precedingVisible === void 0 ? 0 : (restIndexOf.get(precedingVisible) ?? 0) + 1;
|
|
601
|
+
const finalFull = [
|
|
602
|
+
...restNow.slice(0, spliceAt),
|
|
603
|
+
...segmentRows,
|
|
604
|
+
...restNow.slice(spliceAt)
|
|
605
|
+
];
|
|
606
|
+
const target = spliceAt;
|
|
607
|
+
let t = target < start ? target : target + segmentLength;
|
|
608
|
+
if (target >= start && containerId === doc.meta.id && scopeOffset === 0 && t === sequence.length) {
|
|
609
|
+
const segmentLast = segmentRows[segmentRows.length - 1];
|
|
610
|
+
const segmentLastBlock = segmentLast === void 0 ? null : doc.getBlock(segmentLast);
|
|
611
|
+
const trailingId = containerSequence[containerSequence.length - 1];
|
|
612
|
+
const trailingBlock = trailingId === void 0 ? null : doc.getBlock(trailingId);
|
|
613
|
+
if (segmentLastBlock !== null && trailingId !== void 0 && trailingBlock !== null && trailingId !== segmentLast && !BLOCK_REGISTRY[segmentLastBlock.type].content && BLOCK_REGISTRY[trailingBlock.type].content && indentOf(doc, trailingId) === 0) t -= 1;
|
|
614
|
+
}
|
|
615
|
+
let before;
|
|
616
|
+
for (let i = target - 1; i >= 0; i -= 1) {
|
|
617
|
+
const id = finalFull[i];
|
|
618
|
+
if (id !== void 0 && !isFixed(id)) {
|
|
619
|
+
before = id;
|
|
620
|
+
break;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
let after;
|
|
624
|
+
for (let i = target + segmentLength; i < finalFull.length; i += 1) {
|
|
625
|
+
const id = finalFull[i];
|
|
626
|
+
if (id !== void 0 && !isFixed(id)) {
|
|
627
|
+
after = id;
|
|
628
|
+
break;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
if (!(before !== void 0 && candidateIndent === indentOf(doc, before) || after !== void 0 && candidateIndent === indentOf(doc, after))) return false;
|
|
632
|
+
const delta = candidateIndent - indentOf(doc, draggedId);
|
|
633
|
+
const followers = segmentOthers.map((id) => ({
|
|
634
|
+
id,
|
|
635
|
+
indent: indentOf(doc, id) + delta
|
|
636
|
+
}));
|
|
637
|
+
if (delta === 0) return moveSegmentInSequence(doc, containerId, scopeOffset + start, segmentLength, scopeOffset + t);
|
|
638
|
+
const applyIndents = () => {
|
|
639
|
+
doc.updateBlock(draggedId, { indent: candidateIndent });
|
|
640
|
+
for (const follower of followers) doc.updateBlock(follower.id, { indent: follower.indent });
|
|
641
|
+
};
|
|
642
|
+
try {
|
|
643
|
+
doc.transact(() => {
|
|
644
|
+
if (delta < 0) applyIndents();
|
|
645
|
+
const blockIds = doc.getChildren(containerId).slice(scopeOffset + start, scopeOffset + start + segmentLength);
|
|
646
|
+
for (let i = 0; i < segmentLength; i += 1) {
|
|
647
|
+
const landing = target < start ? scopeOffset + t + i : scopeOffset + t;
|
|
648
|
+
doc.moveBlock(blockIds[i], containerId, landing);
|
|
649
|
+
}
|
|
650
|
+
if (delta > 0) applyIndents();
|
|
651
|
+
if (containerId === doc.meta.id) ensureTrailingParagraph(doc, doc.meta.id);
|
|
652
|
+
});
|
|
653
|
+
} catch (error) {
|
|
654
|
+
if (error instanceof InvalidAttrsError || error instanceof InvalidOperationError) return false;
|
|
655
|
+
throw error;
|
|
656
|
+
}
|
|
657
|
+
return true;
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
660
|
+
* The tail seat's clamp (the preview side of the commit's tail landing):
|
|
661
|
+
* framer's own reorder lets the dragged row cross the full-doc root's
|
|
662
|
+
* trailing text paragraph (the pointer beyond the last row stands it
|
|
663
|
+
* last). The commit stands the segment above the seat (the doc's tail
|
|
664
|
+
* contract), so the preview stands the crossing too — the dragged row
|
|
665
|
+
* clamps to the seat's slot, and the release settles at the preview's
|
|
666
|
+
* stand. The seat's own drag stands unclamped (it is a real block — the
|
|
667
|
+
* guarantee stands its own stand on release).
|
|
668
|
+
*/
|
|
669
|
+
function clampTailSeatCrossing(next, draggedId, seatId) {
|
|
670
|
+
if (seatId === void 0 || draggedId === seatId) return [...next];
|
|
671
|
+
const draggedAt = next.indexOf(draggedId);
|
|
672
|
+
const seatAt = next.indexOf(seatId);
|
|
673
|
+
if (draggedAt < 0 || seatAt < 0 || draggedAt <= seatAt) return [...next];
|
|
674
|
+
const rest = next.filter((id) => id !== draggedId);
|
|
675
|
+
const seatAfter = rest.indexOf(seatId);
|
|
676
|
+
return [
|
|
677
|
+
...rest.slice(0, seatAfter),
|
|
678
|
+
draggedId,
|
|
679
|
+
...rest.slice(seatAfter)
|
|
680
|
+
];
|
|
681
|
+
}
|
|
682
|
+
//#endregion
|
|
683
|
+
//#region src/motion/utils.ts
|
|
684
|
+
/** The motion editor's pure helpers: the drag event's pointer Y (the
|
|
685
|
+
* seam measurement's input) and the tail seat's id (the drag's preview
|
|
686
|
+
* clamp). */
|
|
687
|
+
/**
|
|
688
|
+
* The drag's pointer Y (the seam measurement's input): the pointer/mouse
|
|
689
|
+
* event's clientY, or the last changed touch's (the touch release event
|
|
690
|
+
* carries no coordinate of its own). Null when the event stands no
|
|
691
|
+
* usable coordinate (the measurement is stale — the caller keeps the
|
|
692
|
+
* preview or skips the commit).
|
|
693
|
+
*/
|
|
694
|
+
function pointerYOf(event) {
|
|
695
|
+
if (event instanceof TouchEvent) {
|
|
696
|
+
const touch = event.changedTouches[event.changedTouches.length - 1];
|
|
697
|
+
return touch === null || touch === void 0 ? null : touch.clientY;
|
|
698
|
+
}
|
|
699
|
+
return event.clientY;
|
|
700
|
+
}
|
|
701
|
+
/**
|
|
702
|
+
* The tail seat's id for the drag's preview clamp (or undefined when the
|
|
703
|
+
* clamp does not stand): the seat is the full-doc root's trailing row
|
|
704
|
+
* (the guaranteed trailing text paragraph), and it stands for a dragged
|
|
705
|
+
* row whose derived segment is NON-TEXT-terminated (a text-terminated
|
|
706
|
+
* segment lands its text tail last — the doc's tail invariant stands and
|
|
707
|
+
* the crossing is clean — no clamp, the end landing is the preview's
|
|
708
|
+
* stand). The seat's own drag stands unclamped too (it is a real block).
|
|
709
|
+
*/
|
|
710
|
+
function tailSeatId(doc, rootBlockId, draggedId, trailingRowId) {
|
|
711
|
+
if (trailingRowId === void 0 || draggedId === trailingRowId) return void 0;
|
|
712
|
+
const segment = segmentRange(doc, rootBlockId, draggedId);
|
|
713
|
+
if (segment === null) return void 0;
|
|
714
|
+
const lastId = doc.getChildren(rootBlockId)[segment.end - 1];
|
|
715
|
+
if (lastId === void 0) return void 0;
|
|
716
|
+
const lastBlock = doc.getBlock(lastId);
|
|
717
|
+
if (lastBlock === null) return void 0;
|
|
718
|
+
if (BLOCK_REGISTRY[lastBlock.type].content) return void 0;
|
|
719
|
+
const trailingBlock = doc.getBlock(trailingRowId);
|
|
720
|
+
if (trailingBlock === null || trailingRowId === lastId) return void 0;
|
|
721
|
+
if (!BLOCK_REGISTRY[trailingBlock.type].content) return void 0;
|
|
722
|
+
const trailingAttrs = "attrs" in trailingBlock ? trailingBlock.attrs : void 0;
|
|
723
|
+
return (typeof trailingAttrs?.indent === "number" ? trailingAttrs.indent : 0) === 0 ? trailingRowId : void 0;
|
|
724
|
+
}
|
|
725
|
+
//#endregion
|
|
726
|
+
//#region src/motion/use-motion-drag.ts
|
|
727
|
+
/** The motion editor's drag-reorder session (pure framer `Reorder` — one
|
|
728
|
+
* drag = one range move of the dragged block's derived subtree): the local
|
|
729
|
+
* row order (live during a drag, locked to the store otherwise) + the
|
|
730
|
+
* committed order's ref + the framer drag key + the in-flight seam
|
|
731
|
+
* candidate + the layout slide's arm. The 2 resyncs (the drag's row
|
|
732
|
+
* vanished — a peer deleted it mid-drag; a store change (remote or local,
|
|
733
|
+
* drag-free) resyncs the local order) + the 3 drag handlers (the row's
|
|
734
|
+
* drag start, the drag's live seam candidate, the row's release) + the
|
|
735
|
+
* framer reorder's live preview (the tail seat's clamp). The seam
|
|
736
|
+
* candidate at the pointer's Y (the drag's live preview and the release's
|
|
737
|
+
* commit input share it) — the visible rows' viewport boxes against the
|
|
738
|
+
* seam rule. */
|
|
739
|
+
/** The local row order: live during a drag (framer's reorder), locked to the store otherwise. */
|
|
740
|
+
function useMotionDrag({ doc, rootBlockId, store, rootRef, rowKeys, nodesById }) {
|
|
741
|
+
const [order, setOrder] = useState(rowKeys);
|
|
742
|
+
const orderRef = useRef(order);
|
|
743
|
+
const [draggingKey, setDraggingKey] = useState(null);
|
|
744
|
+
const [previewIndent, setPreviewIndent] = useState(null);
|
|
745
|
+
const [layoutSlide, setLayoutSlide] = useState(false);
|
|
746
|
+
const slideTimerRef = useRef(null);
|
|
747
|
+
const prevKeysRef = useRef(rowKeys);
|
|
748
|
+
const entryContainerRef = useRef(null);
|
|
749
|
+
/**
|
|
750
|
+
* Keep the layout slide armed for the settle window after a drag or a
|
|
751
|
+
* structural change, then release it so in-place content changes
|
|
752
|
+
* reflow instantly again (the zero-duration layout transition).
|
|
753
|
+
*/
|
|
754
|
+
const armSlide = useCallback(() => {
|
|
755
|
+
if (slideTimerRef.current !== null) window.clearTimeout(slideTimerRef.current);
|
|
756
|
+
slideTimerRef.current = window.setTimeout(() => {
|
|
757
|
+
slideTimerRef.current = null;
|
|
758
|
+
setLayoutSlide(false);
|
|
759
|
+
}, 400);
|
|
760
|
+
setLayoutSlide(true);
|
|
761
|
+
}, []);
|
|
762
|
+
useEffect(() => () => {
|
|
763
|
+
if (slideTimerRef.current !== null) window.clearTimeout(slideTimerRef.current);
|
|
764
|
+
}, []);
|
|
765
|
+
useLayoutEffect(() => {
|
|
766
|
+
orderRef.current = order;
|
|
767
|
+
}, [order]);
|
|
768
|
+
if (draggingKey !== null && !rowKeys.includes(draggingKey)) {
|
|
769
|
+
setDraggingKey(null);
|
|
770
|
+
setPreviewIndent(null);
|
|
771
|
+
}
|
|
772
|
+
if (draggingKey === null && !sameIds(order, rowKeys)) setOrder(rowKeys);
|
|
773
|
+
if (prevKeysRef.current !== rowKeys) {
|
|
774
|
+
const prev = prevKeysRef.current;
|
|
775
|
+
const removed = prev.some((key) => !rowKeys.includes(key));
|
|
776
|
+
const reordered = !sameIds(prev.filter((key) => rowKeys.includes(key)), rowKeys.filter((key) => prev.includes(key)));
|
|
777
|
+
if (removed || reordered) armSlide();
|
|
778
|
+
prevKeysRef.current = rowKeys;
|
|
779
|
+
}
|
|
780
|
+
/**
|
|
781
|
+
* The seam candidate at the pointer's Y (the drag's live preview and
|
|
782
|
+
* the release's commit input share it): the visible rows' viewport
|
|
783
|
+
* boxes against the seam rule (`reorder.ts`). Null when the measurement
|
|
784
|
+
* is stale (the segment vanished, or the rows are gone) — the caller
|
|
785
|
+
* keeps the preview (move) or skips the commit (release, the
|
|
786
|
+
* render-phase resync snaps the order back to the store).
|
|
787
|
+
*/
|
|
788
|
+
const seamCandidateAt = useCallback((id, pointerY) => {
|
|
789
|
+
const root = rootRef.current;
|
|
790
|
+
if (root === null) return null;
|
|
791
|
+
const session = entryContainerRef.current;
|
|
792
|
+
const containerId = session !== null && session.rootBlockId === rootBlockId ? session.value : entrySequenceParent(doc, rootBlockId);
|
|
793
|
+
const segment = segmentRange(doc, containerId, id);
|
|
794
|
+
if (segment === null) return null;
|
|
795
|
+
const segmentIds = new Set(doc.getChildren(containerId).slice(segment.start, segment.end));
|
|
796
|
+
const rows = [];
|
|
797
|
+
for (const el of Array.from(root.querySelectorAll("[data-slot=\"docblocks/row\"]"))) {
|
|
798
|
+
const wrapperId = el.querySelector("[data-block-id]")?.getAttribute("data-block-id") ?? null;
|
|
799
|
+
if (wrapperId === null) continue;
|
|
800
|
+
const node = nodesById.get(wrapperId);
|
|
801
|
+
if (node === void 0) continue;
|
|
802
|
+
let top = 0;
|
|
803
|
+
for (let ancestor = el; ancestor !== null; ancestor = ancestor.offsetParent) top += ancestor.offsetTop;
|
|
804
|
+
top -= window.scrollY;
|
|
805
|
+
rows.push({
|
|
806
|
+
id: wrapperId,
|
|
807
|
+
top,
|
|
808
|
+
bottom: top + el.offsetHeight,
|
|
809
|
+
indent: node.indent
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
if (rows.length === 0) return null;
|
|
813
|
+
return dropCandidateIndent(rows, pointerY, segmentIds, id);
|
|
814
|
+
}, [
|
|
815
|
+
doc,
|
|
816
|
+
nodesById,
|
|
817
|
+
rootBlockId,
|
|
818
|
+
rootRef
|
|
819
|
+
]);
|
|
820
|
+
return {
|
|
821
|
+
order,
|
|
822
|
+
draggingKey,
|
|
823
|
+
previewIndent,
|
|
824
|
+
layoutSlide,
|
|
825
|
+
onRowDragStart: useCallback((id) => {
|
|
826
|
+
armSlide();
|
|
827
|
+
setDraggingKey(id);
|
|
828
|
+
setPreviewIndent(null);
|
|
829
|
+
entryContainerRef.current = {
|
|
830
|
+
rootBlockId,
|
|
831
|
+
value: entrySequenceParent(doc, rootBlockId)
|
|
832
|
+
};
|
|
833
|
+
}, [
|
|
834
|
+
armSlide,
|
|
835
|
+
doc,
|
|
836
|
+
rootBlockId
|
|
837
|
+
]),
|
|
838
|
+
onRowDragMove: useCallback((id, event) => {
|
|
839
|
+
const pointerY = pointerYOf(event);
|
|
840
|
+
if (pointerY === null) return;
|
|
841
|
+
const candidate = seamCandidateAt(id, pointerY);
|
|
842
|
+
if (candidate === null) return;
|
|
843
|
+
setPreviewIndent(candidate);
|
|
844
|
+
}, [seamCandidateAt]),
|
|
845
|
+
onRowDragEnd: useCallback((id, event) => {
|
|
846
|
+
armSlide();
|
|
847
|
+
setDraggingKey(null);
|
|
848
|
+
rootRef.current?.focus({ preventScroll: true });
|
|
849
|
+
const pointerY = pointerYOf(event);
|
|
850
|
+
setPreviewIndent(null);
|
|
851
|
+
if (pointerY === null) return;
|
|
852
|
+
const candidate = seamCandidateAt(id, pointerY);
|
|
853
|
+
if (candidate === null) return;
|
|
854
|
+
commitFlatDrag(doc, rootBlockId, id, orderRef.current, store.getState().collapsedIds, candidate);
|
|
855
|
+
}, [
|
|
856
|
+
armSlide,
|
|
857
|
+
doc,
|
|
858
|
+
orderRef,
|
|
859
|
+
rootBlockId,
|
|
860
|
+
rootRef,
|
|
861
|
+
seamCandidateAt,
|
|
862
|
+
store
|
|
863
|
+
]),
|
|
864
|
+
onRowReorder: useCallback((next) => {
|
|
865
|
+
setOrder(clampTailSeatCrossing(next, draggingKey ?? "", rootBlockId === doc.meta.id && draggingKey !== null ? tailSeatId(doc, rootBlockId, draggingKey, rowKeys[rowKeys.length - 1]) : void 0));
|
|
866
|
+
}, [
|
|
867
|
+
doc,
|
|
868
|
+
draggingKey,
|
|
869
|
+
rootBlockId,
|
|
870
|
+
rowKeys
|
|
871
|
+
])
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
//#endregion
|
|
875
|
+
//#region src/motion/MotionEditor.tsx
|
|
876
|
+
/**
|
|
877
|
+
* `MotionEditor` — the animated editor view, a drop-in replacement for the
|
|
878
|
+
* base `Editor` under the same `EditorProvider`: the same root element
|
|
879
|
+
* (the focus/keyboard scope), the same block rendering and editing
|
|
880
|
+
* wiring, and the same presence overlay. The page view renders the FLAT
|
|
881
|
+
* page sequence (every page block is its own `Reorder.Item` row), the
|
|
882
|
+
* top-level rows render through `AnimatePresence` (`sync`), and the
|
|
883
|
+
* drag-reorder is pure framer `Reorder` (one drag = one range move of the
|
|
884
|
+
* dragged block's derived subtree).
|
|
885
|
+
*/
|
|
886
|
+
function MotionEditor({ view = "page", formatPalette = FORMAT_PALETTE }) {
|
|
887
|
+
const { doc, store, rootBlockId, viewMode } = useEditorContext();
|
|
888
|
+
const rootChildren = useStore(store, (state) => state.rootChildren);
|
|
889
|
+
const seqStamp = useStore(store, (state) => state.seqStamp);
|
|
890
|
+
const collapsedIds = useStore(store, (state) => state.collapsedIds);
|
|
891
|
+
const pageChildren = useMemo(() => rootChildren.filter((id) => store.getState().blocks[id]?.type !== "surface"), [
|
|
892
|
+
rootChildren,
|
|
893
|
+
seqStamp,
|
|
894
|
+
store
|
|
895
|
+
]);
|
|
896
|
+
const rootContext = useMemo(() => ({
|
|
897
|
+
parentId: entrySequenceParent(doc, rootBlockId),
|
|
898
|
+
siblings: rootChildren
|
|
899
|
+
}), [
|
|
900
|
+
doc,
|
|
901
|
+
rootBlockId,
|
|
902
|
+
rootChildren
|
|
903
|
+
]);
|
|
904
|
+
const { rootRef, onKeyDown, onCopy, onPointerDown: onCrossDragPointerDown } = useEditing();
|
|
905
|
+
const slash = useSlashMenu(rootRef);
|
|
906
|
+
const mention = useMentionMenu(rootRef);
|
|
907
|
+
const format = useFormatToolbar(rootRef, formatPalette);
|
|
908
|
+
useTypingFlag(rootRef);
|
|
909
|
+
const enterGate = useEnterAnimationGate(rootRef);
|
|
910
|
+
const prevRef = useRef(null);
|
|
911
|
+
const derivation = useMemo(() => {
|
|
912
|
+
const result = deriveFlat(pageChildren, store.getState().blocks, prevRef.current ?? void 0);
|
|
913
|
+
prevRef.current = result.byId;
|
|
914
|
+
return result;
|
|
915
|
+
}, [
|
|
916
|
+
pageChildren,
|
|
917
|
+
seqStamp,
|
|
918
|
+
store
|
|
919
|
+
]);
|
|
920
|
+
const rowKeys = useMemo(() => visibleRows(pageChildren, derivation.byId, collapsedIds), [
|
|
921
|
+
pageChildren,
|
|
922
|
+
derivation,
|
|
923
|
+
collapsedIds
|
|
924
|
+
]);
|
|
925
|
+
const nodesById = derivation.byId;
|
|
926
|
+
const emptyKey = useStore(store, (state) => viewMode ? pageChildren.map((id) => {
|
|
927
|
+
const block = state.blocks[id];
|
|
928
|
+
return isEmptyChainText(block) ? "o" : block === void 0 ? "-" : ".";
|
|
929
|
+
}).join("") : "");
|
|
930
|
+
const { order, draggingKey, previewIndent, layoutSlide, onRowDragStart, onRowDragMove, onRowDragEnd, onRowReorder } = useMotionDrag({
|
|
931
|
+
doc,
|
|
932
|
+
rootBlockId,
|
|
933
|
+
store,
|
|
934
|
+
rootRef,
|
|
935
|
+
rowKeys: useMemo(() => {
|
|
936
|
+
if (!viewMode) return rowKeys;
|
|
937
|
+
const nodes = [];
|
|
938
|
+
for (const id of rowKeys) {
|
|
939
|
+
const node = nodesById.get(id);
|
|
940
|
+
if (node !== void 0) nodes.push(node);
|
|
941
|
+
}
|
|
942
|
+
return collapseEmptyTextChains(nodes, (id) => isEmptyChainText(store.getState().blocks[id])).map((node) => node.id);
|
|
943
|
+
}, [
|
|
944
|
+
rowKeys,
|
|
945
|
+
viewMode,
|
|
946
|
+
emptyKey,
|
|
947
|
+
nodesById,
|
|
948
|
+
store
|
|
949
|
+
]),
|
|
950
|
+
nodesById
|
|
951
|
+
});
|
|
952
|
+
const onRootPointerDown = (event) => {
|
|
953
|
+
onCrossDragPointerDown(event);
|
|
954
|
+
};
|
|
955
|
+
return /* @__PURE__ */ jsx(Card, {
|
|
956
|
+
className: cn(view === "canvas" && "min-h-0 flex-1"),
|
|
957
|
+
children: /* @__PURE__ */ jsx(EnumOptionsMotionSlot.Provider, {
|
|
958
|
+
value: MotionEnumOptionsEditor,
|
|
959
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
960
|
+
ref: rootRef,
|
|
961
|
+
"data-slot": "docblocks/editor",
|
|
962
|
+
"data-docblocks-motion": "",
|
|
963
|
+
"data-view": view,
|
|
964
|
+
"data-readonly": viewMode ? "true" : void 0,
|
|
965
|
+
tabIndex: -1,
|
|
966
|
+
className: cn("relative min-h-0 flex-1 selection:bg-(--docblocks-selection-background)", "font-(family-name:--docblocks-font-family) text-(length:--docblocks-font-size)", "leading-(--docblocks-line-height) text-(--docblocks-text-color)"),
|
|
967
|
+
onKeyDown,
|
|
968
|
+
onCopy,
|
|
969
|
+
onPointerDown: onRootPointerDown,
|
|
970
|
+
children: [
|
|
971
|
+
/* @__PURE__ */ jsx("style", { children: HANDLE_GRAB_STYLE }),
|
|
972
|
+
/* @__PURE__ */ jsx(MentionMenuContext.Provider, {
|
|
973
|
+
value: mention.api,
|
|
974
|
+
children: /* @__PURE__ */ jsx(SlashMenuContext.Provider, {
|
|
975
|
+
value: slash.api,
|
|
976
|
+
children: /* @__PURE__ */ jsx(FormatPaletteContext.Provider, {
|
|
977
|
+
value: formatPalette,
|
|
978
|
+
children: /* @__PURE__ */ jsxs(BlockContext.Provider, {
|
|
979
|
+
value: rootContext,
|
|
980
|
+
children: [view === "canvas" ? /* @__PURE__ */ jsx(CanvasView, {}) : /* @__PURE__ */ jsx("div", {
|
|
981
|
+
"data-slot": "docblocks/page",
|
|
982
|
+
className: "\n mx-auto w-full max-w-(--docblocks-page-column-max-width) px-(--docblocks-page-padding-x)\n ",
|
|
983
|
+
children: /* @__PURE__ */ jsx(RowList, {
|
|
984
|
+
order,
|
|
985
|
+
nodesById,
|
|
986
|
+
layoutSlide: layoutSlide || draggingKey !== null,
|
|
987
|
+
draggingKey,
|
|
988
|
+
previewIndent,
|
|
989
|
+
animated: enterGate.current,
|
|
990
|
+
onReorder: onRowReorder,
|
|
991
|
+
onDragStart: onRowDragStart,
|
|
992
|
+
onDragMove: onRowDragMove,
|
|
993
|
+
onDragEnd: onRowDragEnd
|
|
994
|
+
})
|
|
995
|
+
}), viewMode ? null : /* @__PURE__ */ jsx(PresenceCaretOverlay, { rootRef })]
|
|
996
|
+
})
|
|
997
|
+
})
|
|
998
|
+
})
|
|
999
|
+
}),
|
|
1000
|
+
slash.menuNode,
|
|
1001
|
+
mention.menuNode,
|
|
1002
|
+
viewMode ? null : format.toolbarNode
|
|
1003
|
+
]
|
|
1004
|
+
})
|
|
1005
|
+
})
|
|
1006
|
+
});
|
|
1007
|
+
}
|
|
1008
|
+
//#endregion
|
|
1009
|
+
export { MotionEditor };
|