quoin-editor 0.1.1 → 0.2.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 CHANGED
@@ -37,6 +37,8 @@ export function Editor() {
37
37
 
38
38
  A document is an array of `QuoinBlock`, each `{ id, type, data }`. Five block types ship by default: `MARKDOWN`, `CALLOUT`, `CODE`, `IMAGE` and `FILE`. The main entry point is marked `'use client'`, so render `BlockCanvas` from a client component.
39
39
 
40
+ If your application has Tailwind of its own, import the stylesheet from your CSS into a layer you have named instead of from JavaScript, so you decide which of the two wins. See [Cascade layers and import order](#cascade-layers-and-import-order).
41
+
40
42
  ## The controlled-component contract
41
43
 
42
44
  `BlockCanvas` behaves like a controlled `<input value onChange>`. Update `blocks` synchronously from `onChange`.
@@ -124,6 +126,46 @@ export function EditorWithDiscard({ saved }: { saved: QuoinBlock[] }) {
124
126
 
125
127
  The structural comparison is shallow over each block's `data`. A host whose block `data` contains nested objects rebuilt on every render, and which does not echo `onChange` back into `blocks`, can still lose an edit. All five core block types have flat data, so this only affects custom blocks. Keep `data` flat, or keep it referentially stable.
126
128
 
129
+ ### Undoing a deletion
130
+
131
+ `onChange` reports a new array and nothing about what changed, so a host that wants a "Block deleted / Undo" toast would have to diff two arrays to find out. `onDelete` tells it instead: it fires when a block is removed through its actions menu, with the block and the index it held.
132
+
133
+ It runs just after the `onChange` that reports the document without that block, so by the time your handler runs the new array has already been reported. Restoring is an ordinary document load: splice the block back at its index and supply the result as `blocks`.
134
+
135
+ Do that on a later tick or a later interaction, such as the click of an Undo toast, which is what the sample below does. A restore applied synchronously inside `onDelete` is batched together with the echo of the deletion, so what reaches the canvas is structurally equal to the document it last had from you, which it reads as an unrelated re-render and ignores. Nothing throws; the deletion simply stands.
136
+
137
+ ```tsx
138
+ import { useState } from 'react';
139
+ import { BlockCanvas, type QuoinBlock } from 'quoin-editor';
140
+ import 'quoin-editor/styles.css';
141
+
142
+ export function EditorWithUndo({ saved }: { saved: QuoinBlock[] }) {
143
+ const [blocks, setBlocks] = useState<QuoinBlock[]>(saved);
144
+ const [removed, setRemoved] = useState<{ block: QuoinBlock; index: number } | null>(null);
145
+
146
+ function restore() {
147
+ if (!removed) return;
148
+ const next = [...blocks];
149
+ next.splice(removed.index, 0, removed.block);
150
+ setBlocks(next);
151
+ setRemoved(null);
152
+ }
153
+
154
+ return (
155
+ <>
156
+ <BlockCanvas blocks={blocks} onChange={setBlocks} onDelete={(block, index) => setRemoved({ block, index })} />
157
+ {removed && (
158
+ <button type="button" onClick={restore}>
159
+ Undo delete
160
+ </button>
161
+ )}
162
+ </>
163
+ );
164
+ }
165
+ ```
166
+
167
+ A restored block comes back at rest, even one that was inserted moments before it was deleted, so the undo does not pull the caret out of whatever is being written.
168
+
127
169
  ## Hoist `blockTypes` and your handlers
128
170
 
129
171
  `BlockCanvas` builds its configuration from `blockTypes`, `upload`, `resolveAssetUrl` and `onError`, and memoises it on their identity. Pass a new array or a new function on every render and the memo is busted, which re-renders every block editor on the page.
@@ -232,7 +274,35 @@ A definition that sets `alwaysEditing` should also supply a `preview`. A `readOn
232
274
 
233
275
  ### Reaching uploads from a custom block
234
276
 
235
- A block editor receives only `data` and `onChange`. The canvas configuration reaches it through `useQuoin()`, which returns `blockTypes`, `upload`, `resolveAssetUrl` and `onError`. It throws outside a `QuoinProvider`, so call it from a block the canvas is rendering.
277
+ A block editor receives only `data` and `onChange`, but the canvas configuration reaches it through context, so an upload control inside a custom block needs no wiring from you.
278
+
279
+ `UploadField` is the control the image and file blocks use, and it is exported for yours. It reads `upload` from the canvas it is rendered in, disables itself while a file is in flight, announces the progress to assistive technology, renders a rejection's message beside the button, and says that uploads are not configured when the host wired none.
280
+
281
+ ```tsx
282
+ import { UploadField, type BlockEditorProps } from 'quoin-editor';
283
+
284
+ type SlidesData = { storageKey: string; filename: string };
285
+
286
+ export function SlidesEditor({ data, onChange }: BlockEditorProps<SlidesData>) {
287
+ return (
288
+ <div>
289
+ <p>{data.storageKey === '' ? 'No deck yet' : data.filename}</p>
290
+ <UploadField
291
+ category="slides"
292
+ label="Upload slides"
293
+ accept=".pdf"
294
+ onUploaded={(result) => onChange({ storageKey: result.storageKey, filename: result.filename })}
295
+ />
296
+ </div>
297
+ );
298
+ }
299
+ ```
300
+
301
+ Register that editor with `defineBlock` as above, and the canvas's `upload` function receives `'slides'` as its category. `UploadField` takes `category`, `label`, an optional `accept` and `onUploaded`.
302
+
303
+ `FilePickerButton` is the picker underneath it: a labelled button laid over a transparent file input, which hands you the chosen `File` and uploads nothing itself. Take that one when you want your own busy and error presentation.
304
+
305
+ For anything neither control covers, `useQuoin()` returns `blockTypes`, `upload`, `resolveAssetUrl` and `onError`. It throws outside a `QuoinProvider`, so call it from a block the canvas is rendering.
236
306
 
237
307
  ```tsx
238
308
  import { useQuoin, type BlockEditorProps } from 'quoin-editor';
@@ -354,75 +424,115 @@ export function Reader({ blocks }: { blocks: QuoinBlock[] }) {
354
424
 
355
425
  ## Theming
356
426
 
357
- `quoin-editor/styles.css` is compiled Tailwind CSS v4 and defines these custom properties:
427
+ `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:
358
428
 
359
429
  ```css
360
430
  :root {
361
- --background: oklch(1 0 0);
362
- --foreground: oklch(0.145 0 0);
363
- --muted: oklch(0.97 0 0);
364
- --muted-foreground: oklch(0.556 0 0);
365
- --popover: oklch(1 0 0);
366
- --popover-foreground: oklch(0.145 0 0);
367
- --border: oklch(0.922 0 0);
368
- --input: oklch(0.922 0 0);
369
- --ring: oklch(0.708 0 0);
370
- --primary: oklch(0.205 0 0);
371
- --primary-foreground: oklch(0.985 0 0);
372
- --accent: oklch(0.97 0 0);
373
- --accent-foreground: oklch(0.205 0 0);
374
- --secondary: oklch(0.97 0 0);
375
- --secondary-foreground: oklch(0.205 0 0);
376
- --destructive: oklch(0.577 0.245 27.325);
377
- --radius: 0.625rem;
431
+ --quoin-background: oklch(1 0 0);
432
+ --quoin-foreground: oklch(0.145 0 0);
433
+ --quoin-muted: oklch(0.97 0 0);
434
+ --quoin-muted-foreground: oklch(0.556 0 0);
435
+ --quoin-secondary: oklch(0.97 0 0);
436
+ --quoin-secondary-foreground: oklch(0.205 0 0);
437
+ --quoin-popover: oklch(1 0 0);
438
+ --quoin-popover-foreground: oklch(0.145 0 0);
439
+ --quoin-border: oklch(0.922 0 0);
440
+ --quoin-input: oklch(0.922 0 0);
441
+ --quoin-ring: oklch(0.708 0 0);
442
+ --quoin-primary: oklch(0.205 0 0);
443
+ --quoin-primary-foreground: oklch(0.985 0 0);
444
+ --quoin-accent: oklch(0.97 0 0);
445
+ --quoin-accent-foreground: oklch(0.205 0 0);
446
+ --quoin-destructive: oklch(0.577 0.245 27.325);
447
+ --quoin-radius: 0.625rem;
378
448
  }
379
- ```
380
449
 
381
- Override any of them from your own CSS. They are declared inside `@layer base`, so a plain `:root` rule of yours wins without `!important`:
382
-
383
- ```css
384
- :root {
385
- --primary: oklch(0.55 0.2 260);
386
- --radius: 0.5rem;
450
+ .dark {
451
+ --quoin-background: oklch(0.145 0 0);
452
+ --quoin-foreground: oklch(0.985 0 0);
453
+ --quoin-muted: oklch(0.269 0 0);
454
+ --quoin-muted-foreground: oklch(0.708 0 0);
455
+ --quoin-secondary: oklch(0.269 0 0);
456
+ --quoin-secondary-foreground: oklch(0.985 0 0);
457
+ --quoin-popover: oklch(0.205 0 0);
458
+ --quoin-popover-foreground: oklch(0.985 0 0);
459
+ --quoin-border: oklch(1 0 0 / 10%);
460
+ --quoin-input: oklch(1 0 0 / 15%);
461
+ --quoin-ring: oklch(0.556 0 0);
462
+ --quoin-primary: oklch(0.922 0 0);
463
+ --quoin-primary-foreground: oklch(0.205 0 0);
464
+ --quoin-accent: oklch(0.269 0 0);
465
+ --quoin-accent-foreground: oklch(0.985 0 0);
466
+ --quoin-destructive: oklch(0.704 0.191 22.216);
467
+ --quoin-radius: 0.625rem;
387
468
  }
388
469
  ```
389
470
 
390
- A project that already defines shadcn-style tokens with these names inherits its theme for free, with nothing to configure.
471
+ Out of the box that is a neutral grey editor that works in both modes with nothing configured.
472
+
473
+ Those are the only custom properties the stylesheet declares. Apart from them it reads `--tw-*`, which belong to the Tailwind runtime it is compiled with, and the `--radix-*` values Radix writes on its own portalled elements while they are open. Tailwind's theme tokens are imported by reference, so the package resolves `--spacing`, `--text-base`, `--container-md` and the rest at build time and declares none of them on your page.
391
474
 
392
- ### When a token name collides
475
+ ### The prefix arrived in 0.2.0
393
476
 
394
- The same unprefixed names that make that inheritance free also let a collision happen by accident. A project that already declares one of them for something else hands Quoin a value nobody meant it to read, and Quoin cannot tell that apart from a theme deliberately shared with it.
477
+ Until 0.2.0 these names were unprefixed: `--background`, `--accent`, `--border` and the rest. That was sold as free theme inheritance, and for a shadcn-style host it was. It also meant Quoin read whatever a host happened to keep under those names.
395
478
 
396
- The stock Vite React + TypeScript template is the case to know about. It declares `--accent` as a bright purple and `--border` on `:root` with meanings of its own, so a fresh `npm create vite` project that installs Quoin draws the selected slash-palette row on that purple while keeping Quoin's own near-black `--accent-foreground`. The pair measures around 3.2:1, under the 4.5:1 that WCAG AA asks for, with nothing done wrong by the consumer.
479
+ The stock Vite React + TypeScript template is the case that forced the change. It declares `--accent` as a bright purple and `--border` on `:root` with meanings of its own, so a fresh `npm create vite` project that installed Quoin drew the selected slash-palette row on that purple while keeping Quoin's own near-black `--accent-foreground`. The pair measures around 3.2:1, under the 4.5:1 that WCAG AA asks for, with nothing done wrong by the consumer.
397
480
 
398
- Declare the tokens you did not mean to share on `.quoin` instead. The canvas root carries that class, and so do the menus, palette, select and dialog Quoin renders into `document.body`, so one rule covers all of them. It sits closer to the elements reading the token than your `:root` rule does, so it wins without `!important` and leaves your own `--accent` untouched.
481
+ A fallback form such as `var(--quoin-accent, var(--accent))` does not fix that, because the colliding value is exactly what gets read when only the unprefixed name is set. So the prefix is unconditional, and the inheritance is no longer free. If your theme stopped reaching Quoin when you upgraded, this is why, and the next section is how to hand it over again.
482
+
483
+ ### Mapping your own tokens
484
+
485
+ Point the prefixed names at your own in one block on `.quoin`. The canvas root carries that class, and so do the menus, palette, select and dialog Quoin renders into `document.body`, so one rule covers all of them.
399
486
 
400
487
  ```css
401
488
  .quoin {
402
- --accent: oklch(0.97 0 0);
403
- --accent-foreground: oklch(0.205 0 0);
404
- --border: oklch(0.922 0 0);
489
+ --quoin-background: var(--background);
490
+ --quoin-foreground: var(--foreground);
491
+ --quoin-muted: var(--muted);
492
+ --quoin-muted-foreground: var(--muted-foreground);
493
+ --quoin-secondary: var(--secondary);
494
+ --quoin-secondary-foreground: var(--secondary-foreground);
495
+ --quoin-popover: var(--popover);
496
+ --quoin-popover-foreground: var(--popover-foreground);
497
+ --quoin-border: var(--border);
498
+ --quoin-input: var(--input);
499
+ --quoin-ring: var(--ring);
500
+ --quoin-primary: var(--primary);
501
+ --quoin-primary-foreground: var(--primary-foreground);
502
+ --quoin-accent: var(--accent);
503
+ --quoin-accent-foreground: var(--accent-foreground);
504
+ --quoin-destructive: var(--destructive);
505
+ --quoin-radius: var(--radius);
506
+ }
507
+ ```
508
+
509
+ 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
+
511
+ 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.
512
+
513
+ ```css
514
+ :root {
515
+ --quoin-primary: oklch(0.55 0.2 260);
516
+ --quoin-radius: 0.5rem;
405
517
  }
406
518
 
407
- .dark .quoin {
408
- --accent: oklch(0.269 0 0);
409
- --accent-foreground: oklch(0.985 0 0);
410
- --border: oklch(1 0 0 / 10%);
519
+ .dark {
520
+ --quoin-primary: oklch(0.72 0.17 260);
411
521
  }
412
522
  ```
413
523
 
414
- Both rules are needed once you take a token over: pinning `--accent` on `.quoin` alone would also beat Quoin's own `.dark` value and freeze that token in its light form.
524
+ ### Dark mode
415
525
 
416
- Quoin keeps the unprefixed names rather than reading `--quoin-accent`. A prefix would stop the accident, but it would also cancel the free inheritance for every project that is already relying on it, and the package cannot tell a deliberate `--accent` from an accidental one.
526
+ Dark mode keys on a `.dark` class on an ancestor (for example `<html class="dark">`), not on `prefers-color-scheme`. Quoin redefines its own tokens under `.dark`, and its `dark:` variants follow the same class, so the two cannot disagree when a visitor's OS theme differs from your app's theme.
417
527
 
418
- Dark mode keys on a `.dark` class on an ancestor (for example `<html class="dark">`), not on `prefers-color-scheme`. Quoin redefines the same variables under `.dark`, and its `dark:` variants follow the same class, so the two cannot disagree when a visitor's OS theme differs from your app's theme.
528
+ ### Resets
419
529
 
420
530
  Quoin does not reset your page. The stylesheet contains no `html`, `body`, heading, list or other element rules, and no universal `*` reset. Its own reset (box sizing, zero margins, unstyled lists and form controls) applies only inside a `.quoin` element: the canvas root, and the menus, palette, select and dialog that Quoin renders into `document.body`, which carry the class themselves. Your headings, paragraphs, lists and buttons keep whatever styles you gave them, or the browser's.
421
531
 
422
532
  All of Quoin's own rules are in cascade layers, so an **unlayered** global reset of yours beats them. A host that ships one (Bootstrap's reboot, or a hand-rolled `* { margin: 0; padding: 0; border: 0 }`) will see Quoin's padding and borders vanish. Tailwind 4's preflight is itself layered and is fine. Either declare your reset in a layer that comes before Quoin's, or scope it away from `.quoin`:
423
533
 
424
534
  ```css
425
- @layer reset, theme, base, components, utilities;
535
+ @layer reset, theme, base, components, quoin, utilities;
426
536
 
427
537
  @layer reset {
428
538
  * { margin: 0; padding: 0; border: 0; }
@@ -435,6 +545,34 @@ All of Quoin's own rules are in cascade layers, so an **unlayered** global reset
435
545
 
436
546
  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.
437
547
 
548
+ ### Cascade layers and import order
549
+
550
+ 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.
551
+
552
+ That changed in 0.2.0, and it had to. Until then the package imported Tailwind's utilities into the bare `utilities` layer, so in a Tailwind host they appended into the host's own `utilities` layer, after the host's rules, and beat them on source order. One bare `.hidden { display: none }` from the package was enough to keep every element carrying `hidden md:block` hidden at every width, across the whole application.
553
+
554
+ The package cannot settle this on its own, because a browser orders layers by first appearance. A host whose own Tailwind is parsed first registers `theme`, `base`, `components` and `utilities` before `quoin` appears, so `quoin` sorts last and still wins. Name the order yourself, as the first thing in your stylesheet, and import Quoin into it:
555
+
556
+ ```css
557
+ @layer theme, base, components, quoin, utilities;
558
+ @import "quoin-editor/styles.css" layer(quoin);
559
+ ```
560
+
561
+ With `quoin` named before `utilities`, your own utilities outrank Quoin's, including the responsive ones, while Quoin still outranks your `components`.
562
+
563
+ Naming it first instead puts every layer of yours above the package:
564
+
565
+ ```css
566
+ @layer quoin, theme, base, components, utilities;
567
+ @import "quoin-editor/styles.css" layer(quoin);
568
+ ```
569
+
570
+ Layer position settles more than which utility wins. It also settles who owns a custom property both of you declare on `:root`, which is why it is worth deciding rather than inheriting. Quoin no longer declares any name Tailwind owns, because it imports Tailwind's theme by reference, but a package that does, including Quoin before 0.2.0, resizes every `p-4` on the page from whichever layer sorts last.
571
+
572
+ **Upgrading alone does not fix this.** Import 0.2.0 the way 0.1.1 was imported, with no layer statement of your own, and a host's `hidden md:block` still computes to `display: none`, exactly as before. The layer the package ships is what makes the fix available to you; the two lines above are what apply it.
573
+
574
+ Importing the stylesheet from a root layout, or with a plain `import 'quoin-editor/styles.css'` in JavaScript, gives you no say in where the layer lands. The bundler decides, and in a Tailwind host that usually means Quoin's layer sorts last. That is fine for an application with no Tailwind of its own, and it is what the samples in this README do for brevity. A Tailwind host should use the two lines above and drop the JavaScript import.
575
+
438
576
  ## Headless
439
577
 
440
578
  `quoin-editor/headless` is the text-editing core with no UI in it: pure transforms over a string and a selection, plus two hooks. It has no component or UI library dependency, and its bundle carries no `'use client'` banner, so the pure functions can be imported from a React Server Component.
@@ -508,7 +646,7 @@ The main entry carries a `'use client'` banner, so a React Server Component has
508
646
 
509
647
  ## Exports
510
648
 
511
- From `quoin-editor`: `BlockCanvas`, `QuoinProvider`, `useQuoin`, `defineBlock`, `blockByType`, `paletteBlocks`, `coreBlocks`, `TEXT_COMMANDS`, `useBlockList`, `useAutosave`, `useAutoGrow`, and the headless transforms and hooks, with their types (`QuoinBlock`, `UploadFn`, `UploadResult`, `BlockDefinition`, `AnyBlockDefinition`, `BlockEditorProps`, `BlockPreviewProps`, `BlockCanvasProps` and others).
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).
512
650
 
513
651
  From `quoin-editor/headless`: the transforms and the two hooks listed above, with no UI.
514
652
 
package/dist/index.cjs CHANGED
@@ -32,8 +32,10 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
32
32
  var src_exports = {};
33
33
  __export(src_exports, {
34
34
  BlockCanvas: () => BlockCanvas,
35
+ FilePickerButton: () => FilePickerButton,
35
36
  QuoinProvider: () => QuoinProvider,
36
37
  TEXT_COMMANDS: () => TEXT_COMMANDS,
38
+ UploadField: () => UploadField,
37
39
  applyEdit: () => applyEdit,
38
40
  blockByType: () => blockByType,
39
41
  caretRect: () => caretRect,
@@ -193,12 +195,7 @@ function Button({
193
195
 
194
196
  // src/upload/FilePickerButton.tsx
195
197
  var import_jsx_runtime3 = require("react/jsx-runtime");
196
- function FilePickerButton({
197
- label,
198
- accept,
199
- disabled,
200
- onPick
201
- }) {
198
+ function FilePickerButton({ label, accept, disabled, onPick }) {
202
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: [
203
200
  /* @__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: [
204
201
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_lucide_react.Upload, { "aria-hidden": "true" }),
@@ -1405,12 +1402,7 @@ var import_react = require("react");
1405
1402
  // src/upload/UploadField.tsx
1406
1403
  var React6 = __toESM(require("react"), 1);
1407
1404
  var import_jsx_runtime15 = require("react/jsx-runtime");
1408
- function UploadField({
1409
- category,
1410
- label,
1411
- accept,
1412
- onUploaded
1413
- }) {
1405
+ function UploadField({ category, label, accept, onUploaded }) {
1414
1406
  const { upload, onError } = useQuoin();
1415
1407
  const [busy, setBusy] = React6.useState(false);
1416
1408
  const [error, setError] = React6.useState(null);
@@ -2228,6 +2220,16 @@ function DocumentStarter({
2228
2220
  ] });
2229
2221
  }
2230
2222
 
2223
+ // src/canvas/ReadOnlyCanvas.tsx
2224
+ var import_jsx_runtime25 = require("react/jsx-runtime");
2225
+ function ReadOnlyCanvas({
2226
+ blocks,
2227
+ onUpdate,
2228
+ root
2229
+ }) {
2230
+ return /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("div", { ...root, children: /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("ul", { className: "flex flex-col", children: blocks.map((b, i) => /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("div", { "data-block-index": i, className: "rounded-lg px-2 py-1", children: /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(BlockCard, { block: b, index: i, editingByDefault: false, readOnly: true, onUpdate }) }) }, b.id)) }) });
2231
+ }
2232
+
2231
2233
  // src/canvas/reorder-indices.ts
2232
2234
  function reorderIndices(ids, activeId, overId) {
2233
2235
  if (overId === null || activeId === overId) return null;
@@ -2257,7 +2259,7 @@ function sameBlocks(a, b) {
2257
2259
  var import_sortable = require("@dnd-kit/sortable");
2258
2260
  var import_utilities = require("@dnd-kit/utilities");
2259
2261
  var import_lucide_react12 = require("lucide-react");
2260
- var import_jsx_runtime25 = require("react/jsx-runtime");
2262
+ var import_jsx_runtime26 = require("react/jsx-runtime");
2261
2263
  var GUTTER = [
2262
2264
  "flex items-center justify-end gap-0.5 pb-1",
2263
2265
  "lg:absolute lg:right-full lg:top-1 lg:justify-start lg:pb-0 lg:pr-1",
@@ -2271,16 +2273,16 @@ function SortableBlock({
2271
2273
  children
2272
2274
  }) {
2273
2275
  const { attributes, listeners, setNodeRef, setActivatorNodeRef, transform, transition, isDragging } = (0, import_sortable.useSortable)({ id });
2274
- return /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(
2276
+ return /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(
2275
2277
  "div",
2276
2278
  {
2277
2279
  ref: setNodeRef,
2278
2280
  style: { transform: import_utilities.CSS.Transform.toString(transform), transition },
2279
2281
  className: cn("group/block relative", isDragging && "z-10 opacity-80"),
2280
2282
  children: [
2281
- /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("div", { className: GUTTER, children: [
2283
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("div", { className: GUTTER, children: [
2282
2284
  actions,
2283
- /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
2285
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
2284
2286
  Button,
2285
2287
  {
2286
2288
  type: "button",
@@ -2291,17 +2293,17 @@ function SortableBlock({
2291
2293
  "aria-label": `Reorder block ${index + 1}`,
2292
2294
  ...attributes,
2293
2295
  ...listeners,
2294
- children: /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(import_lucide_react12.GripVertical, { "aria-hidden": "true" })
2296
+ children: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_lucide_react12.GripVertical, { "aria-hidden": "true" })
2295
2297
  }
2296
2298
  )
2297
2299
  ] }),
2298
- /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
2300
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
2299
2301
  "div",
2300
2302
  {
2301
2303
  "data-block-index": index,
2302
2304
  className: cn(
2303
2305
  "rounded-lg px-2 py-1 transition-colors",
2304
- isDragging ? "bg-card shadow-[var(--shadow-md)]" : "group-hover/block:bg-muted/40"
2306
+ isDragging ? "bg-popover shadow-md" : "group-hover/block:bg-muted/40"
2305
2307
  ),
2306
2308
  children
2307
2309
  }
@@ -2311,8 +2313,7 @@ function SortableBlock({
2311
2313
  );
2312
2314
  }
2313
2315
 
2314
- // src/canvas/BlockCanvas.tsx
2315
- var import_jsx_runtime26 = require("react/jsx-runtime");
2316
+ // src/canvas/unacknowledged.ts
2316
2317
  var UNACKNOWLEDGED_LIMIT = 64;
2317
2318
  function remember(outstanding, blocks) {
2318
2319
  outstanding.add(blocks);
@@ -2322,9 +2323,13 @@ function remember(outstanding, blocks) {
2322
2323
  outstanding.delete(oldest.value);
2323
2324
  }
2324
2325
  }
2326
+
2327
+ // src/canvas/BlockCanvas.tsx
2328
+ var import_jsx_runtime27 = require("react/jsx-runtime");
2325
2329
  function Canvas({
2326
2330
  blocks,
2327
2331
  onChange,
2332
+ onDelete,
2328
2333
  blockTypes,
2329
2334
  readOnly,
2330
2335
  className,
@@ -2342,6 +2347,7 @@ function Canvas({
2342
2347
  const [seedSelection, setSeedSelection] = React11.useState(null);
2343
2348
  const listRef = React11.useRef(null);
2344
2349
  const pendingDeleteIndexRef = React11.useRef(null);
2350
+ const removedRef = React11.useRef(null);
2345
2351
  const announcements = React11.useMemo(
2346
2352
  () => buildAnnouncements(list.blocks, blockTypes),
2347
2353
  [list.blocks, blockTypes]
@@ -2354,7 +2360,10 @@ function Canvas({
2354
2360
  lastEmitted.current = list.blocks;
2355
2361
  remember(unacknowledged.current, list.blocks);
2356
2362
  onChange(list.blocks);
2357
- }, [list.blocks, onChange]);
2363
+ const removed = removedRef.current;
2364
+ removedRef.current = null;
2365
+ if (removed) onDelete?.(removed.block, removed.index);
2366
+ }, [list.blocks, onChange, onDelete]);
2358
2367
  React11.useEffect(() => {
2359
2368
  const previous = lastSupplied.current;
2360
2369
  lastSupplied.current = blocks;
@@ -2376,7 +2385,10 @@ function Canvas({
2376
2385
  [insertAt, blockTypes]
2377
2386
  );
2378
2387
  function handleDelete(index, id2) {
2388
+ const block = list.blocks[index];
2379
2389
  pendingDeleteIndexRef.current = index;
2390
+ if (block) removedRef.current = { block, index };
2391
+ setNewBlockId((current) => current === id2 ? null : current);
2380
2392
  remove(id2);
2381
2393
  }
2382
2394
  React11.useEffect(() => {
@@ -2403,12 +2415,10 @@ function Canvas({
2403
2415
  role: ariaLabel ? "group" : void 0,
2404
2416
  "aria-label": ariaLabel
2405
2417
  };
2406
- if (readOnly) {
2407
- return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("div", { ...rootProps, children: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("ul", { className: "flex flex-col", children: list.blocks.map((b, i) => /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("div", { "data-block-index": i, className: "rounded-lg px-2 py-1", children: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(BlockCard, { block: b, index: i, editingByDefault: false, readOnly: true, onUpdate: update }) }) }, b.id)) }) });
2408
- }
2409
- return /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("div", { ...rootProps, children: [
2410
- /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(BlockInserter, { index: 0, onInsert: insertPlain, label: "Insert a block at the start" }),
2411
- /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
2418
+ if (readOnly) return /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(ReadOnlyCanvas, { blocks: list.blocks, onUpdate: update, root: rootProps });
2419
+ return /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("div", { ...rootProps, children: [
2420
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(BlockInserter, { index: 0, onInsert: insertPlain, label: "Insert a block at the start" }),
2421
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
2412
2422
  import_core.DndContext,
2413
2423
  {
2414
2424
  id: dndId,
@@ -2417,13 +2427,13 @@ function Canvas({
2417
2427
  modifiers: [import_modifiers.restrictToVerticalAxis],
2418
2428
  accessibility: { announcements },
2419
2429
  onDragEnd: handleDragEnd,
2420
- children: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_sortable2.SortableContext, { items: list.blocks.map((b) => b.id), strategy: import_sortable2.verticalListSortingStrategy, children: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("ul", { className: "flex flex-col", children: list.blocks.map((b, i) => /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("li", { children: [
2421
- /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
2430
+ children: /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(import_sortable2.SortableContext, { items: list.blocks.map((b) => b.id), strategy: import_sortable2.verticalListSortingStrategy, children: /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("ul", { className: "flex flex-col", children: list.blocks.map((b, i) => /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("li", { children: [
2431
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
2422
2432
  SortableBlock,
2423
2433
  {
2424
2434
  id: b.id,
2425
2435
  index: i,
2426
- actions: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
2436
+ actions: /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
2427
2437
  BlockActionsMenu,
2428
2438
  {
2429
2439
  index: i,
@@ -2433,7 +2443,7 @@ function Canvas({
2433
2443
  onDelete: () => handleDelete(i, b.id)
2434
2444
  }
2435
2445
  ),
2436
- children: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
2446
+ children: /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
2437
2447
  BlockCard,
2438
2448
  {
2439
2449
  block: b,
@@ -2446,7 +2456,7 @@ function Canvas({
2446
2456
  )
2447
2457
  }
2448
2458
  ),
2449
- /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
2459
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
2450
2460
  BlockInserter,
2451
2461
  {
2452
2462
  index: i + 1,
@@ -2457,7 +2467,7 @@ function Canvas({
2457
2467
  ] }, b.id)) }) })
2458
2468
  }
2459
2469
  ),
2460
- /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
2470
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
2461
2471
  DocumentStarter,
2462
2472
  {
2463
2473
  autoFocus: list.blocks.length === 0,
@@ -2473,6 +2483,7 @@ function Canvas({
2473
2483
  function BlockCanvas({
2474
2484
  blocks,
2475
2485
  onChange,
2486
+ onDelete,
2476
2487
  blockTypes = coreBlocks,
2477
2488
  upload,
2478
2489
  resolveAssetUrl,
@@ -2486,11 +2497,12 @@ function BlockCanvas({
2486
2497
  () => ({ blockTypes, upload, resolveAssetUrl, onError }),
2487
2498
  [blockTypes, upload, resolveAssetUrl, onError]
2488
2499
  );
2489
- return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(QuoinProvider, { value: config, children: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
2500
+ return /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(QuoinProvider, { value: config, children: /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
2490
2501
  Canvas,
2491
2502
  {
2492
2503
  blocks,
2493
2504
  onChange,
2505
+ onDelete,
2494
2506
  blockTypes,
2495
2507
  readOnly,
2496
2508
  className,
@@ -2567,8 +2579,10 @@ function useAutosave({
2567
2579
  // Annotate the CommonJS export names for ESM import in node:
2568
2580
  0 && (module.exports = {
2569
2581
  BlockCanvas,
2582
+ FilePickerButton,
2570
2583
  QuoinProvider,
2571
2584
  TEXT_COMMANDS,
2585
+ UploadField,
2572
2586
  applyEdit,
2573
2587
  blockByType,
2574
2588
  caretRect,