@godxjp/block-editor 33.20.0 → 33.22.1

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
@@ -33,22 +33,47 @@ import { BlockEditor } from "@godxjp/block-editor";
33
33
  paste is read as Markdown.
34
34
  - **Uploads**: paste / drop / pick a file → an inline placeholder with progress; failure offers
35
35
  Retry / Remove; deleting the placeholder aborts the host's `signal`.
36
- - **Lossless**: anything the editor does not model (raw HTML, footnotes, an unknown directive) is a
37
- raw block showing its source, editable as source.
36
+ - **Directive blocks**: a container directive the kit does not model (`:::poll[…]{…}`,
37
+ `::::form`) is ONE atomic block — dragged, deleted and inserted whole — showing the host's preview
38
+ for its name (`directives`), or its source; "Edit source" edits its Markdown. A host `/` row
39
+ inserts one with `api.insertMarkdown(skeleton)`.
40
+ - **Lossless**: anything else the editor does not model (raw HTML, footnotes, a malformed toggle or
41
+ column row) is a raw block showing its source, editable as source.
38
42
  - **IME-safe** (menus read the committed document), **ja / en / vi**, a named `textbox` with the
39
43
  WAI-ARIA popup wiring on the `/` and `[[` lists, the kit's Prose as the editing surface.
40
44
 
41
45
  ## Contract
42
46
 
43
- | prop | |
44
- | ------------------------------------------------------------------------ | ------------------------------------------------------- |
45
- | `value` / `defaultValue` / `onValueChange` | Markdown in, canonical Markdown out |
46
- | `upload(file, { signal })` | `→ { url, name? }` — the same shape as `MarkdownEditor` |
47
- | `pickMedia()` | `→ { url, name? } \| null` — the host's library |
48
- | `suggestWikilinks(query)` | `→ { target, label?, heading?, description? }[]` |
49
- | `renderEmbed(target)` | a block embed `![[target]]` |
50
- | `resolveUrl(url)` | display URL for a stored one (`asset:<id>`) |
51
- | `actions`, `labels`, `disabled`, `readOnly`, `autoFocus`, `aria-*`, `id` | |
47
+ | prop | |
48
+ | ------------------------------------------------------------------------ | -------------------------------------------------------- |
49
+ | `value` / `defaultValue` / `onValueChange` | Markdown in, canonical Markdown out |
50
+ | `upload(file, { signal })` | `→ { url, name? }` — the same shape as `MarkdownEditor` |
51
+ | `pickMedia()` | `→ { url, name? } \| null` — the host's library |
52
+ | `suggestWikilinks(query)` | `→ { target, label?, heading?, description? }[]` |
53
+ | `renderEmbed(target)` | a block embed `![[target]]` |
54
+ | `resolveUrl(url)` | display URL for a stored one (`asset:<id>`) |
55
+ | `directives` | `{ [name]: (block) => ReactNode \| undefined }` previews |
56
+ | `actions`, `labels`, `disabled`, `readOnly`, `autoFocus`, `aria-*`, `id` | |
57
+
58
+ ```tsx
59
+ // A poll is a block: the host previews it and inserts its skeleton from the `/` menu.
60
+ <BlockEditor
61
+ value={body}
62
+ onValueChange={setBody}
63
+ directives={{
64
+ // block: { name, label, attributes, source, body } — the codec's canonical Markdown
65
+ poll: (block) => <PollPreview question={block.label} options={block.body} />,
66
+ }}
67
+ actions={[
68
+ {
69
+ key: "poll",
70
+ label: "Poll",
71
+ icon: <ListChecks />,
72
+ run: (api) => api.insertMarkdown(":::poll[Question]{id=poll-1}\n- Option A\n- Option B\n:::"),
73
+ },
74
+ ]}
75
+ />
76
+ ```
52
77
 
53
78
  The grammar it reads and writes is specified in `@godxjp/markdown`'s README ("Grammar v1 and the
54
79
  codec"). The editor's schema is tested to round-trip every fixture exactly:
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import * as React from 'react';
2
2
  import { Editor, Node, Extensions } from '@tiptap/core';
