quoin-editor 0.3.0 → 0.4.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/README.md +42 -8
- package/dist/headless.cjs +21 -11
- package/dist/headless.cjs.map +1 -1
- package/dist/headless.d.cts +1 -1
- package/dist/headless.d.ts +1 -1
- package/dist/headless.js +21 -11
- package/dist/headless.js.map +1 -1
- package/dist/index.cjs +301 -172
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +35 -18
- package/dist/index.d.ts +35 -18
- package/dist/index.js +303 -174
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +23 -6
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
// src/canvas/BlockCanvas.tsx
|
|
4
|
-
import * as
|
|
4
|
+
import * as React15 from "react";
|
|
5
5
|
import {
|
|
6
6
|
DndContext,
|
|
7
7
|
KeyboardSensor,
|
|
@@ -68,12 +68,13 @@ function paletteBlocks(definitions) {
|
|
|
68
68
|
import { Code as Code2, FileText, MessageSquare, Image, Paperclip } from "lucide-react";
|
|
69
69
|
|
|
70
70
|
// src/blocks/MarkdownBlockEditor.tsx
|
|
71
|
-
import * as
|
|
71
|
+
import * as React7 from "react";
|
|
72
72
|
|
|
73
73
|
// src/upload/FilePickerButton.tsx
|
|
74
74
|
import { Upload } from "lucide-react";
|
|
75
75
|
|
|
76
76
|
// src/ui/button.tsx
|
|
77
|
+
import * as React2 from "react";
|
|
77
78
|
import { Slot } from "@radix-ui/react-slot";
|
|
78
79
|
import { cva } from "class-variance-authority";
|
|
79
80
|
|
|
@@ -84,6 +85,15 @@ function cn(...inputs) {
|
|
|
84
85
|
return twMerge(clsx(inputs));
|
|
85
86
|
}
|
|
86
87
|
|
|
88
|
+
// src/ui/focus.ts
|
|
89
|
+
var FOCUS_RING = [
|
|
90
|
+
"outline-hidden",
|
|
91
|
+
"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-solid",
|
|
92
|
+
"focus-visible:outline-[var(--quoin-ring)]"
|
|
93
|
+
].join(" ");
|
|
94
|
+
var HIGHLIGHT_BAR = "data-[highlighted]:shadow-[inset_2px_0_0_var(--quoin-foreground)]";
|
|
95
|
+
var SELECTED_BAR = "data-[selected=true]:shadow-[inset_2px_0_0_var(--quoin-foreground)]";
|
|
96
|
+
|
|
87
97
|
// src/ui/touch-target.ts
|
|
88
98
|
var TAP_TARGET_FLOOR = "pointer-coarse:min-h-11";
|
|
89
99
|
var SQUARE_TAP_TARGET_FLOOR = `${TAP_TARGET_FLOOR} pointer-coarse:min-w-11`;
|
|
@@ -96,7 +106,10 @@ var ICON_TAP_TARGET_FLOOR = [
|
|
|
96
106
|
// src/ui/button.tsx
|
|
97
107
|
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
98
108
|
var buttonVariants = cva(
|
|
99
|
-
|
|
109
|
+
[
|
|
110
|
+
"inline-flex cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-[color,background-color,border-color,box-shadow,opacity] disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 shrink-0",
|
|
111
|
+
FOCUS_RING
|
|
112
|
+
],
|
|
100
113
|
{
|
|
101
114
|
variants: {
|
|
102
115
|
variant: {
|
|
@@ -121,17 +134,12 @@ var buttonVariants = cva(
|
|
|
121
134
|
defaultVariants: { variant: "default", size: "default" }
|
|
122
135
|
}
|
|
123
136
|
);
|
|
124
|
-
|
|
125
|
-
className,
|
|
126
|
-
variant = "default",
|
|
127
|
-
size = "default",
|
|
128
|
-
asChild = false,
|
|
129
|
-
...props
|
|
130
|
-
}) {
|
|
137
|
+
var Button = React2.forwardRef(function Button2({ className, variant = "default", size = "default", asChild = false, ...props }, ref) {
|
|
131
138
|
const Comp = asChild ? Slot : "button";
|
|
132
139
|
return /* @__PURE__ */ jsx2(
|
|
133
140
|
Comp,
|
|
134
141
|
{
|
|
142
|
+
ref,
|
|
135
143
|
"data-slot": "button",
|
|
136
144
|
"data-variant": variant,
|
|
137
145
|
"data-size": size,
|
|
@@ -139,12 +147,12 @@ function Button({
|
|
|
139
147
|
...props
|
|
140
148
|
}
|
|
141
149
|
);
|
|
142
|
-
}
|
|
150
|
+
});
|
|
143
151
|
|
|
144
152
|
// src/upload/FilePickerButton.tsx
|
|
145
153
|
import { jsx as jsx3, jsxs } from "react/jsx-runtime";
|
|
146
154
|
function FilePickerButton({ label, accept, disabled, onPick }) {
|
|
147
|
-
return /* @__PURE__ */ jsxs("span", { className: "relative inline-flex rounded-md has-[:focus-visible]:
|
|
155
|
+
return /* @__PURE__ */ jsxs("span", { className: "relative inline-flex rounded-md has-[:focus-visible]:outline-2 has-[:focus-visible]:outline-offset-2 has-[:focus-visible]:outline-[var(--quoin-ring)]", children: [
|
|
148
156
|
/* @__PURE__ */ jsx3(Button, { asChild: true, variant: "outline", size: "sm", children: /* @__PURE__ */ jsxs("span", { "aria-hidden": "true", className: disabled ? "pointer-events-none opacity-50" : void 0, children: [
|
|
149
157
|
/* @__PURE__ */ jsx3(Upload, { "aria-hidden": "true" }),
|
|
150
158
|
label
|
|
@@ -235,15 +243,18 @@ function insertCommand(command, value, caret) {
|
|
|
235
243
|
};
|
|
236
244
|
}
|
|
237
245
|
|
|
246
|
+
// src/markdown/platform.ts
|
|
247
|
+
function isMac() {
|
|
248
|
+
if (typeof navigator === "undefined") return false;
|
|
249
|
+
const { userAgentData, platform } = navigator;
|
|
250
|
+
return /mac|iphone|ipad/i.test(userAgentData?.platform || platform || "");
|
|
251
|
+
}
|
|
252
|
+
|
|
238
253
|
// src/markdown/shortcut-label.ts
|
|
239
254
|
var MAC_LABEL = { mod: "\u2318", alt: "\u2325", shift: "\u21E7" };
|
|
240
255
|
var OTHER_LABEL = { mod: "Ctrl", alt: "Alt", shift: "Shift" };
|
|
241
|
-
function onMac() {
|
|
242
|
-
if (typeof navigator === "undefined") return false;
|
|
243
|
-
return /Mac|iPhone|iPad/.test(navigator.userAgent);
|
|
244
|
-
}
|
|
245
256
|
function formatShortcut(hint) {
|
|
246
|
-
const mac =
|
|
257
|
+
const mac = isMac();
|
|
247
258
|
const labels = mac ? MAC_LABEL : OTHER_LABEL;
|
|
248
259
|
const parts = hint.split("+").map((part) => labels[part] ?? part.toUpperCase());
|
|
249
260
|
return parts.join(mac ? "" : "+");
|
|
@@ -297,7 +308,8 @@ function DialogContent({
|
|
|
297
308
|
DialogPrimitive.Close,
|
|
298
309
|
{
|
|
299
310
|
className: cn(
|
|
300
|
-
"
|
|
311
|
+
"absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100",
|
|
312
|
+
FOCUS_RING,
|
|
301
313
|
ICON_TAP_TARGET_FLOOR
|
|
302
314
|
),
|
|
303
315
|
children: [
|
|
@@ -597,6 +609,7 @@ function CommandItem({
|
|
|
597
609
|
"data-slot": "command-item",
|
|
598
610
|
className: cn(
|
|
599
611
|
"relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground",
|
|
612
|
+
SELECTED_BAR,
|
|
600
613
|
className
|
|
601
614
|
),
|
|
602
615
|
...props
|
|
@@ -713,7 +726,7 @@ function SlashPalette({
|
|
|
713
726
|
}
|
|
714
727
|
|
|
715
728
|
// src/markdown/useMarkdownKeymap.ts
|
|
716
|
-
import * as
|
|
729
|
+
import * as React3 from "react";
|
|
717
730
|
|
|
718
731
|
// src/markdown/list-continuation.ts
|
|
719
732
|
var LIST_MARKER = /^(\s*)((?:[-*+] \[[ xX]\] )|(?:[-*+] )|(?:\d+[.)] )|(?:> ))(.*)$/;
|
|
@@ -937,11 +950,12 @@ function useMarkdownKeymap({
|
|
|
937
950
|
commit,
|
|
938
951
|
onOpenPalette
|
|
939
952
|
}) {
|
|
940
|
-
const onKeyDown =
|
|
953
|
+
const onKeyDown = React3.useCallback(
|
|
941
954
|
(event) => {
|
|
955
|
+
if (event.nativeEvent.isComposing || event.keyCode === 229) return;
|
|
942
956
|
const el = event.currentTarget;
|
|
943
957
|
const { value, selectionStart, selectionEnd } = el;
|
|
944
|
-
const mod = event.metaKey
|
|
958
|
+
const mod = isMac() ? event.metaKey : event.ctrlKey;
|
|
945
959
|
const run = (edit) => {
|
|
946
960
|
if (!edit) return;
|
|
947
961
|
event.preventDefault();
|
|
@@ -982,7 +996,7 @@ function useMarkdownKeymap({
|
|
|
982
996
|
},
|
|
983
997
|
[commit, onOpenPalette]
|
|
984
998
|
);
|
|
985
|
-
const onPaste =
|
|
999
|
+
const onPaste = React3.useCallback(
|
|
986
1000
|
(event) => {
|
|
987
1001
|
const el = event.currentTarget;
|
|
988
1002
|
const edit = linkPaste(
|
|
@@ -1001,7 +1015,7 @@ function useMarkdownKeymap({
|
|
|
1001
1015
|
}
|
|
1002
1016
|
|
|
1003
1017
|
// src/markdown/useSlashPalette.ts
|
|
1004
|
-
import * as
|
|
1018
|
+
import * as React4 from "react";
|
|
1005
1019
|
|
|
1006
1020
|
// src/markdown/caret-coordinates.ts
|
|
1007
1021
|
var COPIED = [
|
|
@@ -1052,10 +1066,10 @@ function caretRect(el) {
|
|
|
1052
1066
|
|
|
1053
1067
|
// src/markdown/useSlashPalette.ts
|
|
1054
1068
|
function useSlashPalette(ref) {
|
|
1055
|
-
const [open, setOpen] =
|
|
1056
|
-
const [caret, setCaret] =
|
|
1057
|
-
const offsetRef =
|
|
1058
|
-
const openAtCaret =
|
|
1069
|
+
const [open, setOpen] = React4.useState(false);
|
|
1070
|
+
const [caret, setCaret] = React4.useState(null);
|
|
1071
|
+
const offsetRef = React4.useRef(0);
|
|
1072
|
+
const openAtCaret = React4.useCallback(() => {
|
|
1059
1073
|
const el = ref.current;
|
|
1060
1074
|
if (!el) return;
|
|
1061
1075
|
offsetRef.current = el.selectionStart;
|
|
@@ -1066,11 +1080,17 @@ function useSlashPalette(ref) {
|
|
|
1066
1080
|
}
|
|
1067
1081
|
|
|
1068
1082
|
// src/hooks/useAutoGrow.ts
|
|
1069
|
-
import * as
|
|
1083
|
+
import * as React6 from "react";
|
|
1084
|
+
|
|
1085
|
+
// src/hooks/use-isomorphic-layout-effect.ts
|
|
1086
|
+
import * as React5 from "react";
|
|
1087
|
+
var useIsomorphicLayoutEffect = typeof document === "undefined" ? React5.useEffect : React5.useLayoutEffect;
|
|
1088
|
+
|
|
1089
|
+
// src/hooks/useAutoGrow.ts
|
|
1070
1090
|
function useAutoGrow(value) {
|
|
1071
|
-
const ref =
|
|
1072
|
-
const lastWidth =
|
|
1073
|
-
const fit =
|
|
1091
|
+
const ref = React6.useRef(null);
|
|
1092
|
+
const lastWidth = React6.useRef(null);
|
|
1093
|
+
const fit = React6.useCallback(() => {
|
|
1074
1094
|
const el = ref.current;
|
|
1075
1095
|
if (!el) return;
|
|
1076
1096
|
const scroller = document.scrollingElement ?? document.documentElement;
|
|
@@ -1079,8 +1099,8 @@ function useAutoGrow(value) {
|
|
|
1079
1099
|
if (el.scrollHeight > 0) el.style.height = `${el.scrollHeight}px`;
|
|
1080
1100
|
if (scroller.scrollTop !== scrollTop) scroller.scrollTop = scrollTop;
|
|
1081
1101
|
}, []);
|
|
1082
|
-
|
|
1083
|
-
|
|
1102
|
+
useIsomorphicLayoutEffect(fit, [fit, value]);
|
|
1103
|
+
React6.useEffect(() => {
|
|
1084
1104
|
const el = ref.current;
|
|
1085
1105
|
if (!el || typeof ResizeObserver === "undefined") return;
|
|
1086
1106
|
const observer = new ResizeObserver(([entry]) => {
|
|
@@ -1102,17 +1122,18 @@ var str = (v) => typeof v === "string" ? v : "";
|
|
|
1102
1122
|
import { jsx as jsx9, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
1103
1123
|
var BOX = [
|
|
1104
1124
|
"-mx-2 min-h-7 w-[calc(100%+1rem)] resize-none rounded-md border-0 bg-transparent px-2 py-0",
|
|
1105
|
-
"text-base leading-7
|
|
1106
|
-
"transition-[color,box-shadow]
|
|
1125
|
+
"text-base leading-7 placeholder:text-muted-foreground",
|
|
1126
|
+
"transition-[color,box-shadow]",
|
|
1127
|
+
FOCUS_RING
|
|
1107
1128
|
].join(" ");
|
|
1108
1129
|
function MarkdownBlockEditor({ data, onChange, onInsertBlock, onDone }) {
|
|
1109
1130
|
const { onError } = useQuoin();
|
|
1110
1131
|
const markdown = str(data.markdown);
|
|
1111
1132
|
const ref = useAutoGrow(markdown);
|
|
1112
1133
|
const palette = useSlashPalette(ref);
|
|
1113
|
-
const [shortcutsOpen, setShortcutsOpen] =
|
|
1134
|
+
const [shortcutsOpen, setShortcutsOpen] = React7.useState(false);
|
|
1114
1135
|
const set = (patch) => onChange({ ...data, ...patch });
|
|
1115
|
-
const commit =
|
|
1136
|
+
const commit = React7.useCallback(
|
|
1116
1137
|
(next) => onChange({ ...data, markdown: next }),
|
|
1117
1138
|
[data, onChange]
|
|
1118
1139
|
);
|
|
@@ -1178,6 +1199,7 @@ function MarkdownBlockEditor({ data, onChange, onInsertBlock, onDone }) {
|
|
|
1178
1199
|
onBlockCommand: (type) => {
|
|
1179
1200
|
palette.setOpen(false);
|
|
1180
1201
|
onInsertBlock?.(type);
|
|
1202
|
+
onDone?.();
|
|
1181
1203
|
},
|
|
1182
1204
|
onShortcuts: () => {
|
|
1183
1205
|
palette.setOpen(false);
|
|
@@ -1214,7 +1236,8 @@ function SelectTrigger({ className, children, ...props }) {
|
|
|
1214
1236
|
{
|
|
1215
1237
|
"data-slot": "select-trigger",
|
|
1216
1238
|
className: cn(
|
|
1217
|
-
"border-input data-[placeholder]:text-muted-foreground flex h-9 w-full cursor-pointer items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm shadow-xs
|
|
1239
|
+
"border-input data-[placeholder]:text-muted-foreground flex h-9 w-full cursor-pointer items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm shadow-xs disabled:cursor-not-allowed disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
|
1240
|
+
FOCUS_RING,
|
|
1218
1241
|
TAP_TARGET_FLOOR,
|
|
1219
1242
|
className
|
|
1220
1243
|
),
|
|
@@ -1253,6 +1276,7 @@ function SelectItem({ className, children, ...props }) {
|
|
|
1253
1276
|
"data-slot": "select-item",
|
|
1254
1277
|
className: cn(
|
|
1255
1278
|
"focus:bg-accent focus:text-accent-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
1279
|
+
HIGHLIGHT_BAR,
|
|
1256
1280
|
TAP_TARGET_FLOOR,
|
|
1257
1281
|
className
|
|
1258
1282
|
),
|
|
@@ -1266,22 +1290,25 @@ function SelectItem({ className, children, ...props }) {
|
|
|
1266
1290
|
}
|
|
1267
1291
|
|
|
1268
1292
|
// src/ui/textarea.tsx
|
|
1293
|
+
import * as React8 from "react";
|
|
1269
1294
|
import { jsx as jsx11 } from "react/jsx-runtime";
|
|
1270
|
-
|
|
1295
|
+
var Textarea = React8.forwardRef(function Textarea2({ className, ...props }, ref) {
|
|
1271
1296
|
return /* @__PURE__ */ jsx11(
|
|
1272
1297
|
"textarea",
|
|
1273
1298
|
{
|
|
1299
|
+
ref,
|
|
1274
1300
|
"data-slot": "textarea",
|
|
1275
1301
|
className: cn(
|
|
1276
|
-
"border-input placeholder:text-muted-foreground flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow]
|
|
1277
|
-
|
|
1278
|
-
"
|
|
1302
|
+
"border-input placeholder:text-muted-foreground flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] disabled:pointer-events-none disabled:opacity-50 md:text-sm",
|
|
1303
|
+
FOCUS_RING,
|
|
1304
|
+
"focus-visible:border-ring",
|
|
1305
|
+
"aria-invalid:border-destructive",
|
|
1279
1306
|
className
|
|
1280
1307
|
),
|
|
1281
1308
|
...props
|
|
1282
1309
|
}
|
|
1283
1310
|
);
|
|
1284
|
-
}
|
|
1311
|
+
});
|
|
1285
1312
|
|
|
1286
1313
|
// src/blocks/CalloutBlockEditor.tsx
|
|
1287
1314
|
import { jsx as jsx12, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
@@ -1304,24 +1331,27 @@ function CalloutBlockEditor({ data, onChange }) {
|
|
|
1304
1331
|
}
|
|
1305
1332
|
|
|
1306
1333
|
// src/ui/input.tsx
|
|
1334
|
+
import * as React9 from "react";
|
|
1307
1335
|
import { jsx as jsx13 } from "react/jsx-runtime";
|
|
1308
|
-
|
|
1336
|
+
var Input = React9.forwardRef(function Input2({ className, type, ...props }, ref) {
|
|
1309
1337
|
return /* @__PURE__ */ jsx13(
|
|
1310
1338
|
"input",
|
|
1311
1339
|
{
|
|
1340
|
+
ref,
|
|
1312
1341
|
type,
|
|
1313
1342
|
"data-slot": "input",
|
|
1314
1343
|
className: cn(
|
|
1315
|
-
"border-input placeholder:text-muted-foreground flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow]
|
|
1316
|
-
|
|
1317
|
-
"
|
|
1344
|
+
"border-input placeholder:text-muted-foreground flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:opacity-50 md:text-sm",
|
|
1345
|
+
FOCUS_RING,
|
|
1346
|
+
"focus-visible:border-ring",
|
|
1347
|
+
"aria-invalid:border-destructive",
|
|
1318
1348
|
TAP_TARGET_FLOOR,
|
|
1319
1349
|
className
|
|
1320
1350
|
),
|
|
1321
1351
|
...props
|
|
1322
1352
|
}
|
|
1323
1353
|
);
|
|
1324
|
-
}
|
|
1354
|
+
});
|
|
1325
1355
|
|
|
1326
1356
|
// src/blocks/CodeBlockEditor.tsx
|
|
1327
1357
|
import { jsx as jsx14, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
@@ -1356,15 +1386,15 @@ function CodeBlockEditor({ data, onChange }) {
|
|
|
1356
1386
|
}
|
|
1357
1387
|
|
|
1358
1388
|
// src/blocks/ImageBlockEditor.tsx
|
|
1359
|
-
import { useEffect as
|
|
1389
|
+
import { useEffect as useEffect3, useRef as useRef3 } from "react";
|
|
1360
1390
|
|
|
1361
1391
|
// src/upload/UploadField.tsx
|
|
1362
|
-
import * as
|
|
1392
|
+
import * as React10 from "react";
|
|
1363
1393
|
import { jsx as jsx15, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
1364
1394
|
function UploadField({ category, label, accept, onUploaded }) {
|
|
1365
1395
|
const { upload, onError } = useQuoin();
|
|
1366
|
-
const [busy, setBusy] =
|
|
1367
|
-
const [error, setError] =
|
|
1396
|
+
const [busy, setBusy] = React10.useState(false);
|
|
1397
|
+
const [error, setError] = React10.useState(null);
|
|
1368
1398
|
async function run(file) {
|
|
1369
1399
|
if (!upload) return;
|
|
1370
1400
|
setBusy(true);
|
|
@@ -1377,11 +1407,12 @@ function UploadField({ category, label, accept, onUploaded }) {
|
|
|
1377
1407
|
setBusy(false);
|
|
1378
1408
|
return;
|
|
1379
1409
|
}
|
|
1380
|
-
setBusy(false);
|
|
1381
1410
|
try {
|
|
1382
|
-
onUploaded(result);
|
|
1411
|
+
await onUploaded(result);
|
|
1383
1412
|
} catch (cause) {
|
|
1384
1413
|
onError(cause instanceof Error ? cause : new Error(String(cause)));
|
|
1414
|
+
} finally {
|
|
1415
|
+
setBusy(false);
|
|
1385
1416
|
}
|
|
1386
1417
|
}
|
|
1387
1418
|
return /* @__PURE__ */ jsxs10("div", { className: "flex flex-col items-start gap-1.5", children: [
|
|
@@ -1416,14 +1447,18 @@ import { jsx as jsx16, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
|
1416
1447
|
function ImageBlockEditor({ data, onChange }) {
|
|
1417
1448
|
const { resolveAssetUrl } = useQuoin();
|
|
1418
1449
|
const latest = useRef3(data);
|
|
1419
|
-
|
|
1450
|
+
useEffect3(() => {
|
|
1420
1451
|
latest.current = data;
|
|
1421
1452
|
});
|
|
1422
1453
|
const set = (patch) => onChange({ ...latest.current, ...patch });
|
|
1423
|
-
const
|
|
1424
|
-
|
|
1454
|
+
const newestKey = useRef3("");
|
|
1455
|
+
const onUploaded = async ({ storageKey }) => {
|
|
1456
|
+
newestKey.current = storageKey;
|
|
1457
|
+
const { width: _width, height: _height, ...unsized } = latest.current;
|
|
1458
|
+
onChange({ ...unsized, storageKey });
|
|
1459
|
+
const src = resolveAssetUrl(storageKey);
|
|
1425
1460
|
const size = src ? await intrinsicSize(src) : null;
|
|
1426
|
-
set({ storageKey
|
|
1461
|
+
if (size && newestKey.current === storageKey) set({ storageKey, ...size });
|
|
1427
1462
|
};
|
|
1428
1463
|
return /* @__PURE__ */ jsxs11("div", { className: "flex flex-col gap-2", children: [
|
|
1429
1464
|
/* @__PURE__ */ jsx16(UploadField, { category: "image", label: "Upload image", onUploaded }),
|
|
@@ -1451,7 +1486,7 @@ function ImageBlockEditor({ data, onChange }) {
|
|
|
1451
1486
|
}
|
|
1452
1487
|
)
|
|
1453
1488
|
] }),
|
|
1454
|
-
str(data.storageKey) && /* @__PURE__ */ jsx16("span", { className: "text-xs text-muted-foreground", children: "Image attached
|
|
1489
|
+
str(data.storageKey) && /* @__PURE__ */ jsx16("span", { className: "text-xs text-muted-foreground", children: "Image attached" })
|
|
1455
1490
|
] });
|
|
1456
1491
|
}
|
|
1457
1492
|
|
|
@@ -1478,12 +1513,12 @@ function FileBlockEditor({ data, onChange }) {
|
|
|
1478
1513
|
// src/blocks/previews.tsx
|
|
1479
1514
|
import { jsx as jsx18 } from "react/jsx-runtime";
|
|
1480
1515
|
var HINT = "text-sm text-muted-foreground";
|
|
1481
|
-
var
|
|
1482
|
-
info: "border-
|
|
1483
|
-
tip: "border-
|
|
1484
|
-
warning: "border-
|
|
1485
|
-
success: "border-
|
|
1486
|
-
danger: "border-destructive"
|
|
1516
|
+
var TONE = {
|
|
1517
|
+
info: "border-info bg-info/15",
|
|
1518
|
+
tip: "border-primary bg-primary/15",
|
|
1519
|
+
warning: "border-warning bg-warning/15",
|
|
1520
|
+
success: "border-success bg-success/15",
|
|
1521
|
+
danger: "border-destructive bg-destructive/15"
|
|
1487
1522
|
};
|
|
1488
1523
|
function MarkdownPreview({ data }) {
|
|
1489
1524
|
const markdown = str(data.markdown);
|
|
@@ -1510,8 +1545,8 @@ function CalloutPreview({ data }) {
|
|
|
1510
1545
|
const body = str(data.body);
|
|
1511
1546
|
if (body.trim() === "") return /* @__PURE__ */ jsx18("p", { className: HINT, children: "Empty callout" });
|
|
1512
1547
|
const tone = str(data.tone) || "info";
|
|
1513
|
-
const
|
|
1514
|
-
return /* @__PURE__ */ jsx18("div", { "data-tone": tone, className: `rounded-md border-l-4 ${
|
|
1548
|
+
const paint = TONE[tone] ?? "border-border bg-muted/50";
|
|
1549
|
+
return /* @__PURE__ */ jsx18("div", { "data-tone": tone, className: `rounded-md border-l-4 ${paint} p-3 text-sm`, children: body });
|
|
1515
1550
|
}
|
|
1516
1551
|
|
|
1517
1552
|
// src/blocks/core.ts
|
|
@@ -1559,7 +1594,7 @@ var coreBlocks = [
|
|
|
1559
1594
|
];
|
|
1560
1595
|
|
|
1561
1596
|
// src/hooks/useBlockList.ts
|
|
1562
|
-
import * as
|
|
1597
|
+
import * as React11 from "react";
|
|
1563
1598
|
|
|
1564
1599
|
// src/hooks/new-block-id.ts
|
|
1565
1600
|
function newBlockId() {
|
|
@@ -1626,8 +1661,8 @@ function reducer(state, action) {
|
|
|
1626
1661
|
}
|
|
1627
1662
|
}
|
|
1628
1663
|
function useBlockList(initial) {
|
|
1629
|
-
const [state, dispatch] =
|
|
1630
|
-
const insertAt =
|
|
1664
|
+
const [state, dispatch] = React11.useReducer(reducer, { blocks: initial, removals: /* @__PURE__ */ new Map() });
|
|
1665
|
+
const insertAt = React11.useCallback(
|
|
1631
1666
|
(index, type, data = {}) => {
|
|
1632
1667
|
const id = newBlockId();
|
|
1633
1668
|
dispatch({ type: "insertAt", index, block: { id, type, data } });
|
|
@@ -1635,25 +1670,25 @@ function useBlockList(initial) {
|
|
|
1635
1670
|
},
|
|
1636
1671
|
[]
|
|
1637
1672
|
);
|
|
1638
|
-
const update =
|
|
1673
|
+
const update = React11.useCallback((id, data) => {
|
|
1639
1674
|
dispatch({ type: "update", id, data });
|
|
1640
1675
|
}, []);
|
|
1641
|
-
const remove =
|
|
1676
|
+
const remove = React11.useCallback((id) => {
|
|
1642
1677
|
removalSeq += 1;
|
|
1643
1678
|
const token = removalSeq;
|
|
1644
1679
|
dispatch({ type: "remove", id, token });
|
|
1645
1680
|
return token;
|
|
1646
1681
|
}, []);
|
|
1647
|
-
const undo =
|
|
1682
|
+
const undo = React11.useCallback((token) => {
|
|
1648
1683
|
dispatch({ type: "undo", token });
|
|
1649
1684
|
}, []);
|
|
1650
|
-
const move =
|
|
1685
|
+
const move = React11.useCallback((index, dir) => {
|
|
1651
1686
|
dispatch({ type: "move", index, dir });
|
|
1652
1687
|
}, []);
|
|
1653
|
-
const reorder =
|
|
1688
|
+
const reorder = React11.useCallback((from, to) => {
|
|
1654
1689
|
dispatch({ type: "reorder", from, to });
|
|
1655
1690
|
}, []);
|
|
1656
|
-
const reset =
|
|
1691
|
+
const reset = React11.useCallback((blocks) => {
|
|
1657
1692
|
dispatch({ type: "reset", blocks });
|
|
1658
1693
|
}, []);
|
|
1659
1694
|
return { blocks: state.blocks, insertAt, update, remove, undo, move, reorder, reset };
|
|
@@ -1735,7 +1770,8 @@ function DropdownMenuItem({
|
|
|
1735
1770
|
"data-slot": "dropdown-menu-item",
|
|
1736
1771
|
"data-inset": inset,
|
|
1737
1772
|
className: cn(
|
|
1738
|
-
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-
|
|
1773
|
+
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4",
|
|
1774
|
+
HIGHLIGHT_BAR,
|
|
1739
1775
|
TAP_TARGET_FLOOR,
|
|
1740
1776
|
className
|
|
1741
1777
|
),
|
|
@@ -1812,7 +1848,7 @@ function BlockActionsMenu({
|
|
|
1812
1848
|
}
|
|
1813
1849
|
|
|
1814
1850
|
// src/canvas/BlockCard.tsx
|
|
1815
|
-
import * as
|
|
1851
|
+
import * as React12 from "react";
|
|
1816
1852
|
import { Pencil } from "lucide-react";
|
|
1817
1853
|
|
|
1818
1854
|
// src/canvas/BlockEditor.tsx
|
|
@@ -1906,32 +1942,36 @@ function BlockCardImpl({
|
|
|
1906
1942
|
const definition = blockByType(blockTypes, block.type);
|
|
1907
1943
|
const Preview = definition?.preview;
|
|
1908
1944
|
const editable = Boolean(definition?.editor) && !readOnly;
|
|
1909
|
-
const [editing, setEditing] =
|
|
1910
|
-
const editButtonRef =
|
|
1911
|
-
const editorContainerRef =
|
|
1912
|
-
const returnFocusOnClose =
|
|
1913
|
-
const clickedRef =
|
|
1914
|
-
const scrollTopRef =
|
|
1915
|
-
const
|
|
1945
|
+
const [editing, setEditing] = React12.useState(editingByDefault && editable);
|
|
1946
|
+
const editButtonRef = React12.useRef(null);
|
|
1947
|
+
const editorContainerRef = React12.useRef(null);
|
|
1948
|
+
const returnFocusOnClose = React12.useRef(false);
|
|
1949
|
+
const clickedRef = React12.useRef(null);
|
|
1950
|
+
const scrollTopRef = React12.useRef(null);
|
|
1951
|
+
const seedRef = React12.useRef(initialSelection);
|
|
1952
|
+
React12.useEffect(() => {
|
|
1953
|
+
seedRef.current = initialSelection;
|
|
1954
|
+
});
|
|
1955
|
+
const handleInsertBlock = React12.useCallback(
|
|
1916
1956
|
(type) => onInsertBlock?.(index + 1, type),
|
|
1917
1957
|
[onInsertBlock, index]
|
|
1918
1958
|
);
|
|
1919
|
-
const leaveEditing =
|
|
1959
|
+
const leaveEditing = React12.useCallback(() => {
|
|
1920
1960
|
returnFocusOnClose.current = true;
|
|
1921
1961
|
setEditing(false);
|
|
1922
1962
|
}, []);
|
|
1923
|
-
const settleRef =
|
|
1963
|
+
const settleRef = React12.useRef(null);
|
|
1924
1964
|
const clearSettle = () => {
|
|
1925
1965
|
if (settleRef.current === null) return;
|
|
1926
1966
|
window.clearTimeout(settleRef.current);
|
|
1927
1967
|
settleRef.current = null;
|
|
1928
1968
|
};
|
|
1929
|
-
|
|
1969
|
+
React12.useEffect(() => clearSettle, []);
|
|
1930
1970
|
function stillInside(node) {
|
|
1931
1971
|
if (!node) return false;
|
|
1932
1972
|
return Boolean(editorContainerRef.current?.contains(node)) || Boolean(node.closest(PORTALLED_OVERLAY));
|
|
1933
1973
|
}
|
|
1934
|
-
|
|
1974
|
+
React12.useEffect(() => {
|
|
1935
1975
|
if (!editing) return;
|
|
1936
1976
|
function onPointerDown(e) {
|
|
1937
1977
|
if (stillInside(e.target)) return;
|
|
@@ -1948,7 +1988,7 @@ function BlockCardImpl({
|
|
|
1948
1988
|
setEditing(false);
|
|
1949
1989
|
}, 0);
|
|
1950
1990
|
}
|
|
1951
|
-
|
|
1991
|
+
React12.useEffect(() => {
|
|
1952
1992
|
if (!editing) return;
|
|
1953
1993
|
const field = editorContainerRef.current?.querySelector(
|
|
1954
1994
|
'textarea, input, [role="combobox"]'
|
|
@@ -1959,7 +1999,7 @@ function BlockCardImpl({
|
|
|
1959
1999
|
if (clicked === null || !(field instanceof HTMLTextAreaElement)) {
|
|
1960
2000
|
field.focus();
|
|
1961
2001
|
if (field instanceof HTMLTextAreaElement) {
|
|
1962
|
-
const at =
|
|
2002
|
+
const at = seedRef.current ?? { start: field.value.length, end: field.value.length };
|
|
1963
2003
|
field.setSelectionRange(at.start, at.end);
|
|
1964
2004
|
}
|
|
1965
2005
|
return;
|
|
@@ -1967,26 +2007,26 @@ function BlockCardImpl({
|
|
|
1967
2007
|
const offset = sourceOffsetFor(field.value, clicked);
|
|
1968
2008
|
field.focus({ preventScroll: true });
|
|
1969
2009
|
field.setSelectionRange(offset, offset);
|
|
1970
|
-
}, [editing
|
|
1971
|
-
|
|
2010
|
+
}, [editing]);
|
|
2011
|
+
useIsomorphicLayoutEffect(() => {
|
|
1972
2012
|
const scrollTop = scrollTopRef.current;
|
|
1973
2013
|
scrollTopRef.current = null;
|
|
1974
2014
|
if (!editing || scrollTop === null) return;
|
|
1975
2015
|
const scroller = document.scrollingElement ?? document.documentElement;
|
|
1976
2016
|
if (scroller.scrollTop !== scrollTop) scroller.scrollTop = scrollTop;
|
|
1977
2017
|
}, [editing]);
|
|
1978
|
-
|
|
2018
|
+
React12.useEffect(() => {
|
|
1979
2019
|
if (editing || !returnFocusOnClose.current) return;
|
|
1980
2020
|
returnFocusOnClose.current = false;
|
|
1981
2021
|
editButtonRef.current?.focus();
|
|
1982
2022
|
}, [editing]);
|
|
1983
|
-
const reportedType =
|
|
1984
|
-
|
|
2023
|
+
const reportedType = React12.useRef(null);
|
|
2024
|
+
React12.useEffect(() => {
|
|
1985
2025
|
if (definition || reportedType.current === block.type) return;
|
|
1986
2026
|
reportedType.current = block.type;
|
|
1987
2027
|
onError(new Error(`No block type is registered for "${block.type}".`));
|
|
1988
2028
|
}, [definition, block.type, onError]);
|
|
1989
|
-
|
|
2029
|
+
React12.useEffect(() => {
|
|
1990
2030
|
if (!readOnly || !definition || definition.preview) return;
|
|
1991
2031
|
devWarn(
|
|
1992
2032
|
`the block type "${block.type}" has no \`preview\`, so a read-only canvas can draw only its label. Give its definition a \`preview\` to render the block itself.`
|
|
@@ -2067,20 +2107,21 @@ function BlockCardImpl({
|
|
|
2067
2107
|
)
|
|
2068
2108
|
] });
|
|
2069
2109
|
}
|
|
2070
|
-
var BlockCard =
|
|
2110
|
+
var BlockCard = React12.memo(BlockCardImpl);
|
|
2071
2111
|
|
|
2072
2112
|
// src/canvas/BlockInserter.tsx
|
|
2073
|
-
import * as
|
|
2113
|
+
import * as React13 from "react";
|
|
2074
2114
|
import { Plus } from "lucide-react";
|
|
2075
2115
|
import { jsx as jsx23, jsxs as jsxs16 } from "react/jsx-runtime";
|
|
2076
2116
|
function BlockInserter({
|
|
2077
2117
|
index,
|
|
2078
2118
|
onInsert,
|
|
2079
|
-
label
|
|
2119
|
+
label,
|
|
2120
|
+
canvasRef
|
|
2080
2121
|
}) {
|
|
2081
2122
|
const { blockTypes } = useQuoin();
|
|
2082
2123
|
const options = paletteBlocks(blockTypes);
|
|
2083
|
-
const insertedAtRef =
|
|
2124
|
+
const insertedAtRef = React13.useRef(null);
|
|
2084
2125
|
function handleSelect(type) {
|
|
2085
2126
|
insertedAtRef.current = index;
|
|
2086
2127
|
onInsert(index, type);
|
|
@@ -2090,7 +2131,7 @@ function BlockInserter({
|
|
|
2090
2131
|
insertedAtRef.current = null;
|
|
2091
2132
|
if (insertedAt === null) return;
|
|
2092
2133
|
event.preventDefault();
|
|
2093
|
-
const card =
|
|
2134
|
+
const card = canvasRef.current?.querySelector(`[data-block-index="${insertedAt}"]`);
|
|
2094
2135
|
if (!card) return;
|
|
2095
2136
|
if (typeof card.scrollIntoView === "function") {
|
|
2096
2137
|
card.scrollIntoView({ block: "center", behavior: "smooth" });
|
|
@@ -2126,21 +2167,22 @@ function BlockInserter({
|
|
|
2126
2167
|
}
|
|
2127
2168
|
|
|
2128
2169
|
// src/canvas/DocumentStarter.tsx
|
|
2129
|
-
import * as
|
|
2170
|
+
import * as React14 from "react";
|
|
2130
2171
|
import { jsx as jsx24, jsxs as jsxs17 } from "react/jsx-runtime";
|
|
2131
2172
|
var LINE = [
|
|
2132
|
-
"w-full resize-none rounded-md border-0 bg-transparent px-2 py-1
|
|
2173
|
+
"w-full resize-none rounded-md border-0 bg-transparent px-2 py-1",
|
|
2133
2174
|
"text-base leading-7 placeholder:text-muted-foreground",
|
|
2134
|
-
"transition-[color,box-shadow]
|
|
2175
|
+
"transition-[color,box-shadow]",
|
|
2176
|
+
FOCUS_RING
|
|
2135
2177
|
].join(" ");
|
|
2136
2178
|
function DocumentStarter({
|
|
2137
2179
|
autoFocus = false,
|
|
2138
2180
|
onStart,
|
|
2139
2181
|
onInsertBlock
|
|
2140
2182
|
}) {
|
|
2141
|
-
const ref =
|
|
2183
|
+
const ref = React14.useRef(null);
|
|
2142
2184
|
const palette = useSlashPalette(ref);
|
|
2143
|
-
const [shortcutsOpen, setShortcutsOpen] =
|
|
2185
|
+
const [shortcutsOpen, setShortcutsOpen] = React14.useState(false);
|
|
2144
2186
|
function handleChange(e) {
|
|
2145
2187
|
const value = e.target.value;
|
|
2146
2188
|
e.target.value = "";
|
|
@@ -2162,6 +2204,7 @@ function DocumentStarter({
|
|
|
2162
2204
|
defaultValue: "",
|
|
2163
2205
|
onChange: handleChange,
|
|
2164
2206
|
onKeyDown: (e) => {
|
|
2207
|
+
if (e.nativeEvent.isComposing || e.keyCode === 229) return;
|
|
2165
2208
|
if (e.key !== "/") return;
|
|
2166
2209
|
e.preventDefault();
|
|
2167
2210
|
palette.openAtCaret();
|
|
@@ -2288,14 +2331,36 @@ function SortableBlock({
|
|
|
2288
2331
|
}
|
|
2289
2332
|
|
|
2290
2333
|
// src/canvas/unacknowledged.ts
|
|
2291
|
-
var
|
|
2334
|
+
var STRUCTURAL_ECHO_LIMIT = 64;
|
|
2335
|
+
function createUnacknowledged() {
|
|
2336
|
+
return { sequences: /* @__PURE__ */ new WeakMap(), emitted: 0, acknowledged: 0, newest: [] };
|
|
2337
|
+
}
|
|
2338
|
+
function keepNewest(outstanding) {
|
|
2339
|
+
const kept = Math.min(STRUCTURAL_ECHO_LIMIT, outstanding.emitted - outstanding.acknowledged);
|
|
2340
|
+
outstanding.newest.splice(0, outstanding.newest.length - kept);
|
|
2341
|
+
}
|
|
2292
2342
|
function remember(outstanding, blocks) {
|
|
2293
|
-
outstanding.
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2343
|
+
outstanding.emitted += 1;
|
|
2344
|
+
outstanding.sequences.set(blocks, outstanding.emitted);
|
|
2345
|
+
outstanding.newest.push(blocks);
|
|
2346
|
+
keepNewest(outstanding);
|
|
2347
|
+
}
|
|
2348
|
+
function copiedSequence(outstanding, blocks) {
|
|
2349
|
+
for (const emitted of outstanding.newest) {
|
|
2350
|
+
if (sameBlocks(emitted, blocks)) return outstanding.sequences.get(emitted);
|
|
2298
2351
|
}
|
|
2352
|
+
return void 0;
|
|
2353
|
+
}
|
|
2354
|
+
function acknowledge(outstanding, blocks) {
|
|
2355
|
+
const echoed = outstanding.sequences.get(blocks) ?? copiedSequence(outstanding, blocks);
|
|
2356
|
+
if (echoed === void 0 || echoed <= outstanding.acknowledged) return false;
|
|
2357
|
+
outstanding.acknowledged = echoed;
|
|
2358
|
+
keepNewest(outstanding);
|
|
2359
|
+
return true;
|
|
2360
|
+
}
|
|
2361
|
+
function forgetAll(outstanding) {
|
|
2362
|
+
outstanding.acknowledged = outstanding.emitted;
|
|
2363
|
+
keepNewest(outstanding);
|
|
2299
2364
|
}
|
|
2300
2365
|
|
|
2301
2366
|
// src/canvas/BlockCanvas.tsx
|
|
@@ -2316,20 +2381,20 @@ function Canvas({
|
|
|
2316
2381
|
useSensor(PointerSensor, { activationConstraint: { distance: 4 } }),
|
|
2317
2382
|
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates })
|
|
2318
2383
|
);
|
|
2319
|
-
const dndId =
|
|
2320
|
-
const [newBlockId2, setNewBlockId] =
|
|
2321
|
-
const [seedSelection, setSeedSelection] =
|
|
2322
|
-
const listRef =
|
|
2323
|
-
const pendingDeleteIndexRef =
|
|
2324
|
-
const removedRef =
|
|
2325
|
-
const announcements =
|
|
2384
|
+
const dndId = React15.useId();
|
|
2385
|
+
const [newBlockId2, setNewBlockId] = React15.useState(null);
|
|
2386
|
+
const [seedSelection, setSeedSelection] = React15.useState(null);
|
|
2387
|
+
const listRef = React15.useRef(null);
|
|
2388
|
+
const pendingDeleteIndexRef = React15.useRef(null);
|
|
2389
|
+
const removedRef = React15.useRef(null);
|
|
2390
|
+
const announcements = React15.useMemo(
|
|
2326
2391
|
() => buildAnnouncements(list.blocks, blockTypes),
|
|
2327
2392
|
[list.blocks, blockTypes]
|
|
2328
2393
|
);
|
|
2329
|
-
const lastEmitted =
|
|
2330
|
-
const lastSupplied =
|
|
2331
|
-
const unacknowledged =
|
|
2332
|
-
|
|
2394
|
+
const lastEmitted = React15.useRef(blocks);
|
|
2395
|
+
const lastSupplied = React15.useRef(blocks);
|
|
2396
|
+
const unacknowledged = React15.useRef(createUnacknowledged());
|
|
2397
|
+
React15.useEffect(() => {
|
|
2333
2398
|
if (list.blocks === lastEmitted.current) return;
|
|
2334
2399
|
lastEmitted.current = list.blocks;
|
|
2335
2400
|
remember(unacknowledged.current, list.blocks);
|
|
@@ -2338,21 +2403,22 @@ function Canvas({
|
|
|
2338
2403
|
removedRef.current = null;
|
|
2339
2404
|
if (removed) onDelete?.(removed.block, removed.index);
|
|
2340
2405
|
}, [list.blocks, onChange, onDelete]);
|
|
2341
|
-
|
|
2406
|
+
React15.useEffect(() => {
|
|
2342
2407
|
const previous = lastSupplied.current;
|
|
2343
2408
|
lastSupplied.current = blocks;
|
|
2344
|
-
if (unacknowledged.current
|
|
2345
|
-
if (blocks === lastEmitted.current) unacknowledged.current.clear();
|
|
2346
|
-
return;
|
|
2347
|
-
}
|
|
2409
|
+
if (acknowledge(unacknowledged.current, blocks)) return;
|
|
2348
2410
|
if (sameBlocks(blocks, previous) || sameBlocks(blocks, list.blocks)) return;
|
|
2349
|
-
unacknowledged.current
|
|
2411
|
+
forgetAll(unacknowledged.current);
|
|
2350
2412
|
lastEmitted.current = blocks;
|
|
2351
2413
|
reset(blocks);
|
|
2352
2414
|
}, [blocks, reset]);
|
|
2353
|
-
|
|
2415
|
+
React15.useEffect(() => {
|
|
2416
|
+
if (newBlockId2 === null) return;
|
|
2417
|
+
setNewBlockId(null);
|
|
2418
|
+
setSeedSelection(null);
|
|
2419
|
+
}, [newBlockId2]);
|
|
2420
|
+
const insertPlain = React15.useCallback(
|
|
2354
2421
|
(index, type, data) => {
|
|
2355
|
-
setSeedSelection(null);
|
|
2356
2422
|
const seed = data ?? blockByType(blockTypes, type)?.initialData?.() ?? {};
|
|
2357
2423
|
setNewBlockId(insertAt(index, type, seed));
|
|
2358
2424
|
},
|
|
@@ -2362,10 +2428,9 @@ function Canvas({
|
|
|
2362
2428
|
const block = list.blocks[index];
|
|
2363
2429
|
pendingDeleteIndexRef.current = index;
|
|
2364
2430
|
if (block) removedRef.current = { block, index };
|
|
2365
|
-
setNewBlockId((current) => current === id2 ? null : current);
|
|
2366
2431
|
remove(id2);
|
|
2367
2432
|
}
|
|
2368
|
-
|
|
2433
|
+
React15.useEffect(() => {
|
|
2369
2434
|
const index = pendingDeleteIndexRef.current;
|
|
2370
2435
|
pendingDeleteIndexRef.current = null;
|
|
2371
2436
|
if (index === null || !listRef.current) return;
|
|
@@ -2391,7 +2456,7 @@ function Canvas({
|
|
|
2391
2456
|
};
|
|
2392
2457
|
if (readOnly) return /* @__PURE__ */ jsx27(ReadOnlyCanvas, { blocks: list.blocks, onUpdate: update, root: rootProps });
|
|
2393
2458
|
return /* @__PURE__ */ jsxs19("div", { ...rootProps, children: [
|
|
2394
|
-
/* @__PURE__ */ jsx27(BlockInserter, { index: 0, onInsert: insertPlain, label: "Insert a block at the start" }),
|
|
2459
|
+
/* @__PURE__ */ jsx27(BlockInserter, { index: 0, onInsert: insertPlain, label: "Insert a block at the start", canvasRef: listRef }),
|
|
2395
2460
|
/* @__PURE__ */ jsx27(
|
|
2396
2461
|
DndContext,
|
|
2397
2462
|
{
|
|
@@ -2435,7 +2500,8 @@ function Canvas({
|
|
|
2435
2500
|
{
|
|
2436
2501
|
index: i + 1,
|
|
2437
2502
|
onInsert: insertPlain,
|
|
2438
|
-
label: `Insert a block after block ${i + 1}
|
|
2503
|
+
label: `Insert a block after block ${i + 1}`,
|
|
2504
|
+
canvasRef: listRef
|
|
2439
2505
|
}
|
|
2440
2506
|
)
|
|
2441
2507
|
] }, b.id)) }) })
|
|
@@ -2467,7 +2533,7 @@ function BlockCanvas({
|
|
|
2467
2533
|
id,
|
|
2468
2534
|
"aria-label": ariaLabel
|
|
2469
2535
|
}) {
|
|
2470
|
-
const config =
|
|
2536
|
+
const config = React15.useMemo(
|
|
2471
2537
|
() => ({ blockTypes, upload, resolveAssetUrl, onError }),
|
|
2472
2538
|
[blockTypes, upload, resolveAssetUrl, onError]
|
|
2473
2539
|
);
|
|
@@ -2487,71 +2553,134 @@ function BlockCanvas({
|
|
|
2487
2553
|
}
|
|
2488
2554
|
|
|
2489
2555
|
// src/hooks/useAutosave.ts
|
|
2490
|
-
import * as
|
|
2556
|
+
import * as React16 from "react";
|
|
2557
|
+
|
|
2558
|
+
// src/hooks/autosave-backoff.ts
|
|
2559
|
+
function clearTimer(ref) {
|
|
2560
|
+
if (ref.current !== null) clearTimeout(ref.current);
|
|
2561
|
+
ref.current = null;
|
|
2562
|
+
}
|
|
2563
|
+
var alwaysRetry = () => true;
|
|
2491
2564
|
function reportedFailure(result) {
|
|
2492
2565
|
return typeof result === "object" && result !== null && "ok" in result && result.ok === false;
|
|
2493
2566
|
}
|
|
2567
|
+
async function attemptSave(run) {
|
|
2568
|
+
try {
|
|
2569
|
+
const result = await run();
|
|
2570
|
+
return reportedFailure(result) ? { error: result } : null;
|
|
2571
|
+
} catch (error) {
|
|
2572
|
+
return { error };
|
|
2573
|
+
}
|
|
2574
|
+
}
|
|
2575
|
+
function nextRetryDelay(error, retriesSoFar, { debounceMs, maxBackoffMs, maxRetries, isRetryable }) {
|
|
2576
|
+
if (retriesSoFar >= maxRetries || !isRetryable(error)) return null;
|
|
2577
|
+
return Math.min(debounceMs * 2 ** (retriesSoFar + 1), maxBackoffMs);
|
|
2578
|
+
}
|
|
2579
|
+
|
|
2580
|
+
// src/hooks/useAutosave.ts
|
|
2494
2581
|
function useAutosave({
|
|
2495
2582
|
deps,
|
|
2496
2583
|
build,
|
|
2497
2584
|
save,
|
|
2498
2585
|
debounceMs = 2e3,
|
|
2499
|
-
maxBackoffMs = 3e4
|
|
2586
|
+
maxBackoffMs = 3e4,
|
|
2587
|
+
maxRetries = 8,
|
|
2588
|
+
isRetryable = alwaysRetry,
|
|
2589
|
+
flushOnHide = true
|
|
2500
2590
|
}) {
|
|
2501
|
-
const [status, setStatus] =
|
|
2502
|
-
const
|
|
2503
|
-
const
|
|
2504
|
-
const
|
|
2505
|
-
const
|
|
2506
|
-
const
|
|
2507
|
-
const
|
|
2591
|
+
const [status, setStatus] = React16.useState("idle");
|
|
2592
|
+
const [error, setError] = React16.useState(null);
|
|
2593
|
+
const dirtyRef = React16.useRef(false);
|
|
2594
|
+
const inFlightRef = React16.useRef(false);
|
|
2595
|
+
const disposedRef = React16.useRef(false);
|
|
2596
|
+
const retriesRef = React16.useRef(0);
|
|
2597
|
+
const debounceRef = React16.useRef(null);
|
|
2598
|
+
const backoffRef = React16.useRef(null);
|
|
2599
|
+
const buildRef = React16.useRef(build);
|
|
2600
|
+
const saveRef = React16.useRef(save);
|
|
2601
|
+
const runRef = React16.useRef(() => {
|
|
2508
2602
|
});
|
|
2509
|
-
|
|
2603
|
+
React16.useEffect(() => {
|
|
2510
2604
|
buildRef.current = build;
|
|
2511
2605
|
saveRef.current = save;
|
|
2606
|
+
const policy = { debounceMs, maxBackoffMs, maxRetries, isRetryable };
|
|
2512
2607
|
runRef.current = () => {
|
|
2608
|
+
clearTimer(debounceRef);
|
|
2609
|
+
clearTimer(backoffRef);
|
|
2610
|
+
if (inFlightRef.current) return;
|
|
2611
|
+
inFlightRef.current = true;
|
|
2612
|
+
dirtyRef.current = false;
|
|
2613
|
+
if (!disposedRef.current) setStatus("saving");
|
|
2513
2614
|
void (async () => {
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
if (
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2615
|
+
const failure = await attemptSave(() => saveRef.current(buildRef.current()));
|
|
2616
|
+
inFlightRef.current = false;
|
|
2617
|
+
if (disposedRef.current) {
|
|
2618
|
+
if (dirtyRef.current) runRef.current();
|
|
2619
|
+
return;
|
|
2620
|
+
}
|
|
2621
|
+
if (!failure) {
|
|
2622
|
+
retriesRef.current = 0;
|
|
2623
|
+
setError(null);
|
|
2624
|
+
if (dirtyRef.current) runRef.current();
|
|
2625
|
+
else setStatus("saved");
|
|
2626
|
+
return;
|
|
2627
|
+
}
|
|
2628
|
+
dirtyRef.current = true;
|
|
2629
|
+
setError(failure.error);
|
|
2630
|
+
const delay = nextRetryDelay(failure.error, retriesRef.current, policy);
|
|
2631
|
+
if (delay === null) {
|
|
2632
|
+
setStatus("failed");
|
|
2633
|
+
return;
|
|
2527
2634
|
}
|
|
2635
|
+
retriesRef.current += 1;
|
|
2636
|
+
setStatus("retrying");
|
|
2637
|
+
backoffRef.current = setTimeout(() => runRef.current(), delay);
|
|
2528
2638
|
})();
|
|
2529
2639
|
};
|
|
2530
2640
|
});
|
|
2531
|
-
const seenRef =
|
|
2532
|
-
|
|
2641
|
+
const seenRef = React16.useRef(null);
|
|
2642
|
+
React16.useEffect(() => {
|
|
2533
2643
|
const seen2 = seenRef.current;
|
|
2534
2644
|
seenRef.current = deps;
|
|
2535
2645
|
if (seen2 === null) return;
|
|
2536
2646
|
const unchanged = seen2.length === deps.length && seen2.every((v, i) => v === deps[i]);
|
|
2537
2647
|
if (!unchanged) dirtyRef.current = true;
|
|
2538
2648
|
}, deps);
|
|
2539
|
-
|
|
2649
|
+
React16.useEffect(() => {
|
|
2540
2650
|
if (!dirtyRef.current) return;
|
|
2541
|
-
|
|
2542
|
-
|
|
2651
|
+
clearTimer(debounceRef);
|
|
2652
|
+
debounceRef.current = setTimeout(() => runRef.current(), debounceMs);
|
|
2653
|
+
return () => clearTimer(debounceRef);
|
|
2543
2654
|
}, deps);
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2655
|
+
React16.useEffect(() => {
|
|
2656
|
+
disposedRef.current = false;
|
|
2657
|
+
return () => {
|
|
2658
|
+
disposedRef.current = true;
|
|
2659
|
+
clearTimer(debounceRef);
|
|
2660
|
+
clearTimer(backoffRef);
|
|
2661
|
+
if (dirtyRef.current) runRef.current();
|
|
2662
|
+
};
|
|
2663
|
+
}, []);
|
|
2664
|
+
const retry = React16.useCallback(() => {
|
|
2665
|
+
retriesRef.current = 0;
|
|
2552
2666
|
runRef.current();
|
|
2553
2667
|
}, []);
|
|
2554
|
-
|
|
2668
|
+
const flush = React16.useCallback(() => {
|
|
2669
|
+
if (dirtyRef.current) retry();
|
|
2670
|
+
}, [retry]);
|
|
2671
|
+
React16.useEffect(() => {
|
|
2672
|
+
if (!flushOnHide) return;
|
|
2673
|
+
const onVisibility = () => {
|
|
2674
|
+
if (document.visibilityState === "hidden") flush();
|
|
2675
|
+
};
|
|
2676
|
+
window.addEventListener("pagehide", flush);
|
|
2677
|
+
document.addEventListener("visibilitychange", onVisibility);
|
|
2678
|
+
return () => {
|
|
2679
|
+
window.removeEventListener("pagehide", flush);
|
|
2680
|
+
document.removeEventListener("visibilitychange", onVisibility);
|
|
2681
|
+
};
|
|
2682
|
+
}, [flushOnHide, flush]);
|
|
2683
|
+
return { status, error, retry, flush };
|
|
2555
2684
|
}
|
|
2556
2685
|
export {
|
|
2557
2686
|
BlockCanvas,
|