@godxjp/block-editor 33.8.1 → 33.10.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
@@ -53,3 +53,30 @@ import { BlockEditor } from "@godxjp/block-editor";
53
53
  The grammar it reads and writes is specified in `@godxjp/markdown`'s README ("Grammar v1 and the
54
54
  codec"). The editor's schema is tested to round-trip every fixture exactly:
55
55
  `serialize(editor.getJSON()) === normalize(markdown)`.
56
+
57
+ ## RichTextEditor and TranslatableRichText — HTML fields
58
+
59
+ For columns that already hold **HTML** (a product description a storefront renders), the package
60
+ also ships a form field with a fixed toolbar — bold, italic, strike, inline code · H1–H3, bulleted /
61
+ numbered list, quote · undo, redo — and its per-locale form (gh#1365).
62
+
63
+ ```tsx
64
+ import { RichTextEditor, TranslatableRichText } from "@godxjp/block-editor";
65
+ import { FormField } from "@godxjp/ui/data-entry";
66
+
67
+ <FormField label="説明" required>
68
+ <TranslatableRichText value={description} onValueChange={setDescription} errors={errors} />
69
+ </FormField>;
70
+ ```
71
+
72
+ - The schema is the sanitiser: `value` (and a paste) is parsed into, and `onValueChange` emits, only
73
+ `p br h1 h2 h3 ul ol li blockquote strong em s code` (`RICH_TEXT_ALLOWED_TAGS`) with no
74
+ attributes except an integer `start` on `<ol>`. Anything else — script, links, images, styles, h4–h6, tables — is kept as its text.
75
+ An empty body is `""`.
76
+ - A `value` from outside (a reset, a locale tab) replaces the document without calling
77
+ `onValueChange`, and never while an IME composition is open.
78
+ - It takes FormField's `id` / `aria-*` on the editable; `status` / `aria-invalid` paint the frame.
79
+ - `TranslatableRichText` is the kit's `TranslatableField` engine around it — the same contract as
80
+ `TranslatableInput`: locales and first tab from AppProvider `contentLocales`, per-locale error
81
+ marks, the active error in FormField, and the fallback locale's text as an empty locale's
82
+ placeholder.
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 { FieldA11yProps, TranslatableLocaleProp } from '@godxjp/ui/data-entry';
3
4
 
4
5
  /**
5
6
  * The block editor's strings, under its own namespace (`blockEditor`), registered with the kit's
@@ -80,6 +81,9 @@ declare const BLOCK_EDITOR_MESSAGES: {
80
81
  readonly replaceImage: "画像を差し替え";
81
82
  readonly removeImage: "画像を削除";
82
83
  readonly properties: "プロパティ(YAML)";
84
+ readonly textStyle: "段落スタイル";
85
+ readonly undo: "元に戻す";
86
+ readonly redo: "やり直す";
83
87
  };
84
88
  readonly en: {
85
89
  readonly editor: "Editor";
@@ -154,6 +158,9 @@ declare const BLOCK_EDITOR_MESSAGES: {
154
158
  readonly replaceImage: "Replace image";
155
159
  readonly removeImage: "Remove image";
156
160
  readonly properties: "Properties (YAML)";
161
+ readonly textStyle: "Text style";
162
+ readonly undo: "Undo";
163
+ readonly redo: "Redo";
157
164
  };
158
165
  readonly vi: {
159
166
  readonly editor: "Trình soạn thảo";
@@ -228,6 +235,9 @@ declare const BLOCK_EDITOR_MESSAGES: {
228
235
  readonly replaceImage: "Thay ảnh";
229
236
  readonly removeImage: "Xoá ảnh";
230
237
  readonly properties: "Thuộc tính (YAML)";
238
+ readonly textStyle: "Kiểu đoạn văn";
239
+ readonly undo: "Hoàn tác";
240
+ readonly redo: "Làm lại";
231
241
  };
232
242
  };
233
243
  type BlockEditorLabels = {
@@ -348,4 +358,91 @@ declare function schemaExtensions(options: {
348
358
  toggleLabel?: (open: boolean) => string;
349
359
  }): Extensions;
350
360
 
351
- export { BLOCK_EDITOR_MESSAGES, BlockEditor, type BlockEditorAction, type BlockEditorApi, type BlockEditorHandle, type BlockEditorLabels, type BlockEditorProps, type BlockEditorUploadResult, type CalloutTitles, type WikilinkSuggestion, schemaExtensions };
361
+ /** The strings RichTextEditor renders — a subset of the block editor's `blockEditor.*` catalogue. */
362
+ type RichTextEditorLabels = Pick<BlockEditorLabels, "editor" | "formatting" | "bold" | "italic" | "strike" | "inlineCode" | "textStyle" | "heading1" | "heading2" | "heading3" | "bulletList" | "numberedList" | "quote" | "undo" | "redo">;
363
+ type RichTextEditorProps = FieldA11yProps & {
364
+ /**
365
+ * The body as HTML. Controlled with `onValueChange`; uncontrolled with `defaultValue`. Read through
366
+ * the editor's schema, so only the tags in {@link RICH_TEXT_ALLOWED_TAGS} survive.
367
+ */
368
+ value?: string;
369
+ defaultValue?: string;
370
+ /** Called with the schema's HTML after each edit — `""` for an empty body, never `<p></p>`. */
371
+ onValueChange?: (html: string) => void;
372
+ /** Shown while the body is empty. */
373
+ placeholder?: string;
374
+ /** Validation state the frame paints — as on Input. `error` also comes from `aria-invalid`. */
375
+ status?: "error" | "warning";
376
+ /** BCP-47 tag of the body's language (spell-check, screen-reader voice, CJK line breaking). */
377
+ lang?: string;
378
+ /** Override any string the toolbar renders (defaults: the kit's ja / en / vi catalogue). */
379
+ labels?: Partial<RichTextEditorLabels>;
380
+ disabled?: boolean;
381
+ readOnly?: boolean;
382
+ autoFocus?: boolean;
383
+ id?: string;
384
+ className?: string;
385
+ };
386
+ type RichTextEditorHandle = {
387
+ focus: () => void;
388
+ /** The Tiptap editor, for host integrations. */
389
+ editor: Editor | null;
390
+ };
391
+ /**
392
+ * Every element `onValueChange` can emit. It is the editor's SCHEMA, not a filter run afterwards:
393
+ * HTML in `value` (or pasted) is parsed into these nodes and marks, anything else is dropped to its
394
+ * text, and `getHTML()` writes only these back — with one attribute in all: `start` on an `<ol>`
395
+ * that does not start at 1, always an integer.
396
+ */
397
+ declare const RICH_TEXT_ALLOWED_TAGS: readonly ["p", "br", "h1", "h2", "h3", "ul", "ol", "li", "blockquote", "strong", "em", "s", "code"];
398
+ /**
399
+ * RichTextEditor (gh#1365) — a form field that edits HTML: a fixed toolbar (bold, italic, strike,
400
+ * inline code · paragraph / H1–H3 / bulleted / numbered list / quote · undo, redo) over a Tiptap
401
+ * surface whose schema is exactly {@link RICH_TEXT_ALLOWED_TAGS}, so the schema IS the sanitiser on
402
+ * the way in and on the way out.
403
+ *
404
+ * Controlled like the kit's other fields: a new `value` that is not what the editor last emitted
405
+ * replaces the document WITHOUT calling `onValueChange` (a locale tab, a form reset), and never
406
+ * while an IME composition is open. It takes FormField's `id` / `aria-*` on the editable itself.
407
+ */
408
+ declare const RichTextEditor: React.ForwardRefExoticComponent<FieldA11yProps & {
409
+ /**
410
+ * The body as HTML. Controlled with `onValueChange`; uncontrolled with `defaultValue`. Read through
411
+ * the editor's schema, so only the tags in {@link RICH_TEXT_ALLOWED_TAGS} survive.
412
+ */
413
+ value?: string;
414
+ defaultValue?: string;
415
+ /** Called with the schema's HTML after each edit — `""` for an empty body, never `<p></p>`. */
416
+ onValueChange?: (html: string) => void;
417
+ /** Shown while the body is empty. */
418
+ placeholder?: string;
419
+ /** Validation state the frame paints — as on Input. `error` also comes from `aria-invalid`. */
420
+ status?: "error" | "warning";
421
+ /** BCP-47 tag of the body's language (spell-check, screen-reader voice, CJK line breaking). */
422
+ lang?: string;
423
+ /** Override any string the toolbar renders (defaults: the kit's ja / en / vi catalogue). */
424
+ labels?: Partial<RichTextEditorLabels>;
425
+ disabled?: boolean;
426
+ readOnly?: boolean;
427
+ autoFocus?: boolean;
428
+ id?: string;
429
+ className?: string;
430
+ } & React.RefAttributes<RichTextEditorHandle>>;
431
+
432
+ type TranslatableRichTextProps = Omit<RichTextEditorProps, "value" | "defaultValue" | "onValueChange" | "lang"> & TranslatableLocaleProp & {
433
+ /** The tab strip's tier, as on TranslatableTextarea (`sm` tabs; `md` for `lg`). */
434
+ size?: "sm" | "md" | "lg";
435
+ };
436
+ /**
437
+ * TranslatableRichText (gh#1365) — ONE {@link RichTextEditor} holding an HTML value per locale
438
+ * (`{ ja, en, vi }`), switched by locale tabs: the kit's `TranslatableField` engine, the same as
439
+ * `TranslatableInput` / `TranslatableTextarea` (locales and the first tab from AppProvider
440
+ * `contentLocales`, fallback placeholder, per-locale error marks, FormField error report). An empty
441
+ * locale shows the fallback locale's TEXT as its placeholder. `ref` is the editor's handle.
442
+ */
443
+ declare const TranslatableRichText: React.ForwardRefExoticComponent<Omit<RichTextEditorProps, "value" | "lang" | "defaultValue" | "onValueChange"> & TranslatableLocaleProp & {
444
+ /** The tab strip's tier, as on TranslatableTextarea (`sm` tabs; `md` for `lg`). */
445
+ size?: "sm" | "md" | "lg";
446
+ } & React.RefAttributes<RichTextEditorHandle>>;
447
+
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 };
package/dist/index.js CHANGED
@@ -941,7 +941,10 @@ var BLOCK_EDITOR_MESSAGES = {
941
941
  imageTitle: "\u30BF\u30A4\u30C8\u30EB\uFF08\u30C4\u30FC\u30EB\u30C1\u30C3\u30D7\uFF09",
942
942
  replaceImage: "\u753B\u50CF\u3092\u5DEE\u3057\u66FF\u3048",
943
943
  removeImage: "\u753B\u50CF\u3092\u524A\u9664",
944
- properties: "\u30D7\u30ED\u30D1\u30C6\u30A3\uFF08YAML\uFF09"
944
+ properties: "\u30D7\u30ED\u30D1\u30C6\u30A3\uFF08YAML\uFF09",
945
+ textStyle: "\u6BB5\u843D\u30B9\u30BF\u30A4\u30EB",
946
+ undo: "\u5143\u306B\u623B\u3059",
947
+ redo: "\u3084\u308A\u76F4\u3059"
945
948
  },