3
+ import { DirectiveBlock } from '@godxjp/markdown/codec';
3
4
  import { FieldA11yProps, TranslatableLocaleProp } from '@godxjp/ui/data-entry';
4
5
 
5
6
  /**
@@ -55,6 +56,7 @@ declare const BLOCK_EDITOR_MESSAGES: {
55
56
  readonly editSource: "Markdown を編集";
56
57
  readonly done: "完了";
57
58
  readonly embed: "埋め込み: {target}";
59
+ readonly directive: "ブロック: {name}";
58
60
  readonly calloutNote: "メモ";
59
61
  readonly calloutTip: "ヒント";
60
62
  readonly calloutImportant: "重要";
@@ -132,6 +134,7 @@ declare const BLOCK_EDITOR_MESSAGES: {
132
134
  readonly editSource: "Edit Markdown";
133
135
  readonly done: "Done";
134
136
  readonly embed: "Embed: {target}";
137
+ readonly directive: "Block: {name}";
135
138
  readonly calloutNote: "Note";
136
139
  readonly calloutTip: "Tip";
137
140
  readonly calloutImportant: "Important";
@@ -209,6 +212,7 @@ declare const BLOCK_EDITOR_MESSAGES: {
209
212
  readonly editSource: "Sửa Markdown";
210
213
  readonly done: "Xong";
211
214
  readonly embed: "Nhúng: {target}";
215
+ readonly directive: "Khối: {name}";
212
216
  readonly calloutNote: "Ghi chú";
213
217
  readonly calloutTip: "Mẹo";
214
218
  readonly calloutImportant: "Quan trọng";
@@ -244,6 +248,9 @@ type BlockEditorLabels = {
244
248
  [K in keyof (typeof BLOCK_EDITOR_MESSAGES)["en"]]: string;
245
249
  };
246
250
 
251
+ /** Draws a directive block's preview in the editor; `undefined` shows its source instead. */
252
+ type BlockEditorDirectiveRenderer = (block: DirectiveBlock) => React.ReactNode | undefined;
253
+
247
254
  /** What the host's storage returns for one file — the same shape as MarkdownEditor's. */
