quoin-editor 0.2.0 → 0.4.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
@@ -42,18 +42,11 @@ type BlockEditorProps<D = Record<string, unknown>> = {
42
42
  onDone?: () => void;
43
43
  };
44
44
 
45
- /** Describes one block type: how to label it, how to draw it at rest, how to edit it. */
46
- type BlockDefinition<D extends object = Record<string, unknown>> = {
45
+ /** The fields every block definition carries, whatever it can do with its data. */
46
+ type BlockDefinitionCommon<D extends object> = {
47
47
  type: string;
48
48
  label: string;
49
49
  icon: LucideIcon;
50
- editor: ComponentType<BlockEditorProps<D>>;
51
- /**
52
- * What the block shows when it is not being edited. Optional so a host can register an
53
- * edit-only block type, and overridable so a host can supply rich rendering (a full
54
- * markdown pipeline, a chart) without Quoin depending on one.
55
- */
56
- preview?: ComponentType<BlockPreviewProps<D>>;
57
50
  /**
58
51
  * Called when a new block of this type is inserted, to seed its data.
59
52
  * `NoInfer` keeps `D` inferred from `editor` and `preview` alone, so the seed is checked
@@ -69,6 +62,31 @@ type BlockDefinition<D extends object = Record<string, unknown>> = {
69
62
  */
70
63
  alwaysEditing?: boolean;
71
64
  };
65
+ /**
66
+ * Describes one block type: how to label it, how to draw it at rest, how to edit it.
67
+ *
68
+ * Supply `editor`, `preview`, or both, and the canvas does the rest. A definition with no
69
+ * `preview` is edit-only: a read-only canvas has nothing to draw for it but its label.
70
+ * A definition with no `editor` can never be edited in the canvas, which is what a block
71
+ * whose content belongs to a server-side record wants; the card offers no edit affordance
72
+ * for it and a click goes through to whatever the preview itself renders.
73
+ *
74
+ * Omitting both is a type error, because such a block would only ever draw its own label.
75
+ */
76
+ type BlockDefinition<D extends object = Record<string, unknown>> = (BlockDefinitionCommon<D> & {
77
+ editor: ComponentType<BlockEditorProps<D>>;
78
+ /**
79
+ * What the block shows when it is not being edited. Optional so a host can register an
80
+ * edit-only block type, and overridable so a host can supply rich rendering (a full
81
+ * markdown pipeline, a chart) without Quoin depending on one.
82
+ */
83
+ preview?: ComponentType<BlockPreviewProps<D>>;
84
+ }) | (BlockDefinitionCommon<D> & {
85
+ /** Omitted: this block is never editable in the canvas. */
86
+ editor?: undefined;
87
+ /** Required here, because it is the only thing that can draw the block. */
88
+ preview: ComponentType<BlockPreviewProps<D>>;
89
+ });
72
90
  /** A definition with its data type erased, so definitions of different shapes compose in one array. */
73
91
  type AnyBlockDefinition = BlockDefinition<Record<string, unknown>>;
74
92
  /**
@@ -77,8 +95,11 @@ type AnyBlockDefinition = BlockDefinition<Record<string, unknown>>;
77
95
  * on `editor`, so without it an array of differently-shaped definitions will not typecheck.
78
96
  * `initialData` is checked against the editor's data type. That relies on `NoInfer`, so
79
97
  * TypeScript 5.4 or later is required.
98
+ *
99
+ * `D` defaults to `Record<string, unknown>`, so a definition whose components take no
100
+ * annotation still gets a usable `data` rather than a bare `object`.
80
101
  */
81
- declare function defineBlock<D extends object>(definition: BlockDefinition<D>): AnyBlockDefinition;
102
+ declare function defineBlock<D extends object = Record<string, unknown>>(definition: BlockDefinition<D>): AnyBlockDefinition;
82
103
  /** The definition for a type, or undefined; the last registration of a type wins, so a host can override a core block. */
83
104
  declare function blockByType(definitions: AnyBlockDefinition[], type: string): AnyBlockDefinition | undefined;
84
105
  /** The definitions offered in the slash palette, in registration order, one per type, with the last registration of a type winning. */
@@ -132,6 +153,54 @@ declare function BlockCanvas({ blocks, onChange, onDelete, blockTypes, upload, r
132
153
  /** The block types Quoin ships with. Spread them alongside your own definitions. */
133
154
  declare const coreBlocks: AnyBlockDefinition[];
134
155
 
156
+ /**
157
+ * The `data` shape each core block keeps its content under. A host that persists blocks
158
+ * itself reads and writes these, so they are part of the public surface rather than an
159
+ * implementation detail. `core-data.test.tsx` holds every one of them against the editor
160
+ * that writes it and the seed it starts from.
161
+ *
162
+ * Every field is declared because something writes it. The optional ones are written
163
+ * later than the block's first save: an image's `caption` when the author types one, its
164
+ * `width` and `height` once the uploaded bytes have been measured, a file's `size` when
165
+ * the upload reports it.
166
+ */
167
+ /** `MARKDOWN`. The source text, not rendered HTML: Quoin stores and edits markdown. */
168
+ type MarkdownBlockData = {
169
+ markdown: string;
170
+ };
171
+ /** `CALLOUT`. The prose is under `body`, not `text`. */
172
+ type CalloutBlockData = {
173
+ tone: string;
174
+ body: string;
175
+ };
176
+ /** `CODE`. `language` is a free string, written through as the author typed it. */
177
+ type CodeBlockData = {
178
+ language: string;
179
+ code: string;
180
+ };
181
+ /** `IMAGE`. `storageKey` names the uploaded file; the host's `resolveAssetUrl` turns it into a URL. */
182
+ type ImageBlockData = {
183
+ storageKey: string;
184
+ alt: string;
185
+ caption?: string;
186
+ width?: number;
187
+ height?: number;
188
+ };
189
+ /** `FILE`. `storageKey` names the uploaded file; `filename` is what the author picked. */
190
+ type FileBlockData = {
191
+ storageKey: string;
192
+ filename: string;
193
+ size?: number;
194
+ };
195
+ /** Maps each core block's `type` string to its data shape. */
196
+ type CoreBlockData = {
197
+ MARKDOWN: MarkdownBlockData;
198
+ CALLOUT: CalloutBlockData;
199
+ CODE: CodeBlockData;
200
+ IMAGE: ImageBlockData;
201
+ FILE: FileBlockData;
202
+ };
203
+
135
204
  /** Props for `UploadField`; the upload function itself comes from the canvas, not from here. */
136
205
  type UploadFieldProps = {
137
206
  /** Names what the file is for, and is passed to the host's `upload` as its second argument. */
@@ -199,15 +268,16 @@ declare function useBlockList(initial: QuoinBlock[]): BlockList;
199
268
 
200
269
  /** Where a debounced save stands, for a "Saving..." or "Saved" indicator. */
201
270
  type AutosaveStatus = 'idle' | 'saving' | 'saved' | 'retrying';
202
- type SaveResult = {
203
- ok?: boolean;
204
- } | void;
205
271
  /** What `useAutosave` tracks, how to build the payload, and how to persist it. */
206
272
  type UseAutosaveInput<T> = {
207
273
  /** The tracked fields. An edit to any of them marks dirty and restarts the debounce. */
208
274
  deps: React.DependencyList;
209
275
  build: () => T;
210
- save: (payload: T) => Promise<SaveResult> | SaveResult;
276
+ /**
277
+ * Persists the payload. Return anything: a mutation or a `fetch` that resolves to a body
278
+ * can be passed straight in. A save fails by rejecting, or by resolving to `{ ok: false }`.
279
+ */
280
+ save: (payload: T) => unknown;
211
281
  debounceMs?: number;
212
282
  maxBackoffMs?: number;
213
283
  };
@@ -318,4 +388,4 @@ declare function useMarkdownKeymap({ commit, onOpenPalette, }: {
318
388
  onPaste: (event: React.ClipboardEvent<HTMLTextAreaElement>) => void;
319
389
  };
320
390
 
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 };
391
+ export { type AnyBlockDefinition, type AutosaveStatus, BlockCanvas, type BlockCanvasProps, type BlockDefinition, type BlockEditorProps, type BlockList, type BlockPreviewProps, type CalloutBlockData, type CaretRect, type CodeBlockData, type CoreBlockData, type Edit, type FileBlockData, FilePickerButton, type FilePickerButtonProps, type ImageBlockData, type InsertableCommand, type LinePrefix, type MarkdownBlockData, 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
@@ -42,18 +42,11 @@ type BlockEditorProps<D = Record<string, unknown>> = {
42
42
  onDone?: () => void;
43
43
  };
44
44
 
45
- /** Describes one block type: how to label it, how to draw it at rest, how to edit it. */
46
- type BlockDefinition<D extends object = Record<string, unknown>> = {
45
+ /** The fields every block definition carries, whatever it can do with its data. */
46
+ type BlockDefinitionCommon<D extends object> = {
47
47
  type: string;
48
48
  label: string;
49
49
  icon: LucideIcon;
50
- editor: ComponentType<BlockEditorProps<D>>;
51
- /**
52
- * What the block shows when it is not being edited. Optional so a host can register an
53
- * edit-only block type, and overridable so a host can supply rich rendering (a full
54
- * markdown pipeline, a chart) without Quoin depending on one.
55
- */
56
- preview?: ComponentType<BlockPreviewProps<D>>;
57
50
  /**
58
51
  * Called when a new block of this type is inserted, to seed its data.
59
52
  * `NoInfer` keeps `D` inferred from `editor` and `preview` alone, so the seed is checked
@@ -69,6 +62,31 @@ type BlockDefinition<D extends object = Record<string, unknown>> = {
69
62
  */
70
63
  alwaysEditing?: boolean;
71
64
  };
65
+ /**
66
+ * Describes one block type: how to label it, how to draw it at rest, how to edit it.
67
+ *
68
+ * Supply `editor`, `preview`, or both, and the canvas does the rest. A definition with no
69
+ * `preview` is edit-only: a read-only canvas has nothing to draw for it but its label.
70
+ * A definition with no `editor` can never be edited in the canvas, which is what a block
71
+ * whose content belongs to a server-side record wants; the card offers no edit affordance
72
+ * for it and a click goes through to whatever the preview itself renders.
73
+ *
74
+ * Omitting both is a type error, because such a block would only ever draw its own label.
75
+ */
76
+ type BlockDefinition<D extends object = Record<string, unknown>> = (BlockDefinitionCommon<D> & {
77
+ editor: ComponentType<BlockEditorProps<D>>;
78
+ /**
79
+ * What the block shows when it is not being edited. Optional so a host can register an
80
+ * edit-only block type, and overridable so a host can supply rich rendering (a full
81
+ * markdown pipeline, a chart) without Quoin depending on one.
82
+ */
83
+ preview?: ComponentType<BlockPreviewProps<D>>;
84
+ }) | (BlockDefinitionCommon<D> & {
85
+ /** Omitted: this block is never editable in the canvas. */
86
+ editor?: undefined;
87
+ /** Required here, because it is the only thing that can draw the block. */
88
+ preview: ComponentType<BlockPreviewProps<D>>;
89
+ });
72
90
  /** A definition with its data type erased, so definitions of different shapes compose in one array. */
73
91
  type AnyBlockDefinition = BlockDefinition<Record<string, unknown>>;
74
92
  /**
@@ -77,8 +95,11 @@ type AnyBlockDefinition = BlockDefinition<Record<string, unknown>>;
77
95
  * on `editor`, so without it an array of differently-shaped definitions will not typecheck.
78
96
  * `initialData` is checked against the editor's data type. That relies on `NoInfer`, so
79
97
  * TypeScript 5.4 or later is required.
98
+ *
99
+ * `D` defaults to `Record<string, unknown>`, so a definition whose components take no
100
+ * annotation still gets a usable `data` rather than a bare `object`.
80
101
  */
81
- declare function defineBlock<D extends object>(definition: BlockDefinition<D>): AnyBlockDefinition;
102
+ declare function defineBlock<D extends object = Record<string, unknown>>(definition: BlockDefinition<D>): AnyBlockDefinition;
82
103
  /** The definition for a type, or undefined; the last registration of a type wins, so a host can override a core block. */
83
104
  declare function blockByType(definitions: AnyBlockDefinition[], type: string): AnyBlockDefinition | undefined;
84
105
  /** The definitions offered in the slash palette, in registration order, one per type, with the last registration of a type winning. */
@@ -132,6 +153,54 @@ declare function BlockCanvas({ blocks, onChange, onDelete, blockTypes, upload, r
132
153
  /** The block types Quoin ships with. Spread them alongside your own definitions. */
133
154
  declare const coreBlocks: AnyBlockDefinition[];
134
155
 
156
+ /**
157
+ * The `data` shape each core block keeps its content under. A host that persists blocks
158
+ * itself reads and writes these, so they are part of the public surface rather than an
159
+ * implementation detail. `core-data.test.tsx` holds every one of them against the editor
160
+ * that writes it and the seed it starts from.
161
+ *
162
+ * Every field is declared because something writes it. The optional ones are written
163
+ * later than the block's first save: an image's `caption` when the author types one, its
164
+ * `width` and `height` once the uploaded bytes have been measured, a file's `size` when
165
+ * the upload reports it.
166
+ */
167
+ /** `MARKDOWN`. The source text, not rendered HTML: Quoin stores and edits markdown. */
168
+ type MarkdownBlockData = {
169
+ markdown: string;
170
+ };
171
+ /** `CALLOUT`. The prose is under `body`, not `text`. */
172
+ type CalloutBlockData = {
173
+ tone: string;
174
+ body: string;
175
+ };
176
+ /** `CODE`. `language` is a free string, written through as the author typed it. */
177
+ type CodeBlockData = {
178
+ language: string;
179
+ code: string;
180
+ };
181
+ /** `IMAGE`. `storageKey` names the uploaded file; the host's `resolveAssetUrl` turns it into a URL. */
182
+ type ImageBlockData = {
183
+ storageKey: string;
184
+ alt: string;
185
+ caption?: string;
186
+ width?: number;
187
+ height?: number;
188
+ };
189
+ /** `FILE`. `storageKey` names the uploaded file; `filename` is what the author picked. */
190
+ type FileBlockData = {
191
+ storageKey: string;
192
+ filename: string;
193
+ size?: number;
194
+ };
195
+ /** Maps each core block's `type` string to its data shape. */
196
+ type CoreBlockData = {
197
+ MARKDOWN: MarkdownBlockData;
198
+ CALLOUT: CalloutBlockData;
199
+ CODE: CodeBlockData;
200
+ IMAGE: ImageBlockData;
201
+ FILE: FileBlockData;
202
+ };
203
+
135
204
  /** Props for `UploadField`; the upload function itself comes from the canvas, not from here. */
136
205
  type UploadFieldProps = {
137
206
  /** Names what the file is for, and is passed to the host's `upload` as its second argument. */
@@ -199,15 +268,16 @@ declare function useBlockList(initial: QuoinBlock[]): BlockList;
199
268
 
200
269
  /** Where a debounced save stands, for a "Saving..." or "Saved" indicator. */
201
270
  type AutosaveStatus = 'idle' | 'saving' | 'saved' | 'retrying';
202
- type SaveResult = {
203
- ok?: boolean;
204
- } | void;
205
271
  /** What `useAutosave` tracks, how to build the payload, and how to persist it. */
206
272
  type UseAutosaveInput<T> = {
207
273
  /** The tracked fields. An edit to any of them marks dirty and restarts the debounce. */
208
274
  deps: React.DependencyList;
209
275
  build: () => T;
210
- save: (payload: T) => Promise<SaveResult> | SaveResult;
276
+ /**
277
+ * Persists the payload. Return anything: a mutation or a `fetch` that resolves to a body
278
+ * can be passed straight in. A save fails by rejecting, or by resolving to `{ ok: false }`.
279
+ */
280
+ save: (payload: T) => unknown;
211
281
  debounceMs?: number;
212
282
  maxBackoffMs?: number;
213
283
  };
@@ -318,4 +388,4 @@ declare function useMarkdownKeymap({ commit, onOpenPalette, }: {
318
388
  onPaste: (event: React.ClipboardEvent<HTMLTextAreaElement>) => void;
319
389
  };
320
390
 
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 };
391
+ export { type AnyBlockDefinition, type AutosaveStatus, BlockCanvas, type BlockCanvasProps, type BlockDefinition, type BlockEditorProps, type BlockList, type BlockPreviewProps, type CalloutBlockData, type CaretRect, type CodeBlockData, type CoreBlockData, type Edit, type FileBlockData, FilePickerButton, type FilePickerButtonProps, type ImageBlockData, type InsertableCommand, type LinePrefix, type MarkdownBlockData, 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
@@ -52,12 +52,12 @@ function blockByType(definitions, type) {
52
52
  return void 0;
53
53
  }
54
54
  function paletteBlocks(definitions) {
55
- const seen = /* @__PURE__ */ new Set();
55
+ const seen2 = /* @__PURE__ */ new Set();
56
56
  const out = [];
57
57
  for (let i = definitions.length - 1; i >= 0; i -= 1) {
58
58
  const d = definitions[i];
59
- if (seen.has(d.type)) continue;
60
- seen.add(d.type);
59
+ if (seen2.has(d.type)) continue;
60
+ seen2.add(d.type);
61
61
  if (d.inSlashPalette === false) continue;
62
62
  out.push(d);
63
63
  }
@@ -1478,12 +1478,12 @@ function FileBlockEditor({ data, onChange }) {
1478
1478
  // src/blocks/previews.tsx
1479
1479
  import { jsx as jsx18 } from "react/jsx-runtime";
1480
1480
  var HINT = "text-sm text-muted-foreground";
1481
- var TONE_BORDER = {
1482
- info: "border-primary",
1483
- tip: "border-accent-foreground",
1484
- warning: "border-muted-foreground",
1485
- success: "border-secondary-foreground",
1486
- danger: "border-destructive"
1481
+ var TONE = {
1482
+ info: "border-info bg-info/15",
1483
+ tip: "border-primary bg-primary/15",
1484
+ warning: "border-warning bg-warning/15",
1485
+ success: "border-success bg-success/15",
1486
+ danger: "border-destructive bg-destructive/15"
1487
1487
  };
1488
1488
  function MarkdownPreview({ data }) {
1489
1489
  const markdown = str(data.markdown);
@@ -1510,8 +1510,8 @@ function CalloutPreview({ data }) {
1510
1510
  const body = str(data.body);
1511
1511
  if (body.trim() === "") return /* @__PURE__ */ jsx18("p", { className: HINT, children: "Empty callout" });
1512
1512
  const tone = str(data.tone) || "info";
1513
- const border = TONE_BORDER[tone] ?? "border-border";
1514
- return /* @__PURE__ */ jsx18("div", { "data-tone": tone, className: `rounded-md border-l-4 ${border} bg-muted/50 p-3 text-sm`, children: body });
1513
+ const paint = TONE[tone] ?? "border-border bg-muted/50";
1514
+ return /* @__PURE__ */ jsx18("div", { "data-tone": tone, className: `rounded-md border-l-4 ${paint} p-3 text-sm`, children: body });
1515
1515
  }
1516
1516
 
1517
1517
  // src/blocks/core.ts
@@ -1825,9 +1825,8 @@ function BlockEditor({
1825
1825
  onDone
1826
1826
  }) {
1827
1827
  const { blockTypes } = useQuoin();
1828
- const definition = blockByType(blockTypes, type);
1829
- if (!definition) return null;
1830
- const Editor = definition.editor;
1828
+ const Editor = blockByType(blockTypes, type)?.editor;
1829
+ if (!Editor) return null;
1831
1830
  return /* @__PURE__ */ jsx21(Editor, { data, onChange, onInsertBlock, onDone });
1832
1831
  }
1833
1832
 
@@ -1876,6 +1875,15 @@ function sourceOffsetFor(source, probe) {
1876
1875
  return search(source, probe);
1877
1876
  }
1878
1877
 
1878
+ // src/dev-warn.ts
1879
+ var seen = /* @__PURE__ */ new Set();
1880
+ function devWarn(message) {
1881
+ if (typeof process !== "undefined" && process?.env?.NODE_ENV === "production") return;
1882
+ if (seen.has(message)) return;
1883
+ seen.add(message);
1884
+ console.warn(`Quoin: ${message}`);
1885
+ }
1886
+
1879
1887
  // src/canvas/BlockCard.tsx
1880
1888
  import { jsx as jsx22, jsxs as jsxs15 } from "react/jsx-runtime";
1881
1889
  var INTERACTIVE_SELECTOR = 'a, button, input, textarea, select, [role="button"], [tabindex], iframe';
@@ -1897,7 +1905,8 @@ function BlockCardImpl({
1897
1905
  const { blockTypes, onError } = useQuoin();
1898
1906
  const definition = blockByType(blockTypes, block.type);
1899
1907
  const Preview = definition?.preview;
1900
- const [editing, setEditing] = React8.useState(editingByDefault && !readOnly);
1908
+ const editable = Boolean(definition?.editor) && !readOnly;
1909
+ const [editing, setEditing] = React8.useState(editingByDefault && editable);
1901
1910
  const editButtonRef = React8.useRef(null);
1902
1911
  const editorContainerRef = React8.useRef(null);
1903
1912
  const returnFocusOnClose = React8.useRef(false);
@@ -1977,7 +1986,13 @@ function BlockCardImpl({
1977
1986
  reportedType.current = block.type;
1978
1987
  onError(new Error(`No block type is registered for "${block.type}".`));
1979
1988
  }, [definition, block.type, onError]);
1980
- if (definition?.alwaysEditing && !readOnly) {
1989
+ React8.useEffect(() => {
1990
+ if (!readOnly || !definition || definition.preview) return;
1991
+ devWarn(
1992
+ `the block type "${block.type}" has no \`preview\`, so a read-only canvas can draw only its label. Give its definition a \`preview\` to render the block itself.`
1993
+ );
1994
+ }, [readOnly, definition, block.type]);
1995
+ if (definition?.alwaysEditing && editable) {
1981
1996
  return /* @__PURE__ */ jsx22(
1982
1997
  BlockEditor,
1983
1998
  {
@@ -2017,7 +2032,7 @@ function BlockCardImpl({
2017
2032
  );
2018
2033
  }
2019
2034
  const rest = Preview ? /* @__PURE__ */ jsx22(Preview, { data: block.data, blockId: block.id }) : /* @__PURE__ */ jsx22("p", { className: "py-1 text-sm text-muted-foreground", children: definition?.label ?? block.type });
2020
- if (readOnly) return /* @__PURE__ */ jsx22("div", { children: rest });
2035
+ if (!editable) return /* @__PURE__ */ jsx22("div", { children: rest });
2021
2036
  return /* @__PURE__ */ jsxs15("div", { className: "group/card relative", children: [
2022
2037
  /* @__PURE__ */ jsx22(
2023
2038
  "div",
@@ -2473,6 +2488,9 @@ function BlockCanvas({
2473
2488
 
2474
2489
  // src/hooks/useAutosave.ts
2475
2490
  import * as React12 from "react";
2491
+ function reportedFailure(result) {
2492
+ return typeof result === "object" && result !== null && "ok" in result && result.ok === false;
2493
+ }
2476
2494
  function useAutosave({
2477
2495
  deps,
2478
2496
  build,
@@ -2496,7 +2514,7 @@ function useAutosave({
2496
2514
  setStatus("saving");
2497
2515
  try {
2498
2516
  const res = await saveRef.current(buildRef.current());
2499
- if (res && res.ok === false) throw new Error("save rejected");
2517
+ if (reportedFailure(res)) throw new Error("save rejected");
2500
2518
  retryRef.current = 0;
2501
2519
  dirtyRef.current = false;
2502
2520
  setStatus("saved");
@@ -2512,10 +2530,10 @@ function useAutosave({
2512
2530
  });
2513
2531
  const seenRef = React12.useRef(null);
2514
2532
  React12.useEffect(() => {
2515
- const seen = seenRef.current;
2533
+ const seen2 = seenRef.current;
2516
2534
  seenRef.current = deps;
2517
- if (seen === null) return;
2518
- const unchanged = seen.length === deps.length && seen.every((v, i) => v === deps[i]);
2535
+ if (seen2 === null) return;
2536
+ const unchanged = seen2.length === deps.length && seen2.every((v, i) => v === deps[i]);
2519
2537
  if (!unchanged) dirtyRef.current = true;
2520
2538
  }, deps);
2521
2539
  React12.useEffect(() => {