946
949
  en: {
947
950
  editor: "Editor",
@@ -1015,7 +1018,10 @@ var BLOCK_EDITOR_MESSAGES = {
1015
1018
  imageTitle: "Title (tooltip)",
1016
1019
  replaceImage: "Replace image",
1017
1020
  removeImage: "Remove image",
1018
- properties: "Properties (YAML)"
1021
+ properties: "Properties (YAML)",
1022
+ textStyle: "Text style",
1023
+ undo: "Undo",
1024
+ redo: "Redo"
1019
1025
  },
1020
1026
  vi: {
1021
1027
  editor: "Tr\xECnh so\u1EA1n th\u1EA3o",
@@ -1089,7 +1095,10 @@ var BLOCK_EDITOR_MESSAGES = {
1089
1095
  imageTitle: "Ti\xEAu \u0111\u1EC1 (ch\xFA gi\u1EA3i)",
1090
1096
  replaceImage: "Thay \u1EA3nh",
1091
1097
  removeImage: "Xo\xE1 \u1EA3nh",
1092
- properties: "Thu\u1ED9c t\xEDnh (YAML)"
1098
+ properties: "Thu\u1ED9c t\xEDnh (YAML)",
1099
+ textStyle: "Ki\u1EC3u \u0111o\u1EA1n v\u0103n",
1100
+ undo: "Ho\xE0n t\xE1c",
1101
+ redo: "L\xE0m l\u1EA1i"
1093
1102
  }
1094
1103
  };
1095
1104
  var registered = false;
@@ -1221,27 +1230,29 @@ import { ToggleGroup, ToggleGroupItem } from "@godxjp/ui/data-entry";
1221
1230
  import { Button as Button2 } from "@godxjp/ui/general";
1222
1231
  import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
1223
1232
  var MARKS = ["bold", "italic", "strike", "code"];
1233
+ var activeMarks = (editor) => MARKS.filter((mark) => editor.isActive(mark));
1234
+ function applyMarks(editor, current, next) {
1235
+ const chain = editor.chain().focus();
1236
+ for (const mark of MARKS) {
1237
+ const on = next.includes(mark);
1238
+ if (on === current.includes(mark)) continue;
1239
+ if (mark === "bold") chain.toggleBold();
1240
+ else if (mark === "italic") chain.toggleItalic();
1241
+ else if (mark === "strike") chain.toggleStrike();
1242
+ else chain.toggleCode();
1243
+ }
1244
+ chain.run();
1245
+ }
1224
1246
  function FormatToolbar({ editor, onEditLink }) {
1225
1247
  const label = useLabel();
1226
1248
  const state = useEditorState({
1227
1249
  editor,
1228
1250
  selector: ({ editor: e }) => ({
1229
- marks: MARKS.filter((mark) => e.isActive(mark)),
1251
+ marks: activeMarks(e),
1230
1252
  link: e.getAttributes("link").href ?? null
1231
1253
  })
1232
1254
  });
1233
- const toggle = (next) => {
1234
- const chain = editor.chain().focus();
1235
- for (const mark of MARKS) {
1236
- const on = next.includes(mark);
1237
- if (on === state.marks.includes(mark)) continue;
1238
- if (mark === "bold") chain.toggleBold();
1239
- else if (mark === "italic") chain.toggleItalic();
1240
- else if (mark === "strike") chain.toggleStrike();
1241
- else chain.toggleCode();
1242
- }
1243
- chain.run();
1244
- };
1255
+ const toggle = (next) => applyMarks(editor, state.marks, next);
1245
1256
  return /* @__PURE__ */ jsx4(
1246
1257
  BubbleMenu,
1247
1258
  {
@@ -2333,8 +2344,354 @@ var BlockEditor = React6.forwardRef(
2333
2344
  ) });
2334
2345
  }
2335
2346
  );