248
255
  type BlockEditorUploadResult = {
249
256
  url: string;
@@ -293,8 +300,20 @@ type BlockEditorProps = {
293
300
  renderEmbed?: (target: string) => React.ReactNode;
294
301
  /** Resolves a stored image URL (`asset:<id>`) for display. The body keeps the original. */
295
302
  resolveUrl?: (url: string) => string | undefined;
296
- /** Extra `/` rows after the built-in blocks — the extension point for host block types. */
303
+ /**
304
+ * Extra `/` rows after the built-in blocks — the extension point for host block types. A row
305
+ * that inserts a directive skeleton (`api.insertMarkdown(":::poll[…]{…}\n- A\n- B\n:::")`)
306
+ * inserts a directive BLOCK.
307
+ */
297
308
  actions?: readonly BlockEditorAction[];
309
+ /**
310
+ * Previews for directive blocks, by name: `{ poll: (block) => <PollPreview … /> }`. A container
311
+ * directive the kit does not model (`:::poll[…]{…}`) is one block — dragged, deleted, inserted
312
+ * whole, its source edited behind "Edit source"; the renderer receives its parts
313
+ * (`DirectiveBlock`: name, label, attributes, canonical source, body). Without one — or when it
314
+ * returns `undefined` — the block shows its source.
315
+ */
316
+ directives?: Readonly<Record<string, BlockEditorDirectiveRenderer>>;
298
317
  /** Override any string the editor renders (defaults: the kit's ja / en / vi catalogue). */
299
318
  labels?: Partial<BlockEditorLabels>;
300
319
  /**
@@ -349,6 +368,7 @@ declare function schemaExtensions(options: {
349
368
  /** Node views the component owns (React); omitted, the nodes render as plain HTML. */
350
369
  embed?: Node;
351
370
  rawBlock?: Node;
371
+ directive?: Node;
352
372
  /** Resolves a stored image URL (`asset:<id>`) to one the browser can load; the doc keeps the original. */
353
373
  resolveUrl?: (url: string) => string | undefined;
354
374
  callout?: Node;
@@ -440,9 +460,9 @@ type TranslatableRichTextProps = Omit<RichTextEditorProps, "value" | "defaultVal
440
460
  * `contentLocales`, fallback placeholder, per-locale error marks, FormField error report). An empty
441
461
  * locale shows the fallback locale's TEXT as its placeholder. `ref` is the editor's handle.
442
462
  */
443
- declare const TranslatableRichText: React.ForwardRefExoticComponent<Omit<RichTextEditorProps, "value" | "lang" | "defaultValue" | "onValueChange"> & TranslatableLocaleProp & {
463
+ declare const TranslatableRichText: React.ForwardRefExoticComponent<Omit<RichTextEditorProps, "value" | "defaultValue" | "lang" | "onValueChange"> & TranslatableLocaleProp & {
444
464
  /** The tab strip's tier, as on TranslatableTextarea (`sm` tabs; `md` for `lg`). */
445
465
  size?: "sm" | "md" | "lg";
446
466
  } & React.RefAttributes<RichTextEditorHandle>>;
447
467
 
448
- export { BLOCK_EDITOR_MESSAGES, BlockEditor, type BlockEditorAction, type BlockEditorApi, type BlockEditorHandle, type BlockEditorLabels, type BlockEditorProps, type BlockEditorUploadResult, type CalloutTitles, RICH_TEXT_ALLOWED_TAGS, RichTextEditor, type RichTextEditorHandle, type RichTextEditorLabels, type RichTextEditorProps, TranslatableRichText, type TranslatableRichTextProps, type WikilinkSuggestion, schemaExtensions };
468
+ export { BLOCK_EDITOR_MESSAGES, BlockEditor, type BlockEditorAction, type BlockEditorApi, type BlockEditorDirectiveRenderer, type BlockEditorHandle, type BlockEditorLabels, type BlockEditorProps, type BlockEditorUploadResult, type CalloutTitles, RICH_TEXT_ALLOWED_TAGS, RichTextEditor, type RichTextEditorHandle, type RichTextEditorLabels, type RichTextEditorProps, TranslatableRichText, type TranslatableRichTextProps, type WikilinkSuggestion, schemaExtensions };
package/dist/index.js CHANGED
@@ -616,6 +616,36 @@ var Frontmatter = Node.create({
616
616
  ];
617
617
  }
618
618
  });
