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/dist/index.d.cts CHANGED
@@ -102,6 +102,17 @@ type BlockCanvasProps = {
102
102
  */
103
103
  blocks: QuoinBlock[];
104
104
  onChange: (blocks: QuoinBlock[]) => void;
105
+ /**
106
+ * Called when a block is removed through its actions menu, with the block and the index it held.
107
+ * It runs just after the `onChange` that reports the document without it, so a host acting on the
108
+ * deletion is already looking at the new array. Restore the block by supplying a `blocks` array
109
+ * with it spliced back at that index, the same way any other document arrives.
110
+ *
111
+ * Restore it on a later tick or a later interaction, such as the click of an Undo toast. A
112
+ * restore applied synchronously inside this callback batches with the echo of the deletion, so
113
+ * what arrives is structurally equal to the document last supplied and is ignored.
114
+ */
115
+ onDelete?: (block: QuoinBlock, index: number) => void;
105
116
  /** Defaults to the five core block types. Pass `[...coreBlocks, yours]` to extend them. */
106
117
  blockTypes?: AnyBlockDefinition[];
107
118
  upload?: UploadFn;
@@ -116,11 +127,39 @@ type BlockCanvasProps = {
116
127
  'aria-label'?: string;
117
128
  };
118
129
  /** The editor: renders `blocks`, reports every edit through `onChange`, and accepts a new `blocks` array from outside at any time. */
119
- declare function BlockCanvas({ blocks, onChange, blockTypes, upload, resolveAssetUrl, onError, readOnly, className, id, 'aria-label': ariaLabel, }: BlockCanvasProps): React.JSX.Element;
130
+ declare function BlockCanvas({ blocks, onChange, onDelete, blockTypes, upload, resolveAssetUrl, onError, readOnly, className, id, 'aria-label': ariaLabel, }: BlockCanvasProps): React.JSX.Element;
120
131
 
121
132
  /** The block types Quoin ships with. Spread them alongside your own definitions. */
122
133
  declare const coreBlocks: AnyBlockDefinition[];
123
134
 
135
+ /** Props for `UploadField`; the upload function itself comes from the canvas, not from here. */
136
+ type UploadFieldProps = {
137
+ /** Names what the file is for, and is passed to the host's `upload` as its second argument. */
138
+ category: string;
139
+ /** Labels the control for both the pointer and assistive technology. */
140
+ label: string;
141
+ /** An `accept` attribute for the file input, such as `'image/*'` or `'.pdf'`. */
142
+ accept?: string;
143
+ /** Called once the upload resolves. Throwing from it is reported through the canvas `onError`. */
144
+ onUploaded: (result: UploadResult) => void;
145
+ };
146
+ /** A file picker wired to the canvas `upload` function, with its own busy state and inline error. */
147
+ declare function UploadField({ category, label, accept, onUploaded }: UploadFieldProps): React.JSX.Element;
148
+
149
+ /** Props for `FilePickerButton`. */
150
+ type FilePickerButtonProps = {
151
+ /** Labels the button and the file input behind it. */
152
+ label: string;
153
+ /** An `accept` attribute for the file input, such as `'image/*'` or `'.pdf'`. */
154
+ accept?: string;
155
+ /** Greys the button out and disables the input behind it. */
156
+ disabled?: boolean;
157
+ /** Called with the chosen file, and again if the same file is picked after a failure. */
158
+ onPick: (file: File) => void;
159
+ };
160
+ /** A button that opens the file picker and hands back the chosen file, performing no upload of its own. */
161
+ declare function FilePickerButton({ label, accept, disabled, onPick }: FilePickerButtonProps): React.JSX.Element;
162
+
124
163
  /** The resolved canvas configuration every block editor reads through `useQuoin`. */
125
164
  type QuoinConfig = {
126
165
  blockTypes: AnyBlockDefinition[];
@@ -279,4 +318,4 @@ declare function useMarkdownKeymap({ commit, onOpenPalette, }: {
279
318
  onPaste: (event: React.ClipboardEvent<HTMLTextAreaElement>) => void;
280
319
  };
281
320
 
282
- export { type AnyBlockDefinition, type AutosaveStatus, BlockCanvas, type BlockCanvasProps, type BlockDefinition, type BlockEditorProps, type BlockList, type BlockPreviewProps, type CaretRect, type Edit, type InsertableCommand, type LinePrefix, type QuoinBlock, type QuoinConfig, type QuoinConfigInput, QuoinProvider, TEXT_COMMANDS, type TextCommand, type UndoToken, type UploadFn, type UploadResult, type UseAutosaveInput, type UseAutosaveResult, type WrapKind, applyEdit, blockByType, caretRect, continueList, coreBlocks, defineBlock, formatShortcut, indentList, insertCommand, lineAt, linkPaste, paletteBlocks, shouldOpenPalette, toggleLinePrefix, useAutoGrow, useAutosave, useBlockList, useMarkdownKeymap, useQuoin, wrapSelection };
321
+ export { type AnyBlockDefinition, type AutosaveStatus, BlockCanvas, type BlockCanvasProps, type BlockDefinition, type BlockEditorProps, type BlockList, type BlockPreviewProps, type CaretRect, type Edit, FilePickerButton, type FilePickerButtonProps, type InsertableCommand, type LinePrefix, type QuoinBlock, type QuoinConfig, type QuoinConfigInput, QuoinProvider, TEXT_COMMANDS, type TextCommand, type UndoToken, UploadField, type UploadFieldProps, type UploadFn, type UploadResult, type UseAutosaveInput, type UseAutosaveResult, type WrapKind, applyEdit, blockByType, caretRect, continueList, coreBlocks, defineBlock, formatShortcut, indentList, insertCommand, lineAt, linkPaste, paletteBlocks, shouldOpenPalette, toggleLinePrefix, useAutoGrow, useAutosave, useBlockList, useMarkdownKeymap, useQuoin, wrapSelection };
package/dist/index.d.ts CHANGED
@@ -102,6 +102,17 @@ type BlockCanvasProps = {
102
102
  */
103
103
  blocks: QuoinBlock[];
104
104
  onChange: (blocks: QuoinBlock[]) => void;
105
+ /**
106
+ * Called when a block is removed through its actions menu, with the block and the index it held.
107
+ * It runs just after the `onChange` that reports the document without it, so a host acting on the
108
+ * deletion is already looking at the new array. Restore the block by supplying a `blocks` array
109
+ * with it spliced back at that index, the same way any other document arrives.
110
+ *
111
+ * Restore it on a later tick or a later interaction, such as the click of an Undo toast. A
112
+ * restore applied synchronously inside this callback batches with the echo of the deletion, so
113
+ * what arrives is structurally equal to the document last supplied and is ignored.
114
+ */
115
+ onDelete?: (block: QuoinBlock, index: number) => void;
105
116
  /** Defaults to the five core block types. Pass `[...coreBlocks, yours]` to extend them. */
106
117
  blockTypes?: AnyBlockDefinition[];
107
118
  upload?: UploadFn;
@@ -116,11 +127,39 @@ type BlockCanvasProps = {
116
127
  'aria-label'?: string;
117
128
  };
118
129
  /** The editor: renders `blocks`, reports every edit through `onChange`, and accepts a new `blocks` array from outside at any time. */
119
- declare function BlockCanvas({ blocks, onChange, blockTypes, upload, resolveAssetUrl, onError, readOnly, className, id, 'aria-label': ariaLabel, }: BlockCanvasProps): React.JSX.Element;
130
+ declare function BlockCanvas({ blocks, onChange, onDelete, blockTypes, upload, resolveAssetUrl, onError, readOnly, className, id, 'aria-label': ariaLabel, }: BlockCanvasProps): React.JSX.Element;
120
131
 
121
132
  /** The block types Quoin ships with. Spread them alongside your own definitions. */
122
133
  declare const coreBlocks: AnyBlockDefinition[];
123
134
 
135
+ /** Props for `UploadField`; the upload function itself comes from the canvas, not from here. */
136
+ type UploadFieldProps = {
137
+ /** Names what the file is for, and is passed to the host's `upload` as its second argument. */
138
+ category: string;
139
+ /** Labels the control for both the pointer and assistive technology. */
140
+ label: string;
141
+ /** An `accept` attribute for the file input, such as `'image/*'` or `'.pdf'`. */
142
+ accept?: string;
143
+ /** Called once the upload resolves. Throwing from it is reported through the canvas `onError`. */
144
+ onUploaded: (result: UploadResult) => void;
145
+ };
146
+ /** A file picker wired to the canvas `upload` function, with its own busy state and inline error. */
147
+ declare function UploadField({ category, label, accept, onUploaded }: UploadFieldProps): React.JSX.Element;
148
+
149
+ /** Props for `FilePickerButton`. */
150
+ type FilePickerButtonProps = {
151
+ /** Labels the button and the file input behind it. */
152
+ label: string;
153
+ /** An `accept` attribute for the file input, such as `'image/*'` or `'.pdf'`. */
154
+ accept?: string;
155
+ /** Greys the button out and disables the input behind it. */
156
+ disabled?: boolean;
157
+ /** Called with the chosen file, and again if the same file is picked after a failure. */
158
+ onPick: (file: File) => void;
159
+ };
160
+ /** A button that opens the file picker and hands back the chosen file, performing no upload of its own. */
161
+ declare function FilePickerButton({ label, accept, disabled, onPick }: FilePickerButtonProps): React.JSX.Element;
162
+
124
163
  /** The resolved canvas configuration every block editor reads through `useQuoin`. */
125
164
  type QuoinConfig = {
126
165
  blockTypes: AnyBlockDefinition[];
@@ -279,4 +318,4 @@ declare function useMarkdownKeymap({ commit, onOpenPalette, }: {
279
318
  onPaste: (event: React.ClipboardEvent<HTMLTextAreaElement>) => void;
280
319
  };
281
320
 
282
- export { type AnyBlockDefinition, type AutosaveStatus, BlockCanvas, type BlockCanvasProps, type BlockDefinition, type BlockEditorProps, type BlockList, type BlockPreviewProps, type CaretRect, type Edit, type InsertableCommand, type LinePrefix, type QuoinBlock, type QuoinConfig, type QuoinConfigInput, QuoinProvider, TEXT_COMMANDS, type TextCommand, type UndoToken, type UploadFn, type UploadResult, type UseAutosaveInput, type UseAutosaveResult, type WrapKind, applyEdit, blockByType, caretRect, continueList, coreBlocks, defineBlock, formatShortcut, indentList, insertCommand, lineAt, linkPaste, paletteBlocks, shouldOpenPalette, toggleLinePrefix, useAutoGrow, useAutosave, useBlockList, useMarkdownKeymap, useQuoin, wrapSelection };
321
+ export { type AnyBlockDefinition, type AutosaveStatus, BlockCanvas, type BlockCanvasProps, type BlockDefinition, type BlockEditorProps, type BlockList, type BlockPreviewProps, type CaretRect, type Edit, FilePickerButton, type FilePickerButtonProps, type InsertableCommand, type LinePrefix, type QuoinBlock, type QuoinConfig, type QuoinConfigInput, QuoinProvider, TEXT_COMMANDS, type TextCommand, type UndoToken, UploadField, type UploadFieldProps, type UploadFn, type UploadResult, type UseAutosaveInput, type UseAutosaveResult, type WrapKind, applyEdit, blockByType, caretRect, continueList, coreBlocks, defineBlock, formatShortcut, indentList, insertCommand, lineAt, linkPaste, paletteBlocks, shouldOpenPalette, toggleLinePrefix, useAutoGrow, useAutosave, useBlockList, useMarkdownKeymap, useQuoin, wrapSelection };
package/dist/index.js CHANGED
@@ -143,12 +143,7 @@ function Button({
143
143
 
144
144
  // src/upload/FilePickerButton.tsx
145
145
  import { jsx as jsx3, jsxs } from "react/jsx-runtime";
146
- function FilePickerButton({
147
- label,
148
- accept,
149
- disabled,
150
- onPick
151
- }) {
146
+ function FilePickerButton({ label, accept, disabled, onPick }) {
152
147
  return /* @__PURE__ */ jsxs("span", { className: "relative inline-flex rounded-md has-[:focus-visible]:ring-[3px] has-[:focus-visible]:ring-ring/50", children: [
153
148
  /* @__PURE__ */ jsx3(Button, { asChild: true, variant: "outline", size: "sm", children: /* @__PURE__ */ jsxs("span", { "aria-hidden": "true", className: disabled ? "pointer-events-none opacity-50" : void 0, children: [
154
149
  /* @__PURE__ */ jsx3(Upload, { "aria-hidden": "true" }),
@@ -1366,12 +1361,7 @@ import { useEffect as useEffect2, useRef as useRef3 } from "react";
1366
1361
  // src/upload/UploadField.tsx
1367
1362
  import * as React6 from "react";
1368
1363
  import { jsx as jsx15, jsxs as jsxs10 } from "react/jsx-runtime";
1369
- function UploadField({
1370
- category,
1371
- label,
1372
- accept,
1373
- onUploaded
1374
- }) {
1364
+ function UploadField({ category, label, accept, onUploaded }) {
1375
1365
  const { upload, onError } = useQuoin();
1376
1366
  const [busy, setBusy] = React6.useState(false);
1377
1367
  const [error, setError] = React6.useState(null);
@@ -2189,6 +2179,16 @@ function DocumentStarter({
2189
2179
  ] });
2190
2180
  }
2191
2181
 
2182
+ // src/canvas/ReadOnlyCanvas.tsx
2183
+ import { jsx as jsx25 } from "react/jsx-runtime";
2184
+ function ReadOnlyCanvas({
2185
+ blocks,
2186
+ onUpdate,
2187
+ root
2188
+ }) {
2189
+ return /* @__PURE__ */ jsx25("div", { ...root, children: /* @__PURE__ */ jsx25("ul", { className: "flex flex-col", children: blocks.map((b, i) => /* @__PURE__ */ jsx25("li", { children: /* @__PURE__ */ jsx25("div", { "data-block-index": i, className: "rounded-lg px-2 py-1", children: /* @__PURE__ */ jsx25(BlockCard, { block: b, index: i, editingByDefault: false, readOnly: true, onUpdate }) }) }, b.id)) }) });
2190
+ }
2191
+
2192
2192
  // src/canvas/reorder-indices.ts
2193
2193
  function reorderIndices(ids, activeId, overId) {
2194
2194
  if (overId === null || activeId === overId) return null;
@@ -2218,7 +2218,7 @@ function sameBlocks(a, b) {
2218
2218
  import { useSortable } from "@dnd-kit/sortable";
2219
2219
  import { CSS } from "@dnd-kit/utilities";
2220
2220
  import { GripVertical } from "lucide-react";
2221
- import { jsx as jsx25, jsxs as jsxs18 } from "react/jsx-runtime";
2221
+ import { jsx as jsx26, jsxs as jsxs18 } from "react/jsx-runtime";
2222
2222
  var GUTTER = [
2223
2223
  "flex items-center justify-end gap-0.5 pb-1",
2224
2224
  "lg:absolute lg:right-full lg:top-1 lg:justify-start lg:pb-0 lg:pr-1",
@@ -2241,7 +2241,7 @@ function SortableBlock({
2241
2241
  children: [
2242
2242
  /* @__PURE__ */ jsxs18("div", { className: GUTTER, children: [
2243
2243
  actions,
2244
- /* @__PURE__ */ jsx25(
2244
+ /* @__PURE__ */ jsx26(
2245
2245
  Button,
2246
2246
  {
2247
2247
  type: "button",
@@ -2252,17 +2252,17 @@ function SortableBlock({
2252
2252
  "aria-label": `Reorder block ${index + 1}`,
2253
2253
  ...attributes,
2254
2254
  ...listeners,
2255
- children: /* @__PURE__ */ jsx25(GripVertical, { "aria-hidden": "true" })
2255
+ children: /* @__PURE__ */ jsx26(GripVertical, { "aria-hidden": "true" })
2256
2256
  }
2257
2257
  )
2258
2258
  ] }),
2259
- /* @__PURE__ */ jsx25(
2259
+ /* @__PURE__ */ jsx26(
2260
2260
  "div",
2261
2261
  {
2262
2262
  "data-block-index": index,
2263
2263
  className: cn(
2264
2264
  "rounded-lg px-2 py-1 transition-colors",
2265
- isDragging ? "bg-card shadow-[var(--shadow-md)]" : "group-hover/block:bg-muted/40"
2265
+ isDragging ? "bg-popover shadow-md" : "group-hover/block:bg-muted/40"
2266
2266
  ),
2267
2267
  children
2268
2268
  }
@@ -2272,8 +2272,7 @@ function SortableBlock({
2272
2272
  );
2273
2273
  }
2274
2274
 
2275
- // src/canvas/BlockCanvas.tsx
2276
- import { jsx as jsx26, jsxs as jsxs19 } from "react/jsx-runtime";
2275
+ // src/canvas/unacknowledged.ts
2277
2276
  var UNACKNOWLEDGED_LIMIT = 64;
2278
2277
  function remember(outstanding, blocks) {
2279
2278
  outstanding.add(blocks);
@@ -2283,9 +2282,13 @@ function remember(outstanding, blocks) {
2283
2282
  outstanding.delete(oldest.value);
2284
2283
  }
2285
2284
  }
2285
+
2286
+ // src/canvas/BlockCanvas.tsx
2287
+ import { jsx as jsx27, jsxs as jsxs19 } from "react/jsx-runtime";
2286
2288
  function Canvas({
2287
2289
  blocks,
2288
2290
  onChange,
2291
+ onDelete,
2289
2292
  blockTypes,
2290
2293
  readOnly,
2291
2294
  className,
@@ -2303,6 +2306,7 @@ function Canvas({
2303
2306
  const [seedSelection, setSeedSelection] = React11.useState(null);
2304
2307
  const listRef = React11.useRef(null);
2305
2308
  const pendingDeleteIndexRef = React11.useRef(null);
2309
+ const removedRef = React11.useRef(null);
2306
2310
  const announcements = React11.useMemo(
2307
2311
  () => buildAnnouncements(list.blocks, blockTypes),
2308
2312
  [list.blocks, blockTypes]
@@ -2315,7 +2319,10 @@ function Canvas({
2315
2319
  lastEmitted.current = list.blocks;
2316
2320
  remember(unacknowledged.current, list.blocks);
2317
2321
  onChange(list.blocks);
2318
- }, [list.blocks, onChange]);
2322
+ const removed = removedRef.current;
2323
+ removedRef.current = null;
2324
+ if (removed) onDelete?.(removed.block, removed.index);
2325
+ }, [list.blocks, onChange, onDelete]);
2319
2326
  React11.useEffect(() => {
2320
2327
  const previous = lastSupplied.current;
2321
2328
  lastSupplied.current = blocks;
@@ -2337,7 +2344,10 @@ function Canvas({
2337
2344
  [insertAt, blockTypes]
2338
2345
  );
2339
2346
  function handleDelete(index, id2) {
2347
+ const block = list.blocks[index];
2340
2348
  pendingDeleteIndexRef.current = index;
2349
+ if (block) removedRef.current = { block, index };
2350
+ setNewBlockId((current) => current === id2 ? null : current);
2341
2351
  remove(id2);
2342
2352
  }
2343
2353
  React11.useEffect(() => {
@@ -2364,12 +2374,10 @@ function Canvas({
2364
2374
  role: ariaLabel ? "group" : void 0,
2365
2375
  "aria-label": ariaLabel
2366
2376
  };
2367
- if (readOnly) {
2368
- return /* @__PURE__ */ jsx26("div", { ...rootProps, children: /* @__PURE__ */ jsx26("ul", { className: "flex flex-col", children: list.blocks.map((b, i) => /* @__PURE__ */ jsx26("li", { children: /* @__PURE__ */ jsx26("div", { "data-block-index": i, className: "rounded-lg px-2 py-1", children: /* @__PURE__ */ jsx26(BlockCard, { block: b, index: i, editingByDefault: false, readOnly: true, onUpdate: update }) }) }, b.id)) }) });
2369
- }
2377
+ if (readOnly) return /* @__PURE__ */ jsx27(ReadOnlyCanvas, { blocks: list.blocks, onUpdate: update, root: rootProps });
2370
2378
  return /* @__PURE__ */ jsxs19("div", { ...rootProps, children: [
2371
- /* @__PURE__ */ jsx26(BlockInserter, { index: 0, onInsert: insertPlain, label: "Insert a block at the start" }),
2372
- /* @__PURE__ */ jsx26(
2379
+ /* @__PURE__ */ jsx27(BlockInserter, { index: 0, onInsert: insertPlain, label: "Insert a block at the start" }),
2380
+ /* @__PURE__ */ jsx27(
2373
2381
  DndContext,
2374
2382
  {
2375
2383
  id: dndId,
@@ -2378,13 +2386,13 @@ function Canvas({
2378
2386
  modifiers: [restrictToVerticalAxis],
2379
2387
  accessibility: { announcements },
2380
2388
  onDragEnd: handleDragEnd,
2381
- children: /* @__PURE__ */ jsx26(SortableContext, { items: list.blocks.map((b) => b.id), strategy: verticalListSortingStrategy, children: /* @__PURE__ */ jsx26("ul", { className: "flex flex-col", children: list.blocks.map((b, i) => /* @__PURE__ */ jsxs19("li", { children: [
2382
- /* @__PURE__ */ jsx26(
2389
+ children: /* @__PURE__ */ jsx27(SortableContext, { items: list.blocks.map((b) => b.id), strategy: verticalListSortingStrategy, children: /* @__PURE__ */ jsx27("ul", { className: "flex flex-col", children: list.blocks.map((b, i) => /* @__PURE__ */ jsxs19("li", { children: [
2390
+ /* @__PURE__ */ jsx27(
2383
2391
  SortableBlock,
2384
2392
  {
2385
2393
  id: b.id,
2386
2394
  index: i,
2387
- actions: /* @__PURE__ */ jsx26(
2395
+ actions: /* @__PURE__ */ jsx27(
2388
2396
  BlockActionsMenu,
2389
2397
  {
2390
2398
  index: i,
@@ -2394,7 +2402,7 @@ function Canvas({
2394
2402
  onDelete: () => handleDelete(i, b.id)
2395
2403
  }
2396
2404
  ),
2397
- children: /* @__PURE__ */ jsx26(
2405
+ children: /* @__PURE__ */ jsx27(
2398
2406
  BlockCard,
2399
2407
  {
2400
2408
  block: b,
@@ -2407,7 +2415,7 @@ function Canvas({
2407
2415
  )
2408
2416
  }
2409
2417
  ),
2410
- /* @__PURE__ */ jsx26(
2418
+ /* @__PURE__ */ jsx27(
2411
2419
  BlockInserter,
2412
2420
  {
2413
2421
  index: i + 1,
@@ -2418,7 +2426,7 @@ function Canvas({
2418
2426
  ] }, b.id)) }) })
2419
2427
  }
2420
2428
  ),
2421
- /* @__PURE__ */ jsx26(
2429
+ /* @__PURE__ */ jsx27(
2422
2430
  DocumentStarter,
2423
2431
  {
2424
2432
  autoFocus: list.blocks.length === 0,
@@ -2434,6 +2442,7 @@ function Canvas({
2434
2442
  function BlockCanvas({
2435
2443
  blocks,
2436
2444
  onChange,
2445
+ onDelete,
2437
2446
  blockTypes = coreBlocks,
2438
2447
  upload,
2439
2448
  resolveAssetUrl,
@@ -2447,11 +2456,12 @@ function BlockCanvas({
2447
2456
  () => ({ blockTypes, upload, resolveAssetUrl, onError }),
2448
2457
  [blockTypes, upload, resolveAssetUrl, onError]
2449
2458
  );
2450
- return /* @__PURE__ */ jsx26(QuoinProvider, { value: config, children: /* @__PURE__ */ jsx26(
2459
+ return /* @__PURE__ */ jsx27(QuoinProvider, { value: config, children: /* @__PURE__ */ jsx27(
2451
2460
  Canvas,
2452
2461
  {
2453
2462
  blocks,
2454
2463
  onChange,
2464
+ onDelete,
2455
2465
  blockTypes,
2456
2466
  readOnly,
2457
2467
  className,
@@ -2527,8 +2537,10 @@ function useAutosave({
2527
2537
  }
2528
2538
  export {
2529
2539
  BlockCanvas,
2540
+ FilePickerButton,
2530
2541
  QuoinProvider,
2531
2542
  TEXT_COMMANDS,
2543
+ UploadField,
2532
2544
  applyEdit,
2533
2545
  blockByType,
2534
2546
  caretRect,