quoin-editor 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +77 -6
- package/dist/index.cjs +39 -21
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +85 -15
- package/dist/index.d.ts +85 -15
- package/dist/index.js +39 -21
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -22,6 +22,10 @@ yarn add quoin-editor
|
|
|
22
22
|
|
|
23
23
|
Quoin needs React 18.2 or later (React 19 is supported) and TypeScript 5.4 or later if you use TypeScript. The block registry relies on the built-in `NoInfer` utility type, which arrived in 5.4.
|
|
24
24
|
|
|
25
|
+
**Tailwind is not required, and either major version works.** See [Tailwind, or no Tailwind](#tailwind-or-no-tailwind) for the one thing a v4 host has to do.
|
|
26
|
+
|
|
27
|
+
The package is around 200 kB packed. It depends on `@dnd-kit` for dragging, five Radix primitives, `cmdk` for the slash palette, `lucide-react` for the icons, and `clsx`, `tailwind-merge` and `class-variance-authority` for class composition. That is the price of the canvas: the cards, the drag gutter, the inserters, the palette and the dialogs. If you want the text editing and none of the UI, [`quoin-editor/headless`](#headless) has no component or UI library dependency at all.
|
|
28
|
+
|
|
25
29
|
## Quickstart
|
|
26
30
|
|
|
27
31
|
```tsx
|
|
@@ -70,8 +74,9 @@ import { useState } from 'react';
|
|
|
70
74
|
import { BlockCanvas, useAutosave, type QuoinBlock } from 'quoin-editor';
|
|
71
75
|
import 'quoin-editor/styles.css';
|
|
72
76
|
|
|
73
|
-
async function saveDocument(blocks: QuoinBlock[])
|
|
74
|
-
await fetch('/api/document', { method: 'PUT', body: JSON.stringify(blocks) });
|
|
77
|
+
async function saveDocument(blocks: QuoinBlock[]) {
|
|
78
|
+
const response = await fetch('/api/document', { method: 'PUT', body: JSON.stringify(blocks) });
|
|
79
|
+
return response.json() as Promise<{ revision: number }>;
|
|
75
80
|
}
|
|
76
81
|
|
|
77
82
|
export function AutosavingEditor({ initial }: { initial: QuoinBlock[] }) {
|
|
@@ -91,6 +96,8 @@ export function AutosavingEditor({ initial }: { initial: QuoinBlock[] }) {
|
|
|
91
96
|
}
|
|
92
97
|
```
|
|
93
98
|
|
|
99
|
+
`save` may return anything, so a mutation, an axios call or a `fetch` that resolves to a body goes straight in with no wrapper to discard the value. A save fails by rejecting, or by resolving to an explicit `{ ok: false }`; everything else, `undefined` included, is a success. `status` moves `idle` to `saving` to `saved`, or to `retrying` while it backs off, and `retry()` tries again at once.
|
|
100
|
+
|
|
94
101
|
### Loading, undoing and discarding
|
|
95
102
|
|
|
96
103
|
You can hand the canvas a new `blocks` array at any time to load, undo or merge a document. A new array replaces what the canvas shows when it differs from the last array you supplied.
|
|
@@ -264,13 +271,50 @@ A definition has these fields:
|
|
|
264
271
|
| `type` | yes | The string stored on each block. It is how the registry finds the definition. |
|
|
265
272
|
| `label` | yes | The name shown in menus and the slash palette. |
|
|
266
273
|
| `icon` | yes | A [lucide](https://lucide.dev) icon component. Quoin depends on `lucide-react@^1`; add that same major to your own dependencies to import icons, so you do not end up with a second copy and mismatched icon types. |
|
|
267
|
-
| `editor` |
|
|
268
|
-
| `preview` |
|
|
274
|
+
| `editor` | one of the two | The editing surface. It receives `data` and `onChange`. Call `onChange` with the whole next `data`, not a patch. Omit it for a block that is never editable here. |
|
|
275
|
+
| `preview` | one of the two | What the block shows when it is not being edited. Omit it for an edit-only block. |
|
|
269
276
|
| `initialData` | no | Seeds `data` when a block of this type is inserted. It is checked against the editor's data type. |
|
|
270
277
|
| `inSlashPalette` | no | Set to `false` to keep the type out of the slash palette and the inserter. Defaults to `true`. |
|
|
271
278
|
| `alwaysEditing` | no | Renders the editor permanently instead of a preview you click to edit. |
|
|
272
279
|
|
|
273
|
-
|
|
280
|
+
Supply `editor`, `preview` or both. Omitting both is a type error, because such a block could only ever draw its own label.
|
|
281
|
+
|
|
282
|
+
**Omit `editor` for a block that is not yours to edit** here: content that belongs to a server-side record, a generated summary, an audit trail. The card then offers no way in, with no edit button and no click target, and `preview` becomes required because it is the only thing left that can draw the block.
|
|
283
|
+
|
|
284
|
+
```tsx
|
|
285
|
+
import { ShieldCheck } from 'lucide-react';
|
|
286
|
+
import { defineBlock } from 'quoin-editor';
|
|
287
|
+
|
|
288
|
+
const audit = defineBlock<{ approver: string }>({
|
|
289
|
+
type: 'AUDIT',
|
|
290
|
+
label: 'Audit trail',
|
|
291
|
+
icon: ShieldCheck,
|
|
292
|
+
preview: ({ data }) => <p>Signed off by {data.approver}</p>,
|
|
293
|
+
});
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
**Omit `preview` only for a block you never show read-only.** A `readOnly` canvas draws previews and never editors, so such a block shows nothing there but its own label. Quoin warns once per type in the console when that happens, naming the type. The same applies to `alwaysEditing`, which is also an editor and is also not drawn on a read-only canvas.
|
|
297
|
+
|
|
298
|
+
### What the core blocks store
|
|
299
|
+
|
|
300
|
+
Each core block keeps its content under its own keys. A host that persists blocks reads and writes these, so they are exported as types: import them rather than retyping the shapes.
|
|
301
|
+
|
|
302
|
+
| Type | `data` | Exported as |
|
|
303
|
+
| --- | --- | --- |
|
|
304
|
+
| `MARKDOWN` | `markdown: string` | `MarkdownBlockData` |
|
|
305
|
+
| `CALLOUT` | `tone: string`, `body: string` | `CalloutBlockData` |
|
|
306
|
+
| `CODE` | `language: string`, `code: string` | `CodeBlockData` |
|
|
307
|
+
| `IMAGE` | `storageKey: string`, `alt: string`, `caption?: string`, `width?: number`, `height?: number` | `ImageBlockData` |
|
|
308
|
+
| `FILE` | `storageKey: string`, `filename: string`, `size?: number` | `FileBlockData` |
|
|
309
|
+
|
|
310
|
+
`CoreBlockData` maps each `type` string to its shape, for a host writing one function over all five.
|
|
311
|
+
|
|
312
|
+
Two of these are worth saying out loud, because both have been guessed wrong:
|
|
313
|
+
|
|
314
|
+
- **The callout's prose is `body`, not `text`.** The label over the field reads "Callout text", which is what the author sees; `body` is what the block stores. `tone` is one of `info`, `tip`, `warning`, `success` or `danger`, each drawn in its own colour; anything else falls back to the neutral border and fill.
|
|
315
|
+
- **Markdown blocks store markdown source, not rendered HTML.** Quoin edits and stores the source and renders it as plain text; supply a `preview` if you want it rendered.
|
|
316
|
+
|
|
317
|
+
The optional fields are written after a block's first save: an image's `caption` when the author types one, its `width` and `height` once the uploaded bytes have been measured, a file's `size` when the upload reports it. `src/blocks/data.test.ts` reads the keys back out of each editor's source and fails if one drifts from the table above.
|
|
274
318
|
|
|
275
319
|
### Reaching uploads from a custom block
|
|
276
320
|
|
|
@@ -422,6 +466,8 @@ export function Reader({ blocks }: { blocks: QuoinBlock[] }) {
|
|
|
422
466
|
|
|
423
467
|
`readOnly` renders each block's `preview` with no inserters, menus, drag handles or editors. `onChange` is still required by the type, and is never called. `aria-label` names the document for assistive technology, and `className` and `id` go on the root element.
|
|
424
468
|
|
|
469
|
+
Because it draws previews and never editors, a block type registered without a `preview` has nothing to show here but its own label. Quoin warns once per type in the console when that happens, so give every type you render read-only a `preview`. It is not an error channel: `onError` is for the document, this is for the registration, so it goes to the developer and not to the reader.
|
|
470
|
+
|
|
425
471
|
## Theming
|
|
426
472
|
|
|
427
473
|
`quoin-editor/styles.css` is compiled Tailwind CSS v4. Every colour and every radius it draws with comes from a custom property under a `--quoin-` prefix. It ships a default for all of them, on `:root` for light and on `.dark` for dark:
|
|
@@ -444,6 +490,9 @@ export function Reader({ blocks }: { blocks: QuoinBlock[] }) {
|
|
|
444
490
|
--quoin-accent: oklch(0.97 0 0);
|
|
445
491
|
--quoin-accent-foreground: oklch(0.205 0 0);
|
|
446
492
|
--quoin-destructive: oklch(0.577 0.245 27.325);
|
|
493
|
+
--quoin-info: oklch(0.55 0.17 250);
|
|
494
|
+
--quoin-success: oklch(0.55 0.14 155);
|
|
495
|
+
--quoin-warning: oklch(0.65 0.16 65);
|
|
447
496
|
--quoin-radius: 0.625rem;
|
|
448
497
|
}
|
|
449
498
|
|
|
@@ -464,6 +513,9 @@ export function Reader({ blocks }: { blocks: QuoinBlock[] }) {
|
|
|
464
513
|
--quoin-accent: oklch(0.269 0 0);
|
|
465
514
|
--quoin-accent-foreground: oklch(0.985 0 0);
|
|
466
515
|
--quoin-destructive: oklch(0.704 0.191 22.216);
|
|
516
|
+
--quoin-info: oklch(0.72 0.14 250);
|
|
517
|
+
--quoin-success: oklch(0.74 0.15 155);
|
|
518
|
+
--quoin-warning: oklch(0.79 0.15 65);
|
|
467
519
|
--quoin-radius: 0.625rem;
|
|
468
520
|
}
|
|
469
521
|
```
|
|
@@ -502,10 +554,15 @@ Point the prefixed names at your own in one block on `.quoin`. The canvas root c
|
|
|
502
554
|
--quoin-accent: var(--accent);
|
|
503
555
|
--quoin-accent-foreground: var(--accent-foreground);
|
|
504
556
|
--quoin-destructive: var(--destructive);
|
|
557
|
+
--quoin-info: var(--info);
|
|
558
|
+
--quoin-success: var(--success);
|
|
559
|
+
--quoin-warning: var(--warning);
|
|
505
560
|
--quoin-radius: var(--radius);
|
|
506
561
|
}
|
|
507
562
|
```
|
|
508
563
|
|
|
564
|
+
`--quoin-info`, `--quoin-success` and `--quoin-warning` arrived in 0.4.0 and are read only by the callout block, for its rule and the wash behind it, never for type. If your own theme has no equivalent, leave those three out of the block and the defaults above stand; the rest of the editor does not touch them.
|
|
565
|
+
|
|
509
566
|
That one block covers both modes: the tokens on the right already flip with your own dark class, and each mapping beats Quoin's `:root` and `.dark` defaults alike. Write it unlayered, or in a layer that sorts after `quoin`, so it outranks those defaults.
|
|
510
567
|
|
|
511
568
|
To change one token rather than adopt a whole theme, declare it on `:root`, and pair it with a `.dark` rule if you theme dark at all. Pinning a token on `:root` alone also beats Quoin's `.dark` value for it, which would freeze that one token in its light form.
|
|
@@ -545,6 +602,20 @@ All of Quoin's own rules are in cascade layers, so an **unlayered** global reset
|
|
|
545
602
|
|
|
546
603
|
The layer statement has to come before Quoin's stylesheet is parsed, so put it first in your own CSS or in an inline `<style>` in the document head.
|
|
547
604
|
|
|
605
|
+
### Tailwind, or no Tailwind
|
|
606
|
+
|
|
607
|
+
`quoin-editor/styles.css` is compiled output. Every value in it is already resolved, so it is a plain stylesheet, not a Tailwind plugin, and a project with no Tailwind at all can import it and be done.
|
|
608
|
+
|
|
609
|
+
If you do use Tailwind, both major versions work, and what you have to do differs:
|
|
610
|
+
|
|
611
|
+
| Your setup | What to do |
|
|
612
|
+
| --- | --- |
|
|
613
|
+
| **Tailwind v4** | Name the layer order and import Quoin into it, as [below](#cascade-layers-and-import-order). Without it Quoin's layer sorts last and its utilities beat yours. |
|
|
614
|
+
| **Tailwind v3** | Nothing. v3 emits its own utilities unlayered, and an unlayered rule beats a layered one whatever the order, so yours win over Quoin's by construction. |
|
|
615
|
+
| **No Tailwind** | Nothing. Import the stylesheet and the editor is styled. |
|
|
616
|
+
|
|
617
|
+
**On Tailwind v3, 0.2.0 is the first version that builds at all.** 0.1.1 and earlier emitted bare `@layer base` and `@layer utilities`, which the v3 PostCSS plugin refuses two different ways: importing the stylesheet from a component raises `'@layer base' is used but no matching '@tailwind base' directive is present`, and `@import`ing it from a stylesheet that does carry the `@tailwind` directives raises `TypeError: layerRule.nodes is not iterable`. Nesting everything under `quoin` in 0.2.0 removed both. If you worked around this by copying the stylesheet into `public/`, you can stop.
|
|
618
|
+
|
|
548
619
|
### Cascade layers and import order
|
|
549
620
|
|
|
550
621
|
Everything the stylesheet paints is inside one top-level cascade layer called `quoin`, with nested layers for its own theme, base and utilities inside it. Nothing it ships lands in a bare `theme`, `base`, `components` or `utilities` layer. It carries only the utilities Quoin's own components use, so the set of class names it can collide with at all is small and fixed.
|
|
@@ -646,7 +717,7 @@ The main entry carries a `'use client'` banner, so a React Server Component has
|
|
|
646
717
|
|
|
647
718
|
## Exports
|
|
648
719
|
|
|
649
|
-
From `quoin-editor`: `BlockCanvas`, `QuoinProvider`, `useQuoin`, `defineBlock`, `blockByType`, `paletteBlocks`, `coreBlocks`, `UploadField`, `FilePickerButton`, `TEXT_COMMANDS`, `useBlockList`, `useAutosave`, `useAutoGrow`, and the headless transforms and hooks, with their types (`QuoinBlock`, `UploadFn`, `UploadResult`, `BlockDefinition`, `AnyBlockDefinition`, `BlockEditorProps`, `BlockPreviewProps`, `BlockCanvasProps`, `UploadFieldProps`, `FilePickerButtonProps` and others)
|
|
720
|
+
From `quoin-editor`: `BlockCanvas`, `QuoinProvider`, `useQuoin`, `defineBlock`, `blockByType`, `paletteBlocks`, `coreBlocks`, `UploadField`, `FilePickerButton`, `TEXT_COMMANDS`, `useBlockList`, `useAutosave`, `useAutoGrow`, and the headless transforms and hooks, with their types (`QuoinBlock`, `UploadFn`, `UploadResult`, `BlockDefinition`, `AnyBlockDefinition`, `BlockEditorProps`, `BlockPreviewProps`, `BlockCanvasProps`, `UploadFieldProps`, `FilePickerButtonProps` and others), and the [core block data shapes](#what-the-core-blocks-store): `MarkdownBlockData`, `CalloutBlockData`, `CodeBlockData`, `ImageBlockData`, `FileBlockData` and `CoreBlockData`.
|
|
650
721
|
|
|
651
722
|
From `quoin-editor/headless`: the transforms and the two hooks listed above, with no UI.
|
|
652
723
|
|
package/dist/index.cjs
CHANGED
|
@@ -104,12 +104,12 @@ function blockByType(definitions, type) {
|
|
|
104
104
|
return void 0;
|
|
105
105
|
}
|
|
106
106
|
function paletteBlocks(definitions) {
|
|
107
|
-
const
|
|
107
|
+
const seen2 = /* @__PURE__ */ new Set();
|
|
108
108
|
const out = [];
|
|
109
109
|
for (let i = definitions.length - 1; i >= 0; i -= 1) {
|
|
110
110
|
const d = definitions[i];
|
|
111
|
-
if (
|
|
112
|
-
|
|
111
|
+
if (seen2.has(d.type)) continue;
|
|
112
|
+
seen2.add(d.type);
|
|
113
113
|
if (d.inSlashPalette === false) continue;
|
|
114
114
|
out.push(d);
|
|
115
115
|
}
|
|
@@ -1519,12 +1519,12 @@ function FileBlockEditor({ data, onChange }) {
|
|
|
1519
1519
|
// src/blocks/previews.tsx
|
|
1520
1520
|
var import_jsx_runtime18 = require("react/jsx-runtime");
|
|
1521
1521
|
var HINT = "text-sm text-muted-foreground";
|
|
1522
|
-
var
|
|
1523
|
-
info: "border-
|
|
1524
|
-
tip: "border-
|
|
1525
|
-
warning: "border-
|
|
1526
|
-
success: "border-
|
|
1527
|
-
danger: "border-destructive"
|
|
1522
|
+
var TONE = {
|
|
1523
|
+
info: "border-info bg-info/15",
|
|
1524
|
+
tip: "border-primary bg-primary/15",
|
|
1525
|
+
warning: "border-warning bg-warning/15",
|
|
1526
|
+
success: "border-success bg-success/15",
|
|
1527
|
+
danger: "border-destructive bg-destructive/15"
|
|
1528
1528
|
};
|
|
1529
1529
|
function MarkdownPreview({ data }) {
|
|
1530
1530
|
const markdown = str(data.markdown);
|
|
@@ -1551,8 +1551,8 @@ function CalloutPreview({ data }) {
|
|
|
1551
1551
|
const body = str(data.body);
|
|
1552
1552
|
if (body.trim() === "") return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("p", { className: HINT, children: "Empty callout" });
|
|
1553
1553
|
const tone = str(data.tone) || "info";
|
|
1554
|
-
const
|
|
1555
|
-
return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { "data-tone": tone, className: `rounded-md border-l-4 ${
|
|
1554
|
+
const paint = TONE[tone] ?? "border-border bg-muted/50";
|
|
1555
|
+
return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { "data-tone": tone, className: `rounded-md border-l-4 ${paint} p-3 text-sm`, children: body });
|
|
1556
1556
|
}
|
|
1557
1557
|
|
|
1558
1558
|
// src/blocks/core.ts
|
|
@@ -1866,9 +1866,8 @@ function BlockEditor({
|
|
|
1866
1866
|
onDone
|
|
1867
1867
|
}) {
|
|
1868
1868
|
const { blockTypes } = useQuoin();
|
|
1869
|
-
const
|
|
1870
|
-
if (!
|
|
1871
|
-
const Editor = definition.editor;
|
|
1869
|
+
const Editor = blockByType(blockTypes, type)?.editor;
|
|
1870
|
+
if (!Editor) return null;
|
|
1872
1871
|
return /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(Editor, { data, onChange, onInsertBlock, onDone });
|
|
1873
1872
|
}
|
|
1874
1873
|
|
|
@@ -1917,6 +1916,15 @@ function sourceOffsetFor(source, probe) {
|
|
|
1917
1916
|
return search(source, probe);
|
|
1918
1917
|
}
|
|
1919
1918
|
|
|
1919
|
+
// src/dev-warn.ts
|
|
1920
|
+
var seen = /* @__PURE__ */ new Set();
|
|
1921
|
+
function devWarn(message) {
|
|
1922
|
+
if (typeof process !== "undefined" && process?.env?.NODE_ENV === "production") return;
|
|
1923
|
+
if (seen.has(message)) return;
|
|
1924
|
+
seen.add(message);
|
|
1925
|
+
console.warn(`Quoin: ${message}`);
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1920
1928
|
// src/canvas/BlockCard.tsx
|
|
1921
1929
|
var import_jsx_runtime22 = require("react/jsx-runtime");
|
|
1922
1930
|
var INTERACTIVE_SELECTOR = 'a, button, input, textarea, select, [role="button"], [tabindex], iframe';
|
|
@@ -1938,7 +1946,8 @@ function BlockCardImpl({
|
|
|
1938
1946
|
const { blockTypes, onError } = useQuoin();
|
|
1939
1947
|
const definition = blockByType(blockTypes, block.type);
|
|
1940
1948
|
const Preview = definition?.preview;
|
|
1941
|
-
const
|
|
1949
|
+
const editable = Boolean(definition?.editor) && !readOnly;
|
|
1950
|
+
const [editing, setEditing] = React8.useState(editingByDefault && editable);
|
|
1942
1951
|
const editButtonRef = React8.useRef(null);
|
|
1943
1952
|
const editorContainerRef = React8.useRef(null);
|
|
1944
1953
|
const returnFocusOnClose = React8.useRef(false);
|
|
@@ -2018,7 +2027,13 @@ function BlockCardImpl({
|
|
|
2018
2027
|
reportedType.current = block.type;
|
|
2019
2028
|
onError(new Error(`No block type is registered for "${block.type}".`));
|
|
2020
2029
|
}, [definition, block.type, onError]);
|
|
2021
|
-
|
|
2030
|
+
React8.useEffect(() => {
|
|
2031
|
+
if (!readOnly || !definition || definition.preview) return;
|
|
2032
|
+
devWarn(
|
|
2033
|
+
`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.`
|
|
2034
|
+
);
|
|
2035
|
+
}, [readOnly, definition, block.type]);
|
|
2036
|
+
if (definition?.alwaysEditing && editable) {
|
|
2022
2037
|
return /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
|
|
2023
2038
|
BlockEditor,
|
|
2024
2039
|
{
|
|
@@ -2058,7 +2073,7 @@ function BlockCardImpl({
|
|
|
2058
2073
|
);
|
|
2059
2074
|
}
|
|
2060
2075
|
const rest = Preview ? /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(Preview, { data: block.data, blockId: block.id }) : /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("p", { className: "py-1 text-sm text-muted-foreground", children: definition?.label ?? block.type });
|
|
2061
|
-
if (
|
|
2076
|
+
if (!editable) return /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("div", { children: rest });
|
|
2062
2077
|
return /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("div", { className: "group/card relative", children: [
|
|
2063
2078
|
/* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
|
|
2064
2079
|
"div",
|
|
@@ -2514,6 +2529,9 @@ function BlockCanvas({
|
|
|
2514
2529
|
|
|
2515
2530
|
// src/hooks/useAutosave.ts
|
|
2516
2531
|
var React12 = __toESM(require("react"), 1);
|
|
2532
|
+
function reportedFailure(result) {
|
|
2533
|
+
return typeof result === "object" && result !== null && "ok" in result && result.ok === false;
|
|
2534
|
+
}
|
|
2517
2535
|
function useAutosave({
|
|
2518
2536
|
deps,
|
|
2519
2537
|
build,
|
|
@@ -2537,7 +2555,7 @@ function useAutosave({
|
|
|
2537
2555
|
setStatus("saving");
|
|
2538
2556
|
try {
|
|
2539
2557
|
const res = await saveRef.current(buildRef.current());
|
|
2540
|
-
if (res
|
|
2558
|
+
if (reportedFailure(res)) throw new Error("save rejected");
|
|
2541
2559
|
retryRef.current = 0;
|
|
2542
2560
|
dirtyRef.current = false;
|
|
2543
2561
|
setStatus("saved");
|
|
@@ -2553,10 +2571,10 @@ function useAutosave({
|
|
|
2553
2571
|
});
|
|
2554
2572
|
const seenRef = React12.useRef(null);
|
|
2555
2573
|
React12.useEffect(() => {
|
|
2556
|
-
const
|
|
2574
|
+
const seen2 = seenRef.current;
|
|
2557
2575
|
seenRef.current = deps;
|
|
2558
|
-
if (
|
|
2559
|
-
const unchanged =
|
|
2576
|
+
if (seen2 === null) return;
|
|
2577
|
+
const unchanged = seen2.length === deps.length && seen2.every((v, i) => v === deps[i]);
|
|
2560
2578
|
if (!unchanged) dirtyRef.current = true;
|
|
2561
2579
|
}, deps);
|
|
2562
2580
|
React12.useEffect(() => {
|