quoin-editor 0.1.0 → 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 +245 -29
- package/dist/index.cjs +155 -53
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +120 -5
- package/dist/index.d.ts +120 -5
- package/dist/index.js +137 -49
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +40 -11
package/README.md
CHANGED
|
@@ -37,18 +37,20 @@ 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`.
|
|
43
45
|
|
|
44
|
-
|
|
46
|
+
Applying the edit late is tolerated, as long as what you eventually hand back is one of the arrays the canvas gave you. It recognises its own output even when the echo is several keystrokes behind, so `startTransition` and a debounced `setBlocks` both keep typing intact.
|
|
45
47
|
|
|
46
48
|
```tsx
|
|
47
49
|
import { startTransition, useState } from 'react';
|
|
48
50
|
import { BlockCanvas, type QuoinBlock } from 'quoin-editor';
|
|
49
51
|
import 'quoin-editor/styles.css';
|
|
50
52
|
|
|
51
|
-
export function
|
|
53
|
+
export function TransitionEditor() {
|
|
52
54
|
const [blocks, setBlocks] = useState<QuoinBlock[]>([]);
|
|
53
55
|
return (
|
|
54
56
|
<BlockCanvas
|
|
@@ -59,6 +61,8 @@ export function BrokenEditor() {
|
|
|
59
61
|
}
|
|
60
62
|
```
|
|
61
63
|
|
|
64
|
+
What breaks is a store that rebuilds the document on the way through, so what comes back is a structural copy rather than the array the canvas emitted. Arriving behind the canvas's own state, that copy reads as a foreign document and replaces live text with an older snapshot. The damage is not a clean revert: the characters the snapshot predates are dropped and the ones typed after them are kept, so `ZZTYPED hello world` can land as `ZZYED hello wold`. A Redux or Zustand round trip that clones the document, and a server-synced store, are the usual sources.
|
|
65
|
+
|
|
62
66
|
Debounce the side effect, not the state. `useAutosave` does exactly that: it tracks the values you give it, waits for the edits to settle, then calls your `save`, and it retries a failed save with exponential backoff.
|
|
63
67
|
|
|
64
68
|
```tsx
|
|
@@ -122,6 +126,46 @@ export function EditorWithDiscard({ saved }: { saved: QuoinBlock[] }) {
|
|
|
122
126
|
|
|
123
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.
|
|
124
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
|
+
|
|
125
169
|
## Hoist `blockTypes` and your handlers
|
|
126
170
|
|
|
127
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.
|
|
@@ -228,6 +272,74 @@ A definition has these fields:
|
|
|
228
272
|
|
|
229
273
|
A definition that sets `alwaysEditing` should also supply a `preview`. A `readOnly` canvas draws the preview and never the editor, so without a preview the block's content is invisible there.
|
|
230
274
|
|
|
275
|
+
### Reaching uploads from a custom block
|
|
276
|
+
|
|
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.
|
|
306
|
+
|
|
307
|
+
```tsx
|
|
308
|
+
import { useQuoin, type BlockEditorProps } from 'quoin-editor';
|
|
309
|
+
|
|
310
|
+
type AttachmentData = { storageKey: string; filename: string };
|
|
311
|
+
|
|
312
|
+
export function AttachmentEditor({ data, onChange }: BlockEditorProps<AttachmentData>) {
|
|
313
|
+
const { upload, resolveAssetUrl, onError } = useQuoin();
|
|
314
|
+
|
|
315
|
+
async function store(file: File) {
|
|
316
|
+
if (!upload) return;
|
|
317
|
+
try {
|
|
318
|
+
const stored = await upload(file, 'attachment');
|
|
319
|
+
onChange({ storageKey: stored.storageKey, filename: stored.filename });
|
|
320
|
+
} catch (error) {
|
|
321
|
+
onError(error instanceof Error ? error : new Error('The upload failed.'));
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return (
|
|
326
|
+
<div>
|
|
327
|
+
{data.storageKey !== '' && <a href={resolveAssetUrl(data.storageKey)}>{data.filename}</a>}
|
|
328
|
+
<input
|
|
329
|
+
type="file"
|
|
330
|
+
disabled={!upload}
|
|
331
|
+
onChange={(event) => {
|
|
332
|
+
const file = event.target.files?.[0];
|
|
333
|
+
if (file) void store(file);
|
|
334
|
+
}}
|
|
335
|
+
/>
|
|
336
|
+
</div>
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
`upload` is absent when the host wired none, which is why the control above disables itself rather than failing on the pick. `resolveAssetUrl` defaults to returning the key unchanged, and `onError` to `console.error`.
|
|
342
|
+
|
|
231
343
|
The preview is also how you bring your own markdown renderer. Register a `MARKDOWN` definition last, reuse the core editor, and swap the preview:
|
|
232
344
|
|
|
233
345
|
```tsx
|
|
@@ -312,49 +424,115 @@ export function Reader({ blocks }: { blocks: QuoinBlock[] }) {
|
|
|
312
424
|
|
|
313
425
|
## Theming
|
|
314
426
|
|
|
315
|
-
`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:
|
|
316
428
|
|
|
317
429
|
```css
|
|
318
430
|
:root {
|
|
319
|
-
--background: oklch(1 0 0);
|
|
320
|
-
--foreground: oklch(0.145 0 0);
|
|
321
|
-
--muted: oklch(0.97 0 0);
|
|
322
|
-
--muted-foreground: oklch(0.556 0 0);
|
|
323
|
-
--
|
|
324
|
-
--
|
|
325
|
-
--
|
|
326
|
-
--
|
|
327
|
-
--
|
|
328
|
-
--
|
|
329
|
-
--
|
|
330
|
-
--
|
|
331
|
-
--
|
|
332
|
-
--
|
|
333
|
-
--
|
|
334
|
-
--destructive: oklch(0.577 0.245 27.325);
|
|
335
|
-
--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;
|
|
448
|
+
}
|
|
449
|
+
|
|
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;
|
|
468
|
+
}
|
|
469
|
+
```
|
|
470
|
+
|
|
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.
|
|
474
|
+
|
|
475
|
+
### The prefix arrived in 0.2.0
|
|
476
|
+
|
|
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.
|
|
478
|
+
|
|
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.
|
|
480
|
+
|
|
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.
|
|
486
|
+
|
|
487
|
+
```css
|
|
488
|
+
.quoin {
|
|
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);
|
|
336
506
|
}
|
|
337
507
|
```
|
|
338
508
|
|
|
339
|
-
|
|
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.
|
|
340
512
|
|
|
341
513
|
```css
|
|
342
514
|
:root {
|
|
343
|
-
--primary: oklch(0.55 0.2 260);
|
|
344
|
-
--radius: 0.5rem;
|
|
515
|
+
--quoin-primary: oklch(0.55 0.2 260);
|
|
516
|
+
--quoin-radius: 0.5rem;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
.dark {
|
|
520
|
+
--quoin-primary: oklch(0.72 0.17 260);
|
|
345
521
|
}
|
|
346
522
|
```
|
|
347
523
|
|
|
348
|
-
|
|
524
|
+
### Dark mode
|
|
525
|
+
|
|
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.
|
|
349
527
|
|
|
350
|
-
|
|
528
|
+
### Resets
|
|
351
529
|
|
|
352
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.
|
|
353
531
|
|
|
354
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`:
|
|
355
533
|
|
|
356
534
|
```css
|
|
357
|
-
@layer reset, theme, base, components, utilities;
|
|
535
|
+
@layer reset, theme, base, components, quoin, utilities;
|
|
358
536
|
|
|
359
537
|
@layer reset {
|
|
360
538
|
* { margin: 0; padding: 0; border: 0; }
|
|
@@ -367,6 +545,34 @@ All of Quoin's own rules are in cascade layers, so an **unlayered** global reset
|
|
|
367
545
|
|
|
368
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.
|
|
369
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
|
+
|
|
370
576
|
## Headless
|
|
371
577
|
|
|
372
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.
|
|
@@ -393,7 +599,7 @@ export function bold(text: string, start: number, end: number): string {
|
|
|
393
599
|
}
|
|
394
600
|
```
|
|
395
601
|
|
|
396
|
-
`useMarkdownKeymap` and `useAutoGrow` are
|
|
602
|
+
`useMarkdownKeymap` and `useAutoGrow` are importable from a React Server Component, because the module that exports them carries no `'use client'` banner. Being hooks, they are callable only from a client component.
|
|
397
603
|
|
|
398
604
|
```tsx
|
|
399
605
|
'use client';
|
|
@@ -426,11 +632,21 @@ export function Notes() {
|
|
|
426
632
|
|
|
427
633
|
The keymap swallows a `/` typed at the start of a word (at the start of the text, or after a newline, space or tab, outside a code fence) and calls `onOpenPalette` instead, so open a palette of your own there, or insert the slash yourself.
|
|
428
634
|
|
|
429
|
-
|
|
635
|
+
Everything in the table is re-exported from the main `quoin-editor` entry too, alongside `TEXT_COMMANDS`, the built-in slash-palette entries. Import from `quoin-editor/headless` when you want the transforms without the components behind them, and from `quoin-editor` when you are already importing `BlockCanvas`.
|
|
636
|
+
|
|
637
|
+
```ts
|
|
638
|
+
import { toggleLinePrefix } from 'quoin-editor';
|
|
639
|
+
|
|
640
|
+
export function heading(text: string, start: number, end: number): string {
|
|
641
|
+
return toggleLinePrefix('h2', text, start, end).value;
|
|
642
|
+
}
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
The main entry carries a `'use client'` banner, so a React Server Component has to import from `quoin-editor/headless` rather than from `quoin-editor`.
|
|
430
646
|
|
|
431
647
|
## Exports
|
|
432
648
|
|
|
433
|
-
From `quoin`: `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).
|
|
434
650
|
|
|
435
651
|
From `quoin-editor/headless`: the transforms and the two hooks listed above, with no UI.
|
|
436
652
|
|