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 +181 -43
- package/dist/index.cjs +49 -35
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +41 -2
- package/dist/index.d.ts +41 -2
- package/dist/index.js +44 -32
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +1 -1
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
|
|
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
|
|
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
|
-
--
|
|
366
|
-
--
|
|
367
|
-
--
|
|
368
|
-
--
|
|
369
|
-
--
|
|
370
|
-
--
|
|
371
|
-
--
|
|
372
|
-
--
|
|
373
|
-
--
|
|
374
|
-
--
|
|
375
|
-
--
|
|
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
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
:
|
|
385
|
-
--
|
|
386
|
-
--
|
|
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
|
-
|
|
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
|
-
###
|
|
475
|
+
### The prefix arrived in 0.2.0
|
|
393
476
|
|
|
394
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
--
|
|
403
|
-
--
|
|
404
|
-
--
|
|
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
|
|
408
|
-
--
|
|
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
|
-
|
|
524
|
+
### Dark mode
|
|
415
525
|
|
|
416
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
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,
|
|
2283
|
+
/* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("div", { className: GUTTER, children: [
|
|
2282
2284
|
actions,
|
|
2283
|
-
/* @__PURE__ */ (0,
|
|
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,
|
|
2296
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_lucide_react12.GripVertical, { "aria-hidden": "true" })
|
|
2295
2297
|
}
|
|
2296
2298
|
)
|
|
2297
2299
|
] }),
|
|
2298
|
-
/* @__PURE__ */ (0,
|
|
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-
|
|
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/
|
|
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
|
-
|
|
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
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
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,
|
|
2421
|
-
/* @__PURE__ */ (0,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|