quoin-editor 0.4.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/dist/index.cjs CHANGED
@@ -60,7 +60,7 @@ __export(src_exports, {
60
60
  module.exports = __toCommonJS(src_exports);
61
61
 
62
62
  // src/canvas/BlockCanvas.tsx
63
- var React11 = __toESM(require("react"), 1);
63
+ var React15 = __toESM(require("react"), 1);
64
64
  var import_core = require("@dnd-kit/core");
65
65
  var import_modifiers = require("@dnd-kit/modifiers");
66
66
  var import_sortable2 = require("@dnd-kit/sortable");
@@ -120,12 +120,13 @@ function paletteBlocks(definitions) {
120
120
  var import_lucide_react7 = require("lucide-react");
121
121
 
122
122
  // src/blocks/MarkdownBlockEditor.tsx
123
- var React5 = __toESM(require("react"), 1);
123
+ var React7 = __toESM(require("react"), 1);
124
124
 
125
125
  // src/upload/FilePickerButton.tsx
126
126
  var import_lucide_react = require("lucide-react");
127
127
 
128
128
  // src/ui/button.tsx
129
+ var React2 = __toESM(require("react"), 1);
129
130
  var import_react_slot = require("@radix-ui/react-slot");
130
131
  var import_class_variance_authority = require("class-variance-authority");
131
132
 
@@ -136,6 +137,15 @@ function cn(...inputs) {
136
137
  return (0, import_tailwind_merge.twMerge)((0, import_clsx.clsx)(inputs));
137
138
  }
138
139
 
140
+ // src/ui/focus.ts
141
+ var FOCUS_RING = [
142
+ "outline-hidden",
143
+ "focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-solid",
144
+ "focus-visible:outline-[var(--quoin-ring)]"
145
+ ].join(" ");
146
+ var HIGHLIGHT_BAR = "data-[highlighted]:shadow-[inset_2px_0_0_var(--quoin-foreground)]";
147
+ var SELECTED_BAR = "data-[selected=true]:shadow-[inset_2px_0_0_var(--quoin-foreground)]";
148
+
139
149
  // src/ui/touch-target.ts
140
150
  var TAP_TARGET_FLOOR = "pointer-coarse:min-h-11";
141
151
  var SQUARE_TAP_TARGET_FLOOR = `${TAP_TARGET_FLOOR} pointer-coarse:min-w-11`;
@@ -148,7 +158,10 @@ var ICON_TAP_TARGET_FLOOR = [
148
158
  // src/ui/button.tsx
149
159
  var import_jsx_runtime2 = require("react/jsx-runtime");
150
160
  var buttonVariants = (0, import_class_variance_authority.cva)(
151
- "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 outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50",
161
+ [
162
+ "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",
163
+ FOCUS_RING
164
+ ],
152
165
  {
153
166
  variants: {
154
167
  variant: {
@@ -173,17 +186,12 @@ var buttonVariants = (0, import_class_variance_authority.cva)(
173
186
  defaultVariants: { variant: "default", size: "default" }
174
187
  }
175
188
  );
176
- function Button({
177
- className,
178
- variant = "default",
179
- size = "default",
180
- asChild = false,
181
- ...props
182
- }) {
189
+ var Button = React2.forwardRef(function Button2({ className, variant = "default", size = "default", asChild = false, ...props }, ref) {
183
190
  const Comp = asChild ? import_react_slot.Slot : "button";
184
191
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
185
192
  Comp,
186
193
  {
194
+ ref,
187
195
  "data-slot": "button",
188
196
  "data-variant": variant,
189
197
  "data-size": size,
@@ -191,12 +199,12 @@ function Button({
191
199
  ...props
192
200
  }
193
201
  );
194
- }
202
+ });
195
203
 
196
204
  // src/upload/FilePickerButton.tsx
197
205
  var import_jsx_runtime3 = require("react/jsx-runtime");
198
206
  function FilePickerButton({ label, accept, disabled, onPick }) {
199
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "relative inline-flex rounded-md has-[:focus-visible]:ring-[3px] has-[:focus-visible]:ring-ring/50", children: [
207
+ return /* @__PURE__ */ (0, import_jsx_runtime3.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: [
200
208
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Button, { asChild: true, variant: "outline", size: "sm", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { "aria-hidden": "true", className: disabled ? "pointer-events-none opacity-50" : void 0, children: [
201
209
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_lucide_react.Upload, { "aria-hidden": "true" }),
202
210
  label
@@ -287,15 +295,18 @@ function insertCommand(command, value, caret) {
287
295
  };
288
296
  }
289
297
 
298
+ // src/markdown/platform.ts
299
+ function isMac() {
300
+ if (typeof navigator === "undefined") return false;
301
+ const { userAgentData, platform } = navigator;
302
+ return /mac|iphone|ipad/i.test(userAgentData?.platform || platform || "");
303
+ }
304
+
290
305
  // src/markdown/shortcut-label.ts
291
306
  var MAC_LABEL = { mod: "\u2318", alt: "\u2325", shift: "\u21E7" };
292
307
  var OTHER_LABEL = { mod: "Ctrl", alt: "Alt", shift: "Shift" };
293
- function onMac() {
294
- if (typeof navigator === "undefined") return false;
295
- return /Mac|iPhone|iPad/.test(navigator.userAgent);
296
- }
297
308
  function formatShortcut(hint) {
298
- const mac = onMac();
309
+ const mac = isMac();
299
310
  const labels = mac ? MAC_LABEL : OTHER_LABEL;
300
311
  const parts = hint.split("+").map((part) => labels[part] ?? part.toUpperCase());
301
312
  return parts.join(mac ? "" : "+");
@@ -349,7 +360,8 @@ function DialogContent({
349
360
  DialogPrimitive.Close,
350
361
  {
351
362
  className: cn(
352
- "ring-offset-background focus:ring-ring absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-none",
363
+ "absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100",
364
+ FOCUS_RING,
353
365
  ICON_TAP_TARGET_FLOOR
354
366
  ),
355
367
  children: [
@@ -638,6 +650,7 @@ function CommandItem({
638
650
  "data-slot": "command-item",
639
651
  className: cn(
640
652
  "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",
653
+ SELECTED_BAR,
641
654
  className
642
655
  ),
643
656
  ...props
@@ -754,7 +767,7 @@ function SlashPalette({
754
767
  }
755
768
 
756
769
  // src/markdown/useMarkdownKeymap.ts
757
- var React2 = __toESM(require("react"), 1);
770
+ var React3 = __toESM(require("react"), 1);
758
771
 
759
772
  // src/markdown/list-continuation.ts
760
773
  var LIST_MARKER = /^(\s*)((?:[-*+] \[[ xX]\] )|(?:[-*+] )|(?:\d+[.)] )|(?:> ))(.*)$/;
@@ -978,11 +991,12 @@ function useMarkdownKeymap({
978
991
  commit,
979
992
  onOpenPalette
980
993
  }) {
981
- const onKeyDown = React2.useCallback(
994
+ const onKeyDown = React3.useCallback(
982
995
  (event) => {
996
+ if (event.nativeEvent.isComposing || event.keyCode === 229) return;
983
997
  const el = event.currentTarget;
984
998
  const { value, selectionStart, selectionEnd } = el;
985
- const mod = event.metaKey || event.ctrlKey;
999
+ const mod = isMac() ? event.metaKey : event.ctrlKey;
986
1000
  const run = (edit) => {
987
1001
  if (!edit) return;
988
1002
  event.preventDefault();
@@ -1023,7 +1037,7 @@ function useMarkdownKeymap({
1023
1037
  },
1024
1038
  [commit, onOpenPalette]
1025
1039
  );
1026
- const onPaste = React2.useCallback(
1040
+ const onPaste = React3.useCallback(
1027
1041
  (event) => {
1028
1042
  const el = event.currentTarget;
1029
1043
  const edit = linkPaste(
@@ -1042,7 +1056,7 @@ function useMarkdownKeymap({
1042
1056
  }
1043
1057
 
1044
1058
  // src/markdown/useSlashPalette.ts
1045
- var React3 = __toESM(require("react"), 1);
1059
+ var React4 = __toESM(require("react"), 1);
1046
1060
 
1047
1061
  // src/markdown/caret-coordinates.ts
1048
1062
  var COPIED = [
@@ -1093,10 +1107,10 @@ function caretRect(el) {
1093
1107
 
1094
1108
  // src/markdown/useSlashPalette.ts
1095
1109
  function useSlashPalette(ref) {
1096
- const [open, setOpen] = React3.useState(false);
1097
- const [caret, setCaret] = React3.useState(null);
1098
- const offsetRef = React3.useRef(0);
1099
- const openAtCaret = React3.useCallback(() => {
1110
+ const [open, setOpen] = React4.useState(false);
1111
+ const [caret, setCaret] = React4.useState(null);
1112
+ const offsetRef = React4.useRef(0);
1113
+ const openAtCaret = React4.useCallback(() => {
1100
1114
  const el = ref.current;
1101
1115
  if (!el) return;
1102
1116
  offsetRef.current = el.selectionStart;
@@ -1107,11 +1121,17 @@ function useSlashPalette(ref) {
1107
1121
  }
1108
1122
 
1109
1123
  // src/hooks/useAutoGrow.ts
1110
- var React4 = __toESM(require("react"), 1);
1124
+ var React6 = __toESM(require("react"), 1);
1125
+
1126
+ // src/hooks/use-isomorphic-layout-effect.ts
1127
+ var React5 = __toESM(require("react"), 1);
1128
+ var useIsomorphicLayoutEffect = typeof document === "undefined" ? React5.useEffect : React5.useLayoutEffect;
1129
+
1130
+ // src/hooks/useAutoGrow.ts
1111
1131
  function useAutoGrow(value) {
1112
- const ref = React4.useRef(null);
1113
- const lastWidth = React4.useRef(null);
1114
- const fit = React4.useCallback(() => {
1132
+ const ref = React6.useRef(null);
1133
+ const lastWidth = React6.useRef(null);
1134
+ const fit = React6.useCallback(() => {
1115
1135
  const el = ref.current;
1116
1136
  if (!el) return;
1117
1137
  const scroller = document.scrollingElement ?? document.documentElement;
@@ -1120,8 +1140,8 @@ function useAutoGrow(value) {
1120
1140
  if (el.scrollHeight > 0) el.style.height = `${el.scrollHeight}px`;
1121
1141
  if (scroller.scrollTop !== scrollTop) scroller.scrollTop = scrollTop;
1122
1142
  }, []);
1123
- React4.useLayoutEffect(fit, [fit, value]);
1124
- React4.useEffect(() => {
1143
+ useIsomorphicLayoutEffect(fit, [fit, value]);
1144
+ React6.useEffect(() => {
1125
1145
  const el = ref.current;
1126
1146
  if (!el || typeof ResizeObserver === "undefined") return;
1127
1147
  const observer = new ResizeObserver(([entry]) => {
@@ -1143,17 +1163,18 @@ var str = (v) => typeof v === "string" ? v : "";
1143
1163
  var import_jsx_runtime9 = require("react/jsx-runtime");
1144
1164
  var BOX = [
1145
1165
  "-mx-2 min-h-7 w-[calc(100%+1rem)] resize-none rounded-md border-0 bg-transparent px-2 py-0",
1146
- "text-base leading-7 outline-none placeholder:text-muted-foreground",
1147
- "transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:ring-ring/50"
1166
+ "text-base leading-7 placeholder:text-muted-foreground",
1167
+ "transition-[color,box-shadow]",
1168
+ FOCUS_RING
1148
1169
  ].join(" ");
1149
1170
  function MarkdownBlockEditor({ data, onChange, onInsertBlock, onDone }) {
1150
1171
  const { onError } = useQuoin();
1151
1172
  const markdown = str(data.markdown);
1152
1173
  const ref = useAutoGrow(markdown);
1153
1174
  const palette = useSlashPalette(ref);
1154
- const [shortcutsOpen, setShortcutsOpen] = React5.useState(false);
1175
+ const [shortcutsOpen, setShortcutsOpen] = React7.useState(false);
1155
1176
  const set = (patch) => onChange({ ...data, ...patch });
1156
- const commit = React5.useCallback(
1177
+ const commit = React7.useCallback(
1157
1178
  (next) => onChange({ ...data, markdown: next }),
1158
1179
  [data, onChange]
1159
1180
  );
@@ -1219,6 +1240,7 @@ function MarkdownBlockEditor({ data, onChange, onInsertBlock, onDone }) {
1219
1240
  onBlockCommand: (type) => {
1220
1241
  palette.setOpen(false);
1221
1242
  onInsertBlock?.(type);
1243
+ onDone?.();
1222
1244
  },
1223
1245
  onShortcuts: () => {
1224
1246
  palette.setOpen(false);
@@ -1255,7 +1277,8 @@ function SelectTrigger({ className, children, ...props }) {
1255
1277
  {
1256
1278
  "data-slot": "select-trigger",
1257
1279
  className: cn(
1258
- "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 outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
1280
+ "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",
1281
+ FOCUS_RING,
1259
1282
  TAP_TARGET_FLOOR,
1260
1283
  className
1261
1284
  ),
@@ -1294,6 +1317,7 @@ function SelectItem({ className, children, ...props }) {
1294
1317
  "data-slot": "select-item",
1295
1318
  className: cn(
1296
1319
  "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",
1320
+ HIGHLIGHT_BAR,
1297
1321
  TAP_TARGET_FLOOR,
1298
1322
  className
1299
1323
  ),
@@ -1307,22 +1331,25 @@ function SelectItem({ className, children, ...props }) {
1307
1331
  }
1308
1332
 
1309
1333
  // src/ui/textarea.tsx
1334
+ var React8 = __toESM(require("react"), 1);
1310
1335
  var import_jsx_runtime11 = require("react/jsx-runtime");
1311
- function Textarea({ className, ...props }) {
1336
+ var Textarea = React8.forwardRef(function Textarea2({ className, ...props }, ref) {
1312
1337
  return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1313
1338
  "textarea",
1314
1339
  {
1340
+ ref,
1315
1341
  "data-slot": "textarea",
1316
1342
  className: cn(
1317
- "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] outline-none disabled:pointer-events-none disabled:opacity-50 md:text-sm",
1318
- "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
1319
- "aria-invalid:ring-destructive/20 aria-invalid:border-destructive",
1343
+ "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",
1344
+ FOCUS_RING,
1345
+ "focus-visible:border-ring",
1346
+ "aria-invalid:border-destructive",
1320
1347
  className
1321
1348
  ),
1322
1349
  ...props
1323
1350
  }
1324
1351
  );
1325
- }
1352
+ });
1326
1353
 
1327
1354
  // src/blocks/CalloutBlockEditor.tsx
1328
1355
  var import_jsx_runtime12 = require("react/jsx-runtime");
@@ -1345,24 +1372,27 @@ function CalloutBlockEditor({ data, onChange }) {
1345
1372
  }
1346
1373
 
1347
1374
  // src/ui/input.tsx
1375
+ var React9 = __toESM(require("react"), 1);
1348
1376
  var import_jsx_runtime13 = require("react/jsx-runtime");
1349
- function Input({ className, type, ...props }) {
1377
+ var Input = React9.forwardRef(function Input2({ className, type, ...props }, ref) {
1350
1378
  return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
1351
1379
  "input",
1352
1380
  {
1381
+ ref,
1353
1382
  type,
1354
1383
  "data-slot": "input",
1355
1384
  className: cn(
1356
- "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] outline-none 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",
1357
- "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
1358
- "aria-invalid:ring-destructive/20 aria-invalid:border-destructive",
1385
+ "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",
1386
+ FOCUS_RING,
1387
+ "focus-visible:border-ring",
1388
+ "aria-invalid:border-destructive",
1359
1389
  TAP_TARGET_FLOOR,
1360
1390
  className
1361
1391
  ),
1362
1392
  ...props
1363
1393
  }
1364
1394
  );
1365
- }
1395
+ });
1366
1396
 
1367
1397
  // src/blocks/CodeBlockEditor.tsx
1368
1398
  var import_jsx_runtime14 = require("react/jsx-runtime");
@@ -1400,12 +1430,12 @@ function CodeBlockEditor({ data, onChange }) {
1400
1430
  var import_react = require("react");
1401
1431
 
1402
1432
  // src/upload/UploadField.tsx
1403
- var React6 = __toESM(require("react"), 1);
1433
+ var React10 = __toESM(require("react"), 1);
1404
1434
  var import_jsx_runtime15 = require("react/jsx-runtime");
1405
1435
  function UploadField({ category, label, accept, onUploaded }) {
1406
1436
  const { upload, onError } = useQuoin();
1407
- const [busy, setBusy] = React6.useState(false);
1408
- const [error, setError] = React6.useState(null);
1437
+ const [busy, setBusy] = React10.useState(false);
1438
+ const [error, setError] = React10.useState(null);
1409
1439
  async function run(file) {
1410
1440
  if (!upload) return;
1411
1441
  setBusy(true);
@@ -1418,11 +1448,12 @@ function UploadField({ category, label, accept, onUploaded }) {
1418
1448
  setBusy(false);
1419
1449
  return;
1420
1450
  }
1421
- setBusy(false);
1422
1451
  try {
1423
- onUploaded(result);
1452
+ await onUploaded(result);
1424
1453
  } catch (cause) {
1425
1454
  onError(cause instanceof Error ? cause : new Error(String(cause)));
1455
+ } finally {
1456
+ setBusy(false);
1426
1457
  }
1427
1458
  }
1428
1459
  return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "flex flex-col items-start gap-1.5", children: [
@@ -1461,10 +1492,14 @@ function ImageBlockEditor({ data, onChange }) {
1461
1492
  latest.current = data;
1462
1493
  });
1463
1494
  const set = (patch) => onChange({ ...latest.current, ...patch });
1464
- const onUploaded = async (r) => {
1465
- const src = resolveAssetUrl(r.storageKey);
1495
+ const newestKey = (0, import_react.useRef)("");
1496
+ const onUploaded = async ({ storageKey }) => {
1497
+ newestKey.current = storageKey;
1498
+ const { width: _width, height: _height, ...unsized } = latest.current;
1499
+ onChange({ ...unsized, storageKey });
1500
+ const src = resolveAssetUrl(storageKey);
1466
1501
  const size = src ? await intrinsicSize(src) : null;
1467
- set({ storageKey: r.storageKey, ...size });
1502
+ if (size && newestKey.current === storageKey) set({ storageKey, ...size });
1468
1503
  };
1469
1504
  return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "flex flex-col gap-2", children: [
1470
1505
  /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(UploadField, { category: "image", label: "Upload image", onUploaded }),
@@ -1492,7 +1527,7 @@ function ImageBlockEditor({ data, onChange }) {
1492
1527
  }
1493
1528
  )
1494
1529
  ] }),
1495
- str(data.storageKey) && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "text-xs text-muted-foreground", children: "Image attached \u2713" })
1530
+ str(data.storageKey) && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "text-xs text-muted-foreground", children: "Image attached" })
1496
1531
  ] });
1497
1532
  }
1498
1533
 
@@ -1600,7 +1635,7 @@ var coreBlocks = [
1600
1635
  ];
1601
1636
 
1602
1637
  // src/hooks/useBlockList.ts
1603
- var React7 = __toESM(require("react"), 1);
1638
+ var React11 = __toESM(require("react"), 1);
1604
1639
 
1605
1640
  // src/hooks/new-block-id.ts
1606
1641
  function newBlockId() {
@@ -1667,8 +1702,8 @@ function reducer(state, action) {
1667
1702
  }
1668
1703
  }
1669
1704
  function useBlockList(initial) {
1670
- const [state, dispatch] = React7.useReducer(reducer, { blocks: initial, removals: /* @__PURE__ */ new Map() });
1671
- const insertAt = React7.useCallback(
1705
+ const [state, dispatch] = React11.useReducer(reducer, { blocks: initial, removals: /* @__PURE__ */ new Map() });
1706
+ const insertAt = React11.useCallback(
1672
1707
  (index, type, data = {}) => {
1673
1708
  const id = newBlockId();
1674
1709
  dispatch({ type: "insertAt", index, block: { id, type, data } });
@@ -1676,25 +1711,25 @@ function useBlockList(initial) {
1676
1711
  },
1677
1712
  []
1678
1713
  );
1679
- const update = React7.useCallback((id, data) => {
1714
+ const update = React11.useCallback((id, data) => {
1680
1715
  dispatch({ type: "update", id, data });
1681
1716
  }, []);
1682
- const remove = React7.useCallback((id) => {
1717
+ const remove = React11.useCallback((id) => {
1683
1718
  removalSeq += 1;
1684
1719
  const token = removalSeq;
1685
1720
  dispatch({ type: "remove", id, token });
1686
1721
  return token;
1687
1722
  }, []);
1688
- const undo = React7.useCallback((token) => {
1723
+ const undo = React11.useCallback((token) => {
1689
1724
  dispatch({ type: "undo", token });
1690
1725
  }, []);
1691
- const move = React7.useCallback((index, dir) => {
1726
+ const move = React11.useCallback((index, dir) => {
1692
1727
  dispatch({ type: "move", index, dir });
1693
1728
  }, []);
1694
- const reorder = React7.useCallback((from, to) => {
1729
+ const reorder = React11.useCallback((from, to) => {
1695
1730
  dispatch({ type: "reorder", from, to });
1696
1731
  }, []);
1697
- const reset = React7.useCallback((blocks) => {
1732
+ const reset = React11.useCallback((blocks) => {
1698
1733
  dispatch({ type: "reset", blocks });
1699
1734
  }, []);
1700
1735
  return { blocks: state.blocks, insertAt, update, remove, undo, move, reorder, reset };
@@ -1776,7 +1811,8 @@ function DropdownMenuItem({
1776
1811
  "data-slot": "dropdown-menu-item",
1777
1812
  "data-inset": inset,
1778
1813
  className: cn(
1779
- "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-none select-none data-[inset]:pl-8 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4",
1814
+ "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",
1815
+ HIGHLIGHT_BAR,
1780
1816
  TAP_TARGET_FLOOR,
1781
1817
  className
1782
1818
  ),
@@ -1853,7 +1889,7 @@ function BlockActionsMenu({
1853
1889
  }
1854
1890
 
1855
1891
  // src/canvas/BlockCard.tsx
1856
- var React8 = __toESM(require("react"), 1);
1892
+ var React12 = __toESM(require("react"), 1);
1857
1893
  var import_lucide_react10 = require("lucide-react");
1858
1894
 
1859
1895
  // src/canvas/BlockEditor.tsx
@@ -1947,32 +1983,36 @@ function BlockCardImpl({
1947
1983
  const definition = blockByType(blockTypes, block.type);
1948
1984
  const Preview = definition?.preview;
1949
1985
  const editable = Boolean(definition?.editor) && !readOnly;
1950
- const [editing, setEditing] = React8.useState(editingByDefault && editable);
1951
- const editButtonRef = React8.useRef(null);
1952
- const editorContainerRef = React8.useRef(null);
1953
- const returnFocusOnClose = React8.useRef(false);
1954
- const clickedRef = React8.useRef(null);
1955
- const scrollTopRef = React8.useRef(null);
1956
- const handleInsertBlock = React8.useCallback(
1986
+ const [editing, setEditing] = React12.useState(editingByDefault && editable);
1987
+ const editButtonRef = React12.useRef(null);
1988
+ const editorContainerRef = React12.useRef(null);
1989
+ const returnFocusOnClose = React12.useRef(false);
1990
+ const clickedRef = React12.useRef(null);
1991
+ const scrollTopRef = React12.useRef(null);
1992
+ const seedRef = React12.useRef(initialSelection);
1993
+ React12.useEffect(() => {
1994
+ seedRef.current = initialSelection;
1995
+ });
1996
+ const handleInsertBlock = React12.useCallback(
1957
1997
  (type) => onInsertBlock?.(index + 1, type),
1958
1998
  [onInsertBlock, index]
1959
1999
  );
1960
- const leaveEditing = React8.useCallback(() => {
2000
+ const leaveEditing = React12.useCallback(() => {
1961
2001
  returnFocusOnClose.current = true;
1962
2002
  setEditing(false);
1963
2003
  }, []);
1964
- const settleRef = React8.useRef(null);
2004
+ const settleRef = React12.useRef(null);
1965
2005
  const clearSettle = () => {
1966
2006
  if (settleRef.current === null) return;
1967
2007
  window.clearTimeout(settleRef.current);
1968
2008
  settleRef.current = null;
1969
2009
  };
1970
- React8.useEffect(() => clearSettle, []);
2010
+ React12.useEffect(() => clearSettle, []);
1971
2011
  function stillInside(node) {
1972
2012
  if (!node) return false;
1973
2013
  return Boolean(editorContainerRef.current?.contains(node)) || Boolean(node.closest(PORTALLED_OVERLAY));
1974
2014
  }
1975
- React8.useEffect(() => {
2015
+ React12.useEffect(() => {
1976
2016
  if (!editing) return;
1977
2017
  function onPointerDown(e) {
1978
2018
  if (stillInside(e.target)) return;
@@ -1989,7 +2029,7 @@ function BlockCardImpl({
1989
2029
  setEditing(false);
1990
2030
  }, 0);
1991
2031
  }
1992
- React8.useEffect(() => {
2032
+ React12.useEffect(() => {
1993
2033
  if (!editing) return;
1994
2034
  const field = editorContainerRef.current?.querySelector(
1995
2035
  'textarea, input, [role="combobox"]'
@@ -2000,7 +2040,7 @@ function BlockCardImpl({
2000
2040
  if (clicked === null || !(field instanceof HTMLTextAreaElement)) {
2001
2041
  field.focus();
2002
2042
  if (field instanceof HTMLTextAreaElement) {
2003
- const at = initialSelection ?? { start: field.value.length, end: field.value.length };
2043
+ const at = seedRef.current ?? { start: field.value.length, end: field.value.length };
2004
2044
  field.setSelectionRange(at.start, at.end);
2005
2045
  }
2006
2046
  return;
@@ -2008,26 +2048,26 @@ function BlockCardImpl({
2008
2048
  const offset = sourceOffsetFor(field.value, clicked);
2009
2049
  field.focus({ preventScroll: true });
2010
2050
  field.setSelectionRange(offset, offset);
2011
- }, [editing, initialSelection]);
2012
- React8.useLayoutEffect(() => {
2051
+ }, [editing]);
2052
+ useIsomorphicLayoutEffect(() => {
2013
2053
  const scrollTop = scrollTopRef.current;
2014
2054
  scrollTopRef.current = null;
2015
2055
  if (!editing || scrollTop === null) return;
2016
2056
  const scroller = document.scrollingElement ?? document.documentElement;
2017
2057
  if (scroller.scrollTop !== scrollTop) scroller.scrollTop = scrollTop;
2018
2058
  }, [editing]);
2019
- React8.useEffect(() => {
2059
+ React12.useEffect(() => {
2020
2060
  if (editing || !returnFocusOnClose.current) return;
2021
2061
  returnFocusOnClose.current = false;
2022
2062
  editButtonRef.current?.focus();
2023
2063
  }, [editing]);
2024
- const reportedType = React8.useRef(null);
2025
- React8.useEffect(() => {
2064
+ const reportedType = React12.useRef(null);
2065
+ React12.useEffect(() => {
2026
2066
  if (definition || reportedType.current === block.type) return;
2027
2067
  reportedType.current = block.type;
2028
2068
  onError(new Error(`No block type is registered for "${block.type}".`));
2029
2069
  }, [definition, block.type, onError]);
2030
- React8.useEffect(() => {
2070
+ React12.useEffect(() => {
2031
2071
  if (!readOnly || !definition || definition.preview) return;
2032
2072
  devWarn(
2033
2073
  `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.`
@@ -2108,20 +2148,21 @@ function BlockCardImpl({
2108
2148
  )
2109
2149
  ] });
2110
2150
  }
2111
- var BlockCard = React8.memo(BlockCardImpl);
2151
+ var BlockCard = React12.memo(BlockCardImpl);
2112
2152
 
2113
2153
  // src/canvas/BlockInserter.tsx
2114
- var React9 = __toESM(require("react"), 1);
2154
+ var React13 = __toESM(require("react"), 1);
2115
2155
  var import_lucide_react11 = require("lucide-react");
2116
2156
  var import_jsx_runtime23 = require("react/jsx-runtime");
2117
2157
  function BlockInserter({
2118
2158
  index,
2119
2159
  onInsert,
2120
- label
2160
+ label,
2161
+ canvasRef
2121
2162
  }) {
2122
2163
  const { blockTypes } = useQuoin();
2123
2164
  const options = paletteBlocks(blockTypes);
2124
- const insertedAtRef = React9.useRef(null);
2165
+ const insertedAtRef = React13.useRef(null);
2125
2166
  function handleSelect(type) {
2126
2167
  insertedAtRef.current = index;
2127
2168
  onInsert(index, type);
@@ -2131,7 +2172,7 @@ function BlockInserter({
2131
2172
  insertedAtRef.current = null;
2132
2173
  if (insertedAt === null) return;
2133
2174
  event.preventDefault();
2134
- const card = document.querySelector(`[data-block-index="${insertedAt}"]`);
2175
+ const card = canvasRef.current?.querySelector(`[data-block-index="${insertedAt}"]`);
2135
2176
  if (!card) return;
2136
2177
  if (typeof card.scrollIntoView === "function") {
2137
2178
  card.scrollIntoView({ block: "center", behavior: "smooth" });
@@ -2167,21 +2208,22 @@ function BlockInserter({
2167
2208
  }
2168
2209
 
2169
2210
  // src/canvas/DocumentStarter.tsx
2170
- var React10 = __toESM(require("react"), 1);
2211
+ var React14 = __toESM(require("react"), 1);
2171
2212
  var import_jsx_runtime24 = require("react/jsx-runtime");
2172
2213
  var LINE = [
2173
- "w-full resize-none rounded-md border-0 bg-transparent px-2 py-1 outline-none",
2214
+ "w-full resize-none rounded-md border-0 bg-transparent px-2 py-1",
2174
2215
  "text-base leading-7 placeholder:text-muted-foreground",
2175
- "transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:ring-ring/50"
2216
+ "transition-[color,box-shadow]",
2217
+ FOCUS_RING
2176
2218
  ].join(" ");
2177
2219
  function DocumentStarter({
2178
2220
  autoFocus = false,
2179
2221
  onStart,
2180
2222
  onInsertBlock
2181
2223
  }) {
2182
- const ref = React10.useRef(null);
2224
+ const ref = React14.useRef(null);
2183
2225
  const palette = useSlashPalette(ref);
2184
- const [shortcutsOpen, setShortcutsOpen] = React10.useState(false);
2226
+ const [shortcutsOpen, setShortcutsOpen] = React14.useState(false);
2185
2227
  function handleChange(e) {
2186
2228
  const value = e.target.value;
2187
2229
  e.target.value = "";
@@ -2203,6 +2245,7 @@ function DocumentStarter({
2203
2245
  defaultValue: "",
2204
2246
  onChange: handleChange,
2205
2247
  onKeyDown: (e) => {
2248
+ if (e.nativeEvent.isComposing || e.keyCode === 229) return;
2206
2249
  if (e.key !== "/") return;
2207
2250
  e.preventDefault();
2208
2251
  palette.openAtCaret();
@@ -2329,14 +2372,36 @@ function SortableBlock({
2329
2372
  }
2330
2373
 
2331
2374
  // src/canvas/unacknowledged.ts
2332
- var UNACKNOWLEDGED_LIMIT = 64;
2375
+ var STRUCTURAL_ECHO_LIMIT = 64;
2376
+ function createUnacknowledged() {
2377
+ return { sequences: /* @__PURE__ */ new WeakMap(), emitted: 0, acknowledged: 0, newest: [] };
2378
+ }
2379
+ function keepNewest(outstanding) {
2380
+ const kept = Math.min(STRUCTURAL_ECHO_LIMIT, outstanding.emitted - outstanding.acknowledged);
2381
+ outstanding.newest.splice(0, outstanding.newest.length - kept);
2382
+ }
2333
2383
  function remember(outstanding, blocks) {
2334
- outstanding.add(blocks);
2335
- while (outstanding.size > UNACKNOWLEDGED_LIMIT) {
2336
- const oldest = outstanding.values().next();
2337
- if (oldest.done) return;
2338
- outstanding.delete(oldest.value);
2384
+ outstanding.emitted += 1;
2385
+ outstanding.sequences.set(blocks, outstanding.emitted);
2386
+ outstanding.newest.push(blocks);
2387
+ keepNewest(outstanding);
2388
+ }
2389
+ function copiedSequence(outstanding, blocks) {
2390
+ for (const emitted of outstanding.newest) {
2391
+ if (sameBlocks(emitted, blocks)) return outstanding.sequences.get(emitted);
2339
2392
  }
2393
+ return void 0;
2394
+ }
2395
+ function acknowledge(outstanding, blocks) {
2396
+ const echoed = outstanding.sequences.get(blocks) ?? copiedSequence(outstanding, blocks);
2397
+ if (echoed === void 0 || echoed <= outstanding.acknowledged) return false;
2398
+ outstanding.acknowledged = echoed;
2399
+ keepNewest(outstanding);
2400
+ return true;
2401
+ }
2402
+ function forgetAll(outstanding) {
2403
+ outstanding.acknowledged = outstanding.emitted;
2404
+ keepNewest(outstanding);
2340
2405
  }
2341
2406
 
2342
2407
  // src/canvas/BlockCanvas.tsx
@@ -2357,20 +2422,20 @@ function Canvas({
2357
2422
  (0, import_core.useSensor)(import_core.PointerSensor, { activationConstraint: { distance: 4 } }),
2358
2423
  (0, import_core.useSensor)(import_core.KeyboardSensor, { coordinateGetter: import_sortable2.sortableKeyboardCoordinates })
2359
2424
  );
2360
- const dndId = React11.useId();
2361
- const [newBlockId2, setNewBlockId] = React11.useState(null);
2362
- const [seedSelection, setSeedSelection] = React11.useState(null);
2363
- const listRef = React11.useRef(null);
2364
- const pendingDeleteIndexRef = React11.useRef(null);
2365
- const removedRef = React11.useRef(null);
2366
- const announcements = React11.useMemo(
2425
+ const dndId = React15.useId();
2426
+ const [newBlockId2, setNewBlockId] = React15.useState(null);
2427
+ const [seedSelection, setSeedSelection] = React15.useState(null);
2428
+ const listRef = React15.useRef(null);
2429
+ const pendingDeleteIndexRef = React15.useRef(null);
2430
+ const removedRef = React15.useRef(null);
2431
+ const announcements = React15.useMemo(
2367
2432
  () => buildAnnouncements(list.blocks, blockTypes),
2368
2433
  [list.blocks, blockTypes]
2369
2434
  );
2370
- const lastEmitted = React11.useRef(blocks);
2371
- const lastSupplied = React11.useRef(blocks);
2372
- const unacknowledged = React11.useRef(/* @__PURE__ */ new Set());
2373
- React11.useEffect(() => {
2435
+ const lastEmitted = React15.useRef(blocks);
2436
+ const lastSupplied = React15.useRef(blocks);
2437
+ const unacknowledged = React15.useRef(createUnacknowledged());
2438
+ React15.useEffect(() => {
2374
2439
  if (list.blocks === lastEmitted.current) return;
2375
2440
  lastEmitted.current = list.blocks;
2376
2441
  remember(unacknowledged.current, list.blocks);
@@ -2379,21 +2444,22 @@ function Canvas({
2379
2444
  removedRef.current = null;
2380
2445
  if (removed) onDelete?.(removed.block, removed.index);
2381
2446
  }, [list.blocks, onChange, onDelete]);
2382
- React11.useEffect(() => {
2447
+ React15.useEffect(() => {
2383
2448
  const previous = lastSupplied.current;
2384
2449
  lastSupplied.current = blocks;
2385
- if (unacknowledged.current.has(blocks)) {
2386
- if (blocks === lastEmitted.current) unacknowledged.current.clear();
2387
- return;
2388
- }
2450
+ if (acknowledge(unacknowledged.current, blocks)) return;
2389
2451
  if (sameBlocks(blocks, previous) || sameBlocks(blocks, list.blocks)) return;
2390
- unacknowledged.current.clear();
2452
+ forgetAll(unacknowledged.current);
2391
2453
  lastEmitted.current = blocks;
2392
2454
  reset(blocks);
2393
2455
  }, [blocks, reset]);
2394
- const insertPlain = React11.useCallback(
2456
+ React15.useEffect(() => {
2457
+ if (newBlockId2 === null) return;
2458
+ setNewBlockId(null);
2459
+ setSeedSelection(null);
2460
+ }, [newBlockId2]);
2461
+ const insertPlain = React15.useCallback(
2395
2462
  (index, type, data) => {
2396
- setSeedSelection(null);
2397
2463
  const seed = data ?? blockByType(blockTypes, type)?.initialData?.() ?? {};
2398
2464
  setNewBlockId(insertAt(index, type, seed));
2399
2465
  },
@@ -2403,10 +2469,9 @@ function Canvas({
2403
2469
  const block = list.blocks[index];
2404
2470
  pendingDeleteIndexRef.current = index;
2405
2471
  if (block) removedRef.current = { block, index };
2406
- setNewBlockId((current) => current === id2 ? null : current);
2407
2472
  remove(id2);
2408
2473
  }
2409
- React11.useEffect(() => {
2474
+ React15.useEffect(() => {
2410
2475
  const index = pendingDeleteIndexRef.current;
2411
2476
  pendingDeleteIndexRef.current = null;
2412
2477
  if (index === null || !listRef.current) return;
@@ -2432,7 +2497,7 @@ function Canvas({
2432
2497
  };
2433
2498
  if (readOnly) return /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(ReadOnlyCanvas, { blocks: list.blocks, onUpdate: update, root: rootProps });
2434
2499
  return /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("div", { ...rootProps, children: [
2435
- /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(BlockInserter, { index: 0, onInsert: insertPlain, label: "Insert a block at the start" }),
2500
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(BlockInserter, { index: 0, onInsert: insertPlain, label: "Insert a block at the start", canvasRef: listRef }),
2436
2501
  /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
2437
2502
  import_core.DndContext,
2438
2503
  {
@@ -2476,7 +2541,8 @@ function Canvas({
2476
2541
  {
2477
2542
  index: i + 1,
2478
2543
  onInsert: insertPlain,
2479
- label: `Insert a block after block ${i + 1}`
2544
+ label: `Insert a block after block ${i + 1}`,
2545
+ canvasRef: listRef
2480
2546
  }
2481
2547
  )
2482
2548
  ] }, b.id)) }) })
@@ -2508,7 +2574,7 @@ function BlockCanvas({
2508
2574
  id,
2509
2575
  "aria-label": ariaLabel
2510
2576
  }) {
2511
- const config = React11.useMemo(
2577
+ const config = React15.useMemo(
2512
2578
  () => ({ blockTypes, upload, resolveAssetUrl, onError }),
2513
2579
  [blockTypes, upload, resolveAssetUrl, onError]
2514
2580
  );
@@ -2528,71 +2594,134 @@ function BlockCanvas({
2528
2594
  }
2529
2595
 
2530
2596
  // src/hooks/useAutosave.ts
2531
- var React12 = __toESM(require("react"), 1);
2597
+ var React16 = __toESM(require("react"), 1);
2598
+
2599
+ // src/hooks/autosave-backoff.ts
2600
+ function clearTimer(ref) {
2601
+ if (ref.current !== null) clearTimeout(ref.current);
2602
+ ref.current = null;
2603
+ }
2604
+ var alwaysRetry = () => true;
2532
2605
  function reportedFailure(result) {
2533
2606
  return typeof result === "object" && result !== null && "ok" in result && result.ok === false;
2534
2607
  }
2608
+ async function attemptSave(run) {
2609
+ try {
2610
+ const result = await run();
2611
+ return reportedFailure(result) ? { error: result } : null;
2612
+ } catch (error) {
2613
+ return { error };
2614
+ }
2615
+ }
2616
+ function nextRetryDelay(error, retriesSoFar, { debounceMs, maxBackoffMs, maxRetries, isRetryable }) {
2617
+ if (retriesSoFar >= maxRetries || !isRetryable(error)) return null;
2618
+ return Math.min(debounceMs * 2 ** (retriesSoFar + 1), maxBackoffMs);
2619
+ }
2620
+
2621
+ // src/hooks/useAutosave.ts
2535
2622
  function useAutosave({
2536
2623
  deps,
2537
2624
  build,
2538
2625
  save,
2539
2626
  debounceMs = 2e3,
2540
- maxBackoffMs = 3e4
2627
+ maxBackoffMs = 3e4,
2628
+ maxRetries = 8,
2629
+ isRetryable = alwaysRetry,
2630
+ flushOnHide = true
2541
2631
  }) {
2542
- const [status, setStatus] = React12.useState("idle");
2543
- const dirtyRef = React12.useRef(false);
2544
- const retryRef = React12.useRef(0);
2545
- const backoffRef = React12.useRef(null);
2546
- const buildRef = React12.useRef(build);
2547
- const saveRef = React12.useRef(save);
2548
- const runRef = React12.useRef(() => {
2632
+ const [status, setStatus] = React16.useState("idle");
2633
+ const [error, setError] = React16.useState(null);
2634
+ const dirtyRef = React16.useRef(false);
2635
+ const inFlightRef = React16.useRef(false);
2636
+ const disposedRef = React16.useRef(false);
2637
+ const retriesRef = React16.useRef(0);
2638
+ const debounceRef = React16.useRef(null);
2639
+ const backoffRef = React16.useRef(null);
2640
+ const buildRef = React16.useRef(build);
2641
+ const saveRef = React16.useRef(save);
2642
+ const runRef = React16.useRef(() => {
2549
2643
  });
2550
- React12.useEffect(() => {
2644
+ React16.useEffect(() => {
2551
2645
  buildRef.current = build;
2552
2646
  saveRef.current = save;
2647
+ const policy = { debounceMs, maxBackoffMs, maxRetries, isRetryable };
2553
2648
  runRef.current = () => {
2649
+ clearTimer(debounceRef);
2650
+ clearTimer(backoffRef);
2651
+ if (inFlightRef.current) return;
2652
+ inFlightRef.current = true;
2653
+ dirtyRef.current = false;
2654
+ if (!disposedRef.current) setStatus("saving");
2554
2655
  void (async () => {
2555
- setStatus("saving");
2556
- try {
2557
- const res = await saveRef.current(buildRef.current());
2558
- if (reportedFailure(res)) throw new Error("save rejected");
2559
- retryRef.current = 0;
2560
- dirtyRef.current = false;
2561
- setStatus("saved");
2562
- } catch {
2563
- setStatus("retrying");
2564
- retryRef.current += 1;
2565
- const delay = Math.min(debounceMs * 2 ** retryRef.current, maxBackoffMs);
2566
- if (backoffRef.current) clearTimeout(backoffRef.current);
2567
- backoffRef.current = setTimeout(() => runRef.current(), delay);
2656
+ const failure = await attemptSave(() => saveRef.current(buildRef.current()));
2657
+ inFlightRef.current = false;
2658
+ if (disposedRef.current) {
2659
+ if (dirtyRef.current) runRef.current();
2660
+ return;
2661
+ }
2662
+ if (!failure) {
2663
+ retriesRef.current = 0;
2664
+ setError(null);
2665
+ if (dirtyRef.current) runRef.current();
2666
+ else setStatus("saved");
2667
+ return;
2668
+ }
2669
+ dirtyRef.current = true;
2670
+ setError(failure.error);
2671
+ const delay = nextRetryDelay(failure.error, retriesRef.current, policy);
2672
+ if (delay === null) {
2673
+ setStatus("failed");
2674
+ return;
2568
2675
  }
2676
+ retriesRef.current += 1;
2677
+ setStatus("retrying");
2678
+ backoffRef.current = setTimeout(() => runRef.current(), delay);
2569
2679
  })();
2570
2680
  };
2571
2681
  });
2572
- const seenRef = React12.useRef(null);
2573
- React12.useEffect(() => {
2682
+ const seenRef = React16.useRef(null);
2683
+ React16.useEffect(() => {
2574
2684
  const seen2 = seenRef.current;
2575
2685
  seenRef.current = deps;
2576
2686
  if (seen2 === null) return;
2577
2687
  const unchanged = seen2.length === deps.length && seen2.every((v, i) => v === deps[i]);
2578
2688
  if (!unchanged) dirtyRef.current = true;
2579
2689
  }, deps);
2580
- React12.useEffect(() => {
2690
+ React16.useEffect(() => {
2581
2691
  if (!dirtyRef.current) return;
2582
- const handle = setTimeout(() => runRef.current(), debounceMs);
2583
- return () => clearTimeout(handle);
2692
+ clearTimer(debounceRef);
2693
+ debounceRef.current = setTimeout(() => runRef.current(), debounceMs);
2694
+ return () => clearTimer(debounceRef);
2584
2695
  }, deps);
2585
- React12.useEffect(
2586
- () => () => {
2587
- if (backoffRef.current) clearTimeout(backoffRef.current);
2588
- },
2589
- []
2590
- );
2591
- const retry = React12.useCallback(() => {
2592
- if (backoffRef.current) clearTimeout(backoffRef.current);
2696
+ React16.useEffect(() => {
2697
+ disposedRef.current = false;
2698
+ return () => {
2699
+ disposedRef.current = true;
2700
+ clearTimer(debounceRef);
2701
+ clearTimer(backoffRef);
2702
+ if (dirtyRef.current) runRef.current();
2703
+ };
2704
+ }, []);
2705
+ const retry = React16.useCallback(() => {
2706
+ retriesRef.current = 0;
2593
2707
  runRef.current();
2594
2708
  }, []);
2595
- return { status, retry };
2709
+ const flush = React16.useCallback(() => {
2710
+ if (dirtyRef.current) retry();
2711
+ }, [retry]);
2712
+ React16.useEffect(() => {
2713
+ if (!flushOnHide) return;
2714
+ const onVisibility = () => {
2715
+ if (document.visibilityState === "hidden") flush();
2716
+ };
2717
+ window.addEventListener("pagehide", flush);
2718
+ document.addEventListener("visibilitychange", onVisibility);
2719
+ return () => {
2720
+ window.removeEventListener("pagehide", flush);
2721
+ document.removeEventListener("visibilitychange", onVisibility);
2722
+ };
2723
+ }, [flushOnHide, flush]);
2724
+ return { status, error, retry, flush };
2596
2725
  }
2597
2726
  // Annotate the CommonJS export names for ESM import in node:
2598
2727
  0 && (module.exports = {