619
+ var Directive = Node.create({
620
+ name: "directive",
621
+ group: "block",
622
+ atom: true,
623
+ draggable: true,
624
+ addAttributes() {
625
+ return { name: { default: "" }, source: { default: "" } };
626
+ },
627
+ parseHTML() {
628
+ return [
629
+ {
630
+ tag: "pre[data-directive]",
631
+ getAttrs: (element) => ({
632
+ name: element.getAttribute("data-directive") ?? "",
633
+ source: element.textContent ?? ""
634
+ })
635
+ }
636
+ ];
637
+ },
638
+ renderHTML({ node, HTMLAttributes }) {
639
+ return [
640
+ "pre",
641
+ mergeAttributes(HTMLAttributes, {
642
+ "data-directive": node.attrs.name,
643
+ class: "ui-block-editor-raw-block"
644
+ }),
645
+ node.attrs.source
646
+ ];
647
+ }
648
+ });
619
649
  var RawBlock = Node.create({
620
650
  name: "rawBlock",
621
651
  group: "block",
@@ -685,6 +715,7 @@ function schemaExtensions(options) {
685
715
  RawInline,
686
716
  options.embed ?? Embed,
687
717
  options.rawBlock ?? RawBlock,
718
+ options.directive ?? Directive,
688
719
  options.frontmatter ?? Frontmatter,
689
720
  Placeholder.configure({
690
721
  includeChildren: true,
@@ -718,6 +749,7 @@ import {
718
749
  import * as React from "react";
719
750
  var LabelsContext = React.createContext((key) => key);
720
751
  var useLabel = () => React.useContext(LabelsContext);
752
+ var DirectivesContext = React.createContext(void 0);
721
753
 
722
754
  // src/handle.tsx
723
755
  import { jsx as jsx2, jsxs } from "react/jsx-runtime";
@@ -916,6 +948,7 @@ var BLOCK_EDITOR_MESSAGES = {
916
948
  editSource: "Markdown \u3092\u7DE8\u96C6",
917
949
  done: "\u5B8C\u4E86",
918
950
  embed: "\u57CB\u3081\u8FBC\u307F: {target}",
951
+ directive: "\u30D6\u30ED\u30C3\u30AF: {name}",
919
952
  calloutNote: "\u30E1\u30E2",
920
953
  calloutTip: "\u30D2\u30F3\u30C8",
921
954
  calloutImportant: "\u91CD\u8981",
@@ -993,6 +1026,7 @@ var BLOCK_EDITOR_MESSAGES = {
993
1026
  editSource: "Edit Markdown",
994
1027
  done: "Done",
995
1028
  embed: "Embed: {target}",
1029
+ directive: "Block: {name}",
996
1030
  calloutNote: "Note",
997
1031
  calloutTip: "Tip",
998
1032
  calloutImportant: "Important",
@@ -1070,6 +1104,7 @@ var BLOCK_EDITOR_MESSAGES = {
1070
1104
  editSource: "S\u1EEDa Markdown",
1071
1105
  done: "Xong",
1072
1106
  embed: "Nh\xFAng: {target}",
1107
+ directive: "Kh\u1ED1i: {name}",
1073
1108
  calloutNote: "Ghi ch\xFA",
1074
1109
  calloutTip: "M\u1EB9o",
1075
1110
  calloutImportant: "Quan tr\u1ECDng",
@@ -1439,7 +1474,7 @@ import {
1439
1474
  ReactNodeViewRenderer
1440
1475
  } from "@tiptap/react";
1441
1476
  import { ChevronDown, LoaderCircle, TriangleAlert } from "lucide-react";
1442
- import { parse } from "@godxjp/markdown/codec";
1477
+ import { parse, readDirective } from "@godxjp/markdown/codec";
1443
1478
  import { Input as Input2, Select, Textarea } from "@godxjp/ui/data-entry";
1444
1479
  import {
1445
1480
  DropdownMenu as DropdownMenu2,
@@ -1504,6 +1539,68 @@ function RawBlockView({ node, editor, getPos, updateAttributes, selected }) {
1504
1539
  function rawBlockNode() {
1505
1540
  return RawBlock.extend({ addNodeView: () => ReactNodeViewRenderer(RawBlockView) });
1506
1541
  }
1542
+ function DirectiveView({ node, editor, getPos, updateAttributes, selected }) {
1543
+ const tr = useLabel();
1544
+ const previews = React5.useContext(DirectivesContext);
1545
+ const name = node.attrs.name;
1546
+ const source = node.attrs.source;
1547
+ const [editing, setEditing] = React5.useState(false);
1548
+ const [draft, setDraft] = React5.useState(source);
1549
+ const render = previews && Object.hasOwn(previews, name) ? previews[name] : void 0;
1550
+ const block = React5.useMemo(() => render ? readDirective(source) : null, [render, source]);
1551
+ const preview = render && block ? render(block) : void 0;
1552
+ const commit = () => {
1553
+ setEditing(false);
1554
+ const doc = parse(draft);
1555
+ const pos = getPos();
1556
+ const [only] = doc.content;
1557
+ if (doc.content.length === 1 && only.type === "directive") {
1558
+ updateAttributes(only.attrs);
1559
+ return;
1560
+ }
1561
+ if (typeof pos !== "number") return;
1562
+ editor.chain().focus().insertContentAt({ from: pos, to: pos + node.nodeSize }, doc.content).run();
1563
+ };
1564
+ return /* @__PURE__ */ jsxs5(
1565
+ NodeViewWrapper,
1566
+ {
1567
+ className: "ui-block-editor-raw-block",
1568
+ "data-directive": name,
1569
+ "data-selected": selected ? "" : void 0,
1570
+ children: [
1571
+ /* @__PURE__ */ jsxs5("div", { className: "ui-block-editor-raw-block-head", contentEditable: false, children: [
1572
+ /* @__PURE__ */ jsx6(Text, { size: "xs", tone: "muted", children: tr("directive", { name }) }),
1573
+ editor.isEditable ? /* @__PURE__ */ jsx6(
1574
+ Button4,
1575
+ {
1576
+ size: "xs",
1577
+ variant: "ghost",
1578
+ onClick: () => editing ? commit() : (setDraft(source), setEditing(true)),
1579
+ children: editing ? tr("done") : tr("editSource")
1580
+ }
1581
+ ) : null
1582
+ ] }),
1583
+ editing ? /* @__PURE__ */ jsx6(
1584
+ Textarea,
1585
+ {
1586
+ "aria-label": tr("directive", { name }),
1587
+ value: draft,
1588
+ rows: Math.min(12, Math.max(3, draft.split("\n").length)),
1589
+ onValueChange: setDraft,
1590
+ onKeyDown: (event) => {
1591
+ event.stopPropagation();
1592
+ if (event.key === "Escape") commit();
1593
+ },
1594
+ autoFocus: true
1595
+ }
1596
+ ) : preview !== void 0 ? /* @__PURE__ */ jsx6("div", { "data-directive-preview": "", contentEditable: false, children: preview }) : /* @__PURE__ */ jsx6("pre", { className: "ui-block-editor-raw-block-source", contentEditable: false, children: source })
1597
+ ]
1598
+ }
1599
+ );
1600
+ }
1601
+ function directiveNode() {
1602
+ return Directive.extend({ addNodeView: () => ReactNodeViewRenderer(DirectiveView) });
1603
+ }
1507
1604
  function FrontmatterView({ node, updateAttributes, editor, selected }) {
1508
1605
  const label = useLabel();
1509
1606
  const [editing, setEditing] = React5.useState(false);
@@ -1810,6 +1907,7 @@ var BlockEditor = React6.forwardRef(
1810
1907
  renderEmbed,
1811
1908
  resolveUrl,
1812
1909
  actions = [],
1910
+ directives,
1813
1911
  labels,
1814
1912
  appearance = "framed",
1815
1913
  disabled = false,
@@ -2090,6 +2188,7 @@ var BlockEditor = React6.forwardRef(
2090
2188
  },
2091
2189
  embed: embedNode(renderEmbed),
2092
2190
  rawBlock: rawBlockNode(),
2191
+ directive: directiveNode(),
2093
2192
  frontmatter: frontmatterNode(),
2094
2193
  callout: calloutNode(Callout),
2095
2194
  columns: columnsNode(Columns),
@@ -2261,7 +2360,7 @@ var BlockEditor = React6.forwardRef(
2261
2360
  }
2262
2361
  )
2263
2362
  ] }) : null,
2264
- /* @__PURE__ */ jsx7(EditorContent, { editor }),
2363
+ /* @__PURE__ */ jsx7(DirectivesContext.Provider, { value: directives, children: /* @__PURE__ */ jsx7(EditorContent, { editor }) }),
2265
2364
  /* @__PURE__ */ jsx7(
2266
2365
  SuggestionMenu,
2267
2366
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@godxjp/block-editor",
3
- "version": "33.20.0",
3
+ "version": "33.22.1",
4
4
  "description": "Block editor for GoDX apps (Notion / note.com style) on Tiptap: slash menu, block handles, floating format toolbar; reads and writes Markdown through @godxjp/markdown/codec, IME-safe, ja / en / vi.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -45,8 +45,8 @@
45
45
  "lucide-react": "^1.37.0"
46
46
  },
47
47
  "peerDependencies": {
48
- "@godxjp/markdown": "~33.20.0",
49
- "@godxjp/ui": "~33.20.0",
48
+ "@godxjp/markdown": "~33.22.1",
49
+ "@godxjp/ui": "~33.22.1",
50
50
  "react": ">=19.0.0",
51
51
  "react-dom": ">=19.0.0"
52
52
  },