quoin-editor 0.1.1 → 0.3.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
@@ -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
@@ -37,6 +41,8 @@ export function Editor() {
37
41
 
38
42
  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
43
 
44
+ 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).
45
+
40
46
  ## The controlled-component contract
41
47
 
42
48
  `BlockCanvas` behaves like a controlled `<input value onChange>`. Update `blocks` synchronously from `onChange`.
@@ -68,8 +74,9 @@ import { useState } from 'react';
68
74
  import { BlockCanvas, useAutosave, type QuoinBlock } from 'quoin-editor';
69
75
  import 'quoin-editor/styles.css';
70
76
 
71
- async function saveDocument(blocks: QuoinBlock[]): Promise<void> {
72
- 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 }>;
73
80
  }
74
81
 
75
82
  export function AutosavingEditor({ initial }: { initial: QuoinBlock[] }) {
@@ -89,6 +96,8 @@ export function AutosavingEditor({ initial }: { initial: QuoinBlock[] }) {
89
96
  }
90
97
  ```
91
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
+
92
101
  ### Loading, undoing and discarding
93
102
 
94
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.
@@ -124,6 +133,46 @@ export function EditorWithDiscard({ saved }: { saved: QuoinBlock[] }) {
124
133
 
125
134
  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
135
 
136
+ ### Undoing a deletion
137
+
138
+ `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.
139
+
140
+ 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`.
141
+
142
+ 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.
143
+
144
+ ```tsx
145
+ import { useState } from 'react';
146
+ import { BlockCanvas, type QuoinBlock } from 'quoin-editor';
147
+ import 'quoin-editor/styles.css';
148
+
149
+ export function EditorWithUndo({ saved }: { saved: QuoinBlock[] }) {
150
+ const [blocks, setBlocks] = useState<QuoinBlock[]>(saved);
151
+ const [removed, setRemoved] = useState<{ block: QuoinBlock; index: number } | null>(null);
152
+
153
+ function restore() {
154
+ if (!removed) return;
155
+ const next = [...blocks];
156
+ next.splice(removed.index, 0, removed.block);
157
+ setBlocks(next);
158
+ setRemoved(null);
159
+ }
160
+
161
+ return (
162
+ <>
163
+ <BlockCanvas blocks={blocks} onChange={setBlocks} onDelete={(block, index) => setRemoved({ block, index })} />
164
+ {removed && (
165
+ <button type="button" onClick={restore}>
166
+ Undo delete
167
+ </button>
168
+ )}
169
+ </>
170
+ );
171
+ }
172
+ ```
173
+
174
+ 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.
175
+
127
176
  ## Hoist `blockTypes` and your handlers
128
177
 
129
178
  `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.
@@ -222,17 +271,82 @@ A definition has these fields:
222
271
  | `type` | yes | The string stored on each block. It is how the registry finds the definition. |
223
272
  | `label` | yes | The name shown in menus and the slash palette. |
224
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. |
225
- | `editor` | yes | The editing surface. It receives `data` and `onChange`. Call `onChange` with the whole next `data`, not a patch. |
226
- | `preview` | no | What the block shows when it is not being edited. Omit it for an edit-only block. |
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. |
227
276
  | `initialData` | no | Seeds `data` when a block of this type is inserted. It is checked against the editor's data type. |
228
277
  | `inSlashPalette` | no | Set to `false` to keep the type out of the slash palette and the inserter. Defaults to `true`. |
229
278
  | `alwaysEditing` | no | Renders the editor permanently instead of a preview you click to edit. |
230
279
 
231
- 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.
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.
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.
232
318
 
233
319
  ### Reaching uploads from a custom block
234
320
 
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.
321
+ 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.
322
+
323
+ `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.
324
+
325
+ ```tsx
326
+ import { UploadField, type BlockEditorProps } from 'quoin-editor';
327
+
328
+ type SlidesData = { storageKey: string; filename: string };
329
+
330
+ export function SlidesEditor({ data, onChange }: BlockEditorProps<SlidesData>) {
331
+ return (
332
+ <div>
333
+ <p>{data.storageKey === '' ? 'No deck yet' : data.filename}</p>
334
+ <UploadField
335
+ category="slides"
336
+ label="Upload slides"
337
+ accept=".pdf"
338
+ onUploaded={(result) => onChange({ storageKey: result.storageKey, filename: result.filename })}
339
+ />
340
+ </div>
341
+ );
342
+ }
343
+ ```
344
+
345
+ 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`.
346
+
347
+ `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.
348
+
349
+ 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
350
 
237
351
  ```tsx
238
352
  import { useQuoin, type BlockEditorProps } from 'quoin-editor';
@@ -352,77 +466,119 @@ export function Reader({ blocks }: { blocks: QuoinBlock[] }) {
352
466
 
353
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.
354
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
+
355
471
  ## Theming
356
472
 
357
- `quoin-editor/styles.css` is compiled Tailwind CSS v4 and defines these custom properties:
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:
358
474
 
359
475
  ```css
360
476
  :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;
477
+ --quoin-background: oklch(1 0 0);
478
+ --quoin-foreground: oklch(0.145 0 0);
479
+ --quoin-muted: oklch(0.97 0 0);
480
+ --quoin-muted-foreground: oklch(0.556 0 0);
481
+ --quoin-secondary: oklch(0.97 0 0);
482
+ --quoin-secondary-foreground: oklch(0.205 0 0);
483
+ --quoin-popover: oklch(1 0 0);
484
+ --quoin-popover-foreground: oklch(0.145 0 0);
485
+ --quoin-border: oklch(0.922 0 0);
486
+ --quoin-input: oklch(0.922 0 0);
487
+ --quoin-ring: oklch(0.708 0 0);
488
+ --quoin-primary: oklch(0.205 0 0);
489
+ --quoin-primary-foreground: oklch(0.985 0 0);
490
+ --quoin-accent: oklch(0.97 0 0);
491
+ --quoin-accent-foreground: oklch(0.205 0 0);
492
+ --quoin-destructive: oklch(0.577 0.245 27.325);
493
+ --quoin-radius: 0.625rem;
378
494
  }
379
- ```
380
-
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
495
 
383
- ```css
384
- :root {
385
- --primary: oklch(0.55 0.2 260);
386
- --radius: 0.5rem;
496
+ .dark {
497
+ --quoin-background: oklch(0.145 0 0);
498
+ --quoin-foreground: oklch(0.985 0 0);
499
+ --quoin-muted: oklch(0.269 0 0);
500
+ --quoin-muted-foreground: oklch(0.708 0 0);
501
+ --quoin-secondary: oklch(0.269 0 0);
502
+ --quoin-secondary-foreground: oklch(0.985 0 0);
503
+ --quoin-popover: oklch(0.205 0 0);
504
+ --quoin-popover-foreground: oklch(0.985 0 0);
505
+ --quoin-border: oklch(1 0 0 / 10%);
506
+ --quoin-input: oklch(1 0 0 / 15%);
507
+ --quoin-ring: oklch(0.556 0 0);
508
+ --quoin-primary: oklch(0.922 0 0);
509
+ --quoin-primary-foreground: oklch(0.205 0 0);
510
+ --quoin-accent: oklch(0.269 0 0);
511
+ --quoin-accent-foreground: oklch(0.985 0 0);
512
+ --quoin-destructive: oklch(0.704 0.191 22.216);
513
+ --quoin-radius: 0.625rem;
387
514
  }
388
515
  ```
389
516
 
390
- A project that already defines shadcn-style tokens with these names inherits its theme for free, with nothing to configure.
517
+ Out of the box that is a neutral grey editor that works in both modes with nothing configured.
518
+
519
+ 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
520
 
392
- ### When a token name collides
521
+ ### The prefix arrived in 0.2.0
393
522
 
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.
523
+ 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
524
 
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.
525
+ 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
526
 
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.
527
+ 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.
528
+
529
+ ### Mapping your own tokens
530
+
531
+ 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
532
 
400
533
  ```css
401
534
  .quoin {
402
- --accent: oklch(0.97 0 0);
403
- --accent-foreground: oklch(0.205 0 0);
404
- --border: oklch(0.922 0 0);
535
+ --quoin-background: var(--background);
536
+ --quoin-foreground: var(--foreground);
537
+ --quoin-muted: var(--muted);
538
+ --quoin-muted-foreground: var(--muted-foreground);
539
+ --quoin-secondary: var(--secondary);
540
+ --quoin-secondary-foreground: var(--secondary-foreground);
541
+ --quoin-popover: var(--popover);
542
+ --quoin-popover-foreground: var(--popover-foreground);
543
+ --quoin-border: var(--border);
544
+ --quoin-input: var(--input);
545
+ --quoin-ring: var(--ring);
546
+ --quoin-primary: var(--primary);
547
+ --quoin-primary-foreground: var(--primary-foreground);
548
+ --quoin-accent: var(--accent);
549
+ --quoin-accent-foreground: var(--accent-foreground);
550
+ --quoin-destructive: var(--destructive);
551
+ --quoin-radius: var(--radius);
552
+ }
553
+ ```
554
+
555
+ 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.
556
+
557
+ 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.
558
+
559
+ ```css
560
+ :root {
561
+ --quoin-primary: oklch(0.55 0.2 260);
562
+ --quoin-radius: 0.5rem;
405
563
  }
406
564
 
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%);
565
+ .dark {
566
+ --quoin-primary: oklch(0.72 0.17 260);
411
567
  }
412
568
  ```
413
569
 
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.
570
+ ### Dark mode
415
571
 
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.
572
+ 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
573
 
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.
574
+ ### Resets
419
575
 
420
576
  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
577
 
422
578
  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
579
 
424
580
  ```css
425
- @layer reset, theme, base, components, utilities;
581
+ @layer reset, theme, base, components, quoin, utilities;
426
582
 
427
583
  @layer reset {
428
584
  * { margin: 0; padding: 0; border: 0; }
@@ -435,6 +591,48 @@ All of Quoin's own rules are in cascade layers, so an **unlayered** global reset
435
591
 
436
592
  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
593
 
594
+ ### Tailwind, or no Tailwind
595
+
596
+ `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.
597
+
598
+ If you do use Tailwind, both major versions work, and what you have to do differs:
599
+
600
+ | Your setup | What to do |
601
+ | --- | --- |
602
+ | **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. |
603
+ | **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. |
604
+ | **No Tailwind** | Nothing. Import the stylesheet and the editor is styled. |
605
+
606
+ **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.
607
+
608
+ ### Cascade layers and import order
609
+
610
+ 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.
611
+
612
+ 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.
613
+
614
+ 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:
615
+
616
+ ```css
617
+ @layer theme, base, components, quoin, utilities;
618
+ @import "quoin-editor/styles.css" layer(quoin);
619
+ ```
620
+
621
+ With `quoin` named before `utilities`, your own utilities outrank Quoin's, including the responsive ones, while Quoin still outranks your `components`.
622
+
623
+ Naming it first instead puts every layer of yours above the package:
624
+
625
+ ```css
626
+ @layer quoin, theme, base, components, utilities;
627
+ @import "quoin-editor/styles.css" layer(quoin);
628
+ ```
629
+
630
+ 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.
631
+
632
+ **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.
633
+
634
+ 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.
635
+
438
636
  ## Headless
439
637
 
440
638
  `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 +706,7 @@ The main entry carries a `'use client'` banner, so a React Server Component has
508
706
 
509
707
  ## Exports
510
708
 
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).
709
+ 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`.
512
710
 
513
711
  From `quoin-editor/headless`: the transforms and the two hooks listed above, with no UI.
514
712