2347
+
2348
+ // src/rich-text-editor.tsx
2349
+ import * as React7 from "react";
2350
+ import { EditorContent as EditorContent2, useEditor as useEditor2, useEditorState as useEditorState2 } from "@tiptap/react";
2351
+ import StarterKit2 from "@tiptap/starter-kit";
2352
+ import { OrderedList } from "@tiptap/extension-list";
2353
+ import { Placeholder as Placeholder2 } from "@tiptap/extensions";
2354
+ import {
2355
+ Bold as Bold2,
2356
+ Code as Code3,
2357
+ Heading1 as Heading12,
2358
+ Heading2 as Heading22,
2359
+ Heading3 as Heading32,
2360
+ Italic as Italic2,
2361
+ List as List2,
2362
+ ListOrdered as ListOrdered2,
2363
+ Redo2,
2364
+ Strikethrough as Strikethrough2,
2365
+ TextQuote,
2366
+ Undo2
2367
+ } from "lucide-react";
2368
+ import { Toggle } from "@godxjp/ui/data-entry";
2369
+ import { Button as Button5 } from "@godxjp/ui/general";
2370
+ import { Flex as Flex2 } from "@godxjp/ui/layout";
2371
+ import { useTranslation as useTranslation2 } from "@godxjp/ui/i18n";
2372
+ import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
2373
+ var RICH_TEXT_ALLOWED_TAGS = [
2374
+ "p",
2375
+ "br",
2376
+ "h1",
2377
+ "h2",
2378
+ "h3",
2379
+ "ul",
2380
+ "ol",
2381
+ "li",
2382
+ "blockquote",
2383
+ "strong",
2384
+ "em",
2385
+ "s",
2386
+ "code"
2387
+ ];
2388
+ var OrderedListStartOnly = OrderedList.extend({
2389
+ addAttributes() {
2390
+ return {
2391
+ start: {
2392
+ default: 1,
2393
+ parseHTML: (element) => {
2394
+ const start = Number.parseInt(element.getAttribute("start") ?? "", 10);
2395
+ return Number.isSafeInteger(start) ? start : 1;
2396
+ }
2397
+ }
2398
+ };
2399
+ }
2400
+ });
2401
+ var richTextExtensions = (placeholder) => [
2402
+ StarterKit2.configure({
2403
+ heading: { levels: [1, 2, 3] },
2404
+ codeBlock: false,
2405
+ horizontalRule: false,
2406
+ link: false,
2407
+ underline: false,
2408
+ trailingNode: false,
2409
+ orderedList: false
2410
+ }),
2411
+ OrderedListStartOnly,
2412
+ Placeholder2.configure({ placeholder })
2413
+ ];
2414
+ var activeBlock = (editor) => {
2415
+ for (const level of [1, 2, 3]) {
2416
+ if (editor.isActive("heading", { level })) return `heading${level}`;
2417
+ }
2418
+ if (editor.isActive("bulletList")) return "bulletList";
2419
+ if (editor.isActive("orderedList")) return "orderedList";
2420
+ if (editor.isActive("blockquote")) return "quote";
2421
+ return "";
2422
+ };
2423
+ function applyBlock(editor, next) {
2424
+ const chain = editor.chain().focus().clearNodes();
2425
+ if (next === "heading1") chain.setHeading({ level: 1 });
2426
+ else if (next === "heading2") chain.setHeading({ level: 2 });
2427
+ else if (next === "heading3") chain.setHeading({ level: 3 });
2428
+ else if (next === "bulletList") chain.toggleBulletList();
2429
+ else if (next === "orderedList") chain.toggleOrderedList();
2430
+ else if (next === "quote") chain.toggleBlockquote();
2431
+ chain.run();
2432
+ }
2433
+ var htmlOf = (editor) => editor.isEmpty ? "" : editor.getHTML();
2434
+ var RichTextEditor = React7.forwardRef(
2435
+ function RichTextEditor2({
2436
+ value: valueProp,
2437
+ defaultValue = "",
2438
+ onValueChange,
2439
+ placeholder,
2440
+ status,
2441
+ lang,
2442
+ labels,
2443
+ disabled = false,
2444
+ readOnly = false,
2445
+ autoFocus = false,
2446
+ id,
2447
+ className,
2448
+ "aria-label": ariaLabel,
2449
+ "aria-labelledby": ariaLabelledby,
2450
+ "aria-describedby": ariaDescribedby,
2451
+ "aria-errormessage": ariaErrormessage,
2452
+ "aria-invalid": ariaInvalid,
2453
+ "aria-required": ariaRequired,
2454
+ "data-field": dataField
2455
+ }, ref) {
2456
+ ensureBlockEditorMessages();
2457
+ const { t } = useTranslation2();
2458
+ const label = (key) => labels?.[key] ?? t(`blockEditor.${key}`);
2459
+ const onValueChangeRef = React7.useRef(onValueChange);
2460
+ onValueChangeRef.current = onValueChange;
2461
+ const placeholderRef = React7.useRef(placeholder ?? "");
2462
+ placeholderRef.current = placeholder ?? "";
2463
+ const initial = valueProp ?? defaultValue;
2464
+ const lastHtml = React7.useRef(initial);
2465
+ const extensions = React7.useMemo(() => richTextExtensions(() => placeholderRef.current), []);
2466
+ const invalid = status === "error" || ariaInvalid === true || ariaInvalid === "true";
2467
+ const editor = useEditor2({
2468
+ extensions,
2469
+ content: initial,
2470
+ editable: !disabled && !readOnly,
2471
+ autofocus: autoFocus ? "end" : false,
2472
+ immediatelyRender: false,
2473
+ editorProps: {
2474
+ // Re-applied on every render by useEditor, so `lang`, the FormField ids and the error state
2475
+ // follow the props (a locale tab switches `lang` on this same node).
2476
+ attributes: Object.fromEntries(
2477
+ Object.entries({
2478
+ class: "ui-prose ui-block-editor-content",
2479
+ role: "textbox",
2480
+ "aria-multiline": "true",
2481
+ id,
2482
+ lang,
2483
+ "aria-label": ariaLabelledby ? void 0 : ariaLabel ?? label("editor"),
2484
+ "aria-labelledby": ariaLabelledby,
2485
+ "aria-describedby": ariaDescribedby,
2486
+ "aria-errormessage": ariaErrormessage,
2487
+ "aria-invalid": invalid ? "true" : void 0,
2488
+ "aria-required": ariaRequired ? String(ariaRequired) : void 0,
2489
+ "aria-placeholder": placeholder || void 0,
2490
+ "aria-readonly": readOnly ? "true" : void 0,
2491
+ "aria-disabled": disabled ? "true" : void 0,
2492
+ "data-field": dataField
2493
+ }).filter((entry) => entry[1] !== void 0)
2494
+ )
2495
+ },
2496
+ onUpdate: ({ editor: instance }) => {
2497
+ const html = htmlOf(instance);
2498
+ if (html === lastHtml.current) return;
2499
+ lastHtml.current = html;
2500
+ onValueChangeRef.current?.(html);
2501
+ }
2502
+ });
2503
+ React7.useEffect(() => {
2504
+ if (!editor || valueProp === void 0) return;
2505
+ if (valueProp === lastHtml.current || editor.view.composing) return;
2506
+ lastHtml.current = valueProp;
2507
+ editor.commands.setContent(valueProp, { emitUpdate: false });
2508
+ }, [editor, valueProp]);
2509
+ React7.useEffect(() => {
2510
+ editor?.setEditable(!disabled && !readOnly);
2511
+ }, [editor, disabled, readOnly]);
2512
+ React7.useImperativeHandle(ref, () => ({ focus: () => editor?.commands.focus(), editor }), [
2513
+ editor
2514
+ ]);
2515
+ return /* @__PURE__ */ jsxs7(
2516
+ "div",
2517
+ {
2518
+ className: ["ui-rich-text-editor", className].filter(Boolean).join(" "),
2519
+ "data-status": invalid ? "error" : status,
2520
+ "data-disabled": disabled ? "" : void 0,
2521
+ children: [
2522
+ editor && !readOnly ? /* @__PURE__ */ jsx8(RichTextToolbar, { editor, disabled, label }) : null,
2523
+ /* @__PURE__ */ jsx8(EditorContent2, { editor })
2524
+ ]
2525
+ }
2526
+ );
2527
+ }
2528
+ );
2529
+ function RichTextToolbar({
2530
+ editor,
2531
+ disabled,
2532
+ label
2533
+ }) {
2534
+ const state = useEditorState2({
2535
+ editor,
2536
+ selector: ({ editor: e }) => ({
2537
+ marks: activeMarks(e),
2538
+ block: activeBlock(e),
2539
+ canUndo: e.can().undo(),
2540
+ canRedo: e.can().redo()
2541
+ })
2542
+ });
2543
+ const [focusKey, setFocusKey] = React7.useState("bold");
2544
+ const enabledKeys = disabled ? [] : [
2545
+ "bold",
2546
+ "italic",
2547
+ "strike",
2548
+ "code",
2549
+ "heading1",
2550
+ "heading2",
2551
+ "heading3",
2552
+ "bulletList",
2553
+ "orderedList",
2554
+ "quote",
2555
+ ...state.canUndo ? ["undo"] : [],
2556
+ ...state.canRedo ? ["redo"] : []
2557
+ ];
2558
+ const tabStop = enabledKeys.includes(focusKey) ? focusKey : enabledKeys[0];
2559
+ const focusProps = (key) => ({
2560
+ "data-command": key,
2561
+ tabIndex: key === tabStop ? 0 : -1,
2562
+ onFocus: () => setFocusKey(key)
2563
+ });
2564
+ const marks = [
2565
+ ["bold", "bold", Bold2],
2566
+ ["italic", "italic", Italic2],
2567
+ ["strike", "strike", Strikethrough2],
2568
+ ["code", "inlineCode", Code3]
2569
+ ];
2570
+ const blocks = [
2571
+ [
2572
+ ["heading1", "heading1", Heading12],
2573
+ ["heading2", "heading2", Heading22],
2574
+ ["heading3", "heading3", Heading32]
2575
+ ],
2576
+ [
2577
+ ["bulletList", "bulletList", List2],
2578
+ ["orderedList", "numberedList", ListOrdered2],
2579
+ ["quote", "quote", TextQuote]
2580
+ ]
2581
+ ];
2582
+ return /* @__PURE__ */ jsxs7(
2583
+ "div",
2584
+ {
2585
+ className: "ui-rich-text-editor-toolbar",
2586
+ role: "toolbar",
2587
+ "aria-label": label("formatting"),
2588
+ "aria-orientation": "horizontal",
2589
+ onKeyDown: (event) => {
2590
+ if (event.altKey || event.ctrlKey || event.metaKey || !enabledKeys.length) return;
2591
+ const rtl = getComputedStyle(event.currentTarget).direction === "rtl";
2592
+ const index = enabledKeys.indexOf(event.target.dataset.command ?? "");
2593
+ let next;
2594
+ if (event.key === "Home") next = 0;
2595
+ else if (event.key === "End") next = enabledKeys.length - 1;
2596
+ else if (event.key === "ArrowRight") next = index + (rtl ? -1 : 1);
2597
+ else if (event.key === "ArrowLeft") next = index + (rtl ? 1 : -1);
2598
+ else return;
2599
+ event.preventDefault();
2600
+ const key = enabledKeys[(next + enabledKeys.length) % enabledKeys.length];
2601
+ event.currentTarget.querySelector(`[data-command="${key}"]`)?.focus();
2602
+ },
2603
+ children: [
2604
+ /* @__PURE__ */ jsx8(Flex2, { direction: "row", gap: "xs", children: marks.map(([key, labelKey, Icon]) => /* @__PURE__ */ jsx8(
2605
+ Toggle,
2606
+ {
2607
+ type: "button",
2608
+ size: "sm",
2609
+ variant: "quiet",
2610
+ "aria-label": label(labelKey),
2611
+ title: label(labelKey),
2612
+ pressed: state.marks.includes(key),
2613
+ onPressedChange: (pressed) => applyMarks(
2614
+ editor,
2615
+ state.marks,
2616
+ pressed ? [...state.marks, key] : state.marks.filter((mark) => mark !== key)
2617
+ ),
2618
+ disabled,
2619
+ ...focusProps(key),
2620
+ children: /* @__PURE__ */ jsx8(Icon, { "aria-hidden": "true" })
2621
+ },
2622
+ key
2623
+ )) }),
2624
+ blocks.map((group, index) => /* @__PURE__ */ jsx8(Flex2, { direction: "row", gap: "xs", children: group.map(([key, labelKey, Icon]) => /* @__PURE__ */ jsx8(
2625
+ Toggle,
2626
+ {
2627
+ type: "button",
2628
+ size: "sm",
2629
+ variant: "quiet",
2630
+ "aria-label": label(labelKey),
2631
+ title: label(labelKey),
2632
+ pressed: state.block === key,
2633
+ onPressedChange: (pressed) => applyBlock(editor, pressed ? key : ""),
2634
+ disabled,
2635
+ ...focusProps(key),
2636
+ children: /* @__PURE__ */ jsx8(Icon, { "aria-hidden": "true" })
2637
+ },
2638
+ key
2639
+ )) }, index)),
2640
+ /* @__PURE__ */ jsxs7(Flex2, { direction: "row", gap: "xs", children: [
2641
+ /* @__PURE__ */ jsx8(
2642
+ Button5,
2643
+ {
2644
+ type: "button",
2645
+ size: "icon-sm",
2646
+ variant: "ghost",
2647
+ "aria-label": label("undo"),
2648
+ title: label("undo"),
2649
+ disabled: disabled || !state.canUndo,
2650
+ onClick: () => editor.chain().focus().undo().run(),
2651
+ ...focusProps("undo"),
2652
+ children: /* @__PURE__ */ jsx8(Undo2, { "aria-hidden": "true" })
2653
+ }
2654
+ ),
2655
+ /* @__PURE__ */ jsx8(
2656
+ Button5,
2657
+ {
2658
+ type: "button",
2659
+ size: "icon-sm",
2660
+ variant: "ghost",
2661
+ "aria-label": label("redo"),
2662
+ title: label("redo"),
2663
+ disabled: disabled || !state.canRedo,
2664
+ onClick: () => editor.chain().focus().redo().run(),
2665
+ ...focusProps("redo"),
2666
+ children: /* @__PURE__ */ jsx8(Redo2, { "aria-hidden": "true" })
2667
+ }
2668
+ )
2669
+ ] })
2670
+ ]
2671
+ }
2672
+ );
2673
+ }
2674
+
2675
+ // src/translatable-rich-text.tsx
2676
+ import * as React8 from "react";
2677
+ import { TranslatableField } from "@godxjp/ui/data-entry";
2678
+ import { jsx as jsx9 } from "react/jsx-runtime";
2679
+ var htmlToText = (html) => typeof DOMParser === "undefined" ? html.replace(/<[^>]*>/g, "").trim() : new DOMParser().parseFromString(html, "text/html").body.textContent?.trim() ?? "";
2680
+ var TranslatableRichText = React8.forwardRef(function TranslatableRichText2(props, ref) {
2681
+ return /* @__PURE__ */ jsx9(
2682
+ TranslatableField,
2683
+ {
2684
+ ...props,
2685
+ toPlaceholder: htmlToText,
2686
+ renderControl: ({ size: _size, ...control }) => /* @__PURE__ */ jsx9(RichTextEditor, { ...control, ref })
2687
+ }
2688
+ );
2689
+ });
2336
2690
  export {
2337
2691
  BLOCK_EDITOR_MESSAGES,
2338
2692
  BlockEditor,
2693
+ RICH_TEXT_ALLOWED_TAGS,
2694
+ RichTextEditor,
2695
+ TranslatableRichText,
2339
2696
  schemaExtensions
2340
2697
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@godxjp/block-editor",
3
- "version": "33.8.1",
3
+ "version": "33.10.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.8.1",
49
- "@godxjp/ui": "~33.8.1",
48
+ "@godxjp/markdown": "~33.10.1",
49
+ "@godxjp/ui": "~33.10.1",
50
50
  "react": ">=19.0.0",
51
51
  "react-dom": ">=19.0.0"
52
52
  },