@sckyzo/cosmos-react 0.4.1 → 0.6.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.
@@ -6,11 +6,14 @@ import { useEditor, EditorContent, useEditorState } from '@tiptap/react';
6
6
  import { BubbleMenu } from '@tiptap/react/menus';
7
7
  import { Placeholder, CharacterCount } from '@tiptap/extensions';
8
8
  import StarterKit from '@tiptap/starter-kit';
9
+ import TextAlign from '@tiptap/extension-text-align';
9
10
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
10
- import { Redo2, Undo2, Link, Code, Quote, ListOrdered, List, Heading2, Strikethrough, Italic, Bold } from 'lucide-react';
11
- import { PluginKey, Plugin } from '@tiptap/pm/state';
11
+ import { Pilcrow, Heading1, Heading2, Heading3, Redo2, Undo2, Image as Image$1, Link, TextAlignStart, TextAlignCenter, TextAlignEnd, SquareCode, Code, Quote, ListOrdered, List, Strikethrough, Underline, Italic, Bold } from 'lucide-react';
12
+ import { Image } from '@tiptap/extension-image';
13
+ import { DOMParser } from '@tiptap/pm/model';
14
+ import { Plugin, PluginKey, NodeSelection, Selection } from '@tiptap/pm/state';
12
15
  import { Decoration, DecorationSet } from '@tiptap/pm/view';
13
- import { Popover, Tooltip, Button } from '@sckyzo/cosmos-react';
16
+ import { Menu, Popover, Tooltip, Button, Checkbox } from '@sckyzo/cosmos-react';
14
17
 
15
18
  // src/editor/RichTextEditor.tsx
16
19
  function useControllableState({
@@ -124,26 +127,86 @@ var RequiredMark = ({ srText }) => /* @__PURE__ */ jsxs(Fragment, { children: [
124
127
  /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "ms-0.5 text-red-700 dark:text-red-400", children: "*" }),
125
128
  srText && /* @__PURE__ */ jsx("span", { className: "sr-only", children: srText })
126
129
  ] });
130
+ var isSafeSource = (src) => typeof src === "string" && isHttpUrl(src);
131
+ var RichTextImage = Image.extend({
132
+ // Written out in full: `extend` calls `addOptions` again, and a `configure` before it is lost.
133
+ addOptions() {
134
+ return { inline: true, allowBase64: false, HTMLAttributes: {}, resize: false };
135
+ },
136
+ addAttributes() {
137
+ return {
138
+ src: {
139
+ default: null,
140
+ renderHTML: ({ src }) => isSafeSource(src) ? { src } : {}
141
+ },
142
+ // `alt=""` is kept, as it says the image is decorative: no `alt` says nothing.
143
+ alt: { default: null }
144
+ };
145
+ },
146
+ parseHTML() {
147
+ return [
148
+ {
149
+ tag: "img[src]",
150
+ getAttrs: (element) => isSafeSource(element.getAttribute("src")) ? null : false
151
+ }
152
+ ];
153
+ },
154
+ addInputRules() {
155
+ return [];
156
+ },
157
+ addProseMirrorPlugins() {
158
+ const { schema } = this.editor;
159
+ const rules = DOMParser.fromSchema(schema).rules.filter(
160
+ (rule) => !("node" in rule && rule.node === this.name)
161
+ );
162
+ return [new Plugin({ props: { clipboardParser: new DOMParser(schema, rules) } })];
163
+ }
164
+ });
165
+ var selectedImage = (editor) => {
166
+ const { selection } = editor.state;
167
+ if (!(selection instanceof NodeSelection) || selection.node.type.name !== RichTextImage.name) {
168
+ return void 0;
169
+ }
170
+ const { src, alt } = selection.node.attrs;
171
+ return { src: src ?? "", alt };
172
+ };
173
+ var imagePosition = (editor) => {
174
+ const end = Selection.near(editor.state.selection.$to, -1).$to;
175
+ const image = editor.schema.nodes[RichTextImage.name];
176
+ return image && end.parent.canReplaceWith(end.index(), end.index(), image) ? end.pos : void 0;
177
+ };
178
+
179
+ // src/editor/actions.ts
127
180
  var RICH_TEXT_ACTIONS = [
128
181
  "bold",
129
182
  "italic",
183
+ "underline",
130
184
  "strike",
131
185
  "heading",
132
186
  "bulletList",
133
187
  "orderedList",
134
188
  "blockquote",
135
189
  "code",
190
+ "codeBlock",
191
+ "align",
136
192
  "link",
193
+ "image",
137
194
  "undo",
138
195
  "redo"
139
196
  ];
140
197
  var BUBBLE_ACTIONS = [
141
198
  "bold",
142
199
  "italic",
200
+ "underline",
143
201
  "strike",
144
202
  "code",
145
203
  "link"
146
204
  ];
205
+ var HEADINGS = [
206
+ { id: "heading1", level: 1, icon: Heading1 },
207
+ { id: "heading2", level: 2, icon: Heading2 },
208
+ { id: "heading3", level: 3, icon: Heading3 }
209
+ ];
147
210
  var ACTION_SPECS = {
148
211
  bold: {
149
212
  icon: Bold,
@@ -159,6 +222,13 @@ var ACTION_SPECS = {
159
222
  isActive: (editor) => editor.isActive("italic"),
160
223
  run: (editor) => editor.chain().toggleItalic().run()
161
224
  },
225
+ underline: {
226
+ icon: Underline,
227
+ label: "Underline",
228
+ shortcut: ["Mod", "U"],
229
+ isActive: (editor) => editor.isActive("underline"),
230
+ run: (editor) => editor.chain().toggleUnderline().run()
231
+ },
162
232
  strike: {
163
233
  icon: Strikethrough,
164
234
  label: "Strikethrough",
@@ -169,9 +239,27 @@ var ACTION_SPECS = {
169
239
  heading: {
170
240
  icon: Heading2,
171
241
  label: "Heading",
172
- shortcut: ["Mod", "Alt", "2"],
173
- isActive: (editor) => editor.isActive("heading", { level: 2 }),
174
- run: (editor) => editor.chain().toggleHeading({ level: 2 }).run()
242
+ isActive: (editor) => editor.isActive("heading"),
243
+ menu: {
244
+ rest: "paragraph",
245
+ current: (editor) => HEADINGS.find(({ level }) => editor.isActive("heading", { level }))?.id ?? "paragraph",
246
+ choices: [
247
+ {
248
+ id: "paragraph",
249
+ label: "Paragraph",
250
+ icon: Pilcrow,
251
+ shortcut: ["Mod", "Alt", "0"],
252
+ run: (editor) => editor.chain().focus().setParagraph().run()
253
+ },
254
+ ...HEADINGS.map(({ id, level, icon }) => ({
255
+ id,
256
+ label: `Heading ${level}`,
257
+ icon,
258
+ shortcut: ["Mod", "Alt", String(level)],
259
+ run: (editor) => editor.chain().focus().setHeading({ level }).run()
260
+ }))
261
+ ]
262
+ }
175
263
  },
176
264
  bulletList: {
177
265
  icon: List,
@@ -204,11 +292,60 @@ var ACTION_SPECS = {
204
292
  isActive: (editor) => editor.isActive("code"),
205
293
  run: (editor) => editor.chain().toggleCode().run()
206
294
  },
295
+ codeBlock: {
296
+ icon: SquareCode,
297
+ label: "Code block",
298
+ shortcut: ["Mod", "Alt", "C"],
299
+ isActive: (editor) => editor.isActive("codeBlock"),
300
+ run: (editor) => editor.chain().toggleCodeBlock().run()
301
+ },
302
+ align: {
303
+ icon: TextAlignStart,
304
+ mirror: true,
305
+ label: "Text alignment",
306
+ isActive: (editor) => editor.isActive({ textAlign: "center" }) || editor.isActive({ textAlign: "end" }),
307
+ menu: {
308
+ rest: "alignStart",
309
+ current: (editor) => editor.isActive({ textAlign: "center" }) ? "alignCenter" : editor.isActive({ textAlign: "end" }) ? "alignEnd" : "alignStart",
310
+ choices: [
311
+ {
312
+ id: "alignStart",
313
+ label: "Align start",
314
+ icon: TextAlignStart,
315
+ mirror: true,
316
+ // The start is what a block has when nothing is set.
317
+ run: (editor) => editor.chain().focus().unsetTextAlign().run()
318
+ },
319
+ {
320
+ id: "alignCenter",
321
+ label: "Align center",
322
+ icon: TextAlignCenter,
323
+ run: (editor) => editor.chain().focus().setTextAlign("center").run()
324
+ },
325
+ {
326
+ id: "alignEnd",
327
+ label: "Align end",
328
+ icon: TextAlignEnd,
329
+ mirror: true,
330
+ run: (editor) => editor.chain().focus().setTextAlign("end").run()
331
+ }
332
+ ]
333
+ }
334
+ },
207
335
  link: {
208
336
  icon: Link,
209
337
  label: "Link",
210
338
  shortcut: ["Mod", "K"],
211
- isActive: (editor) => editor.isActive("link")
339
+ isActive: (editor) => editor.isActive("link"),
340
+ form: "link"
341
+ },
342
+ image: {
343
+ icon: Image$1,
344
+ label: "Image",
345
+ isActive: (editor) => selectedImage(editor) !== void 0,
346
+ // An image lives in a line of text, and a code block has none.
347
+ isIdle: (editor) => selectedImage(editor) === void 0 && imagePosition(editor) === void 0,
348
+ form: "image"
212
349
  },
213
350
  undo: {
214
351
  icon: Undo2,
@@ -281,6 +418,19 @@ var BlockDirection = Extension.create({
281
418
  ];
282
419
  }
283
420
  });
421
+
422
+ // src/editor/documentJson.ts
423
+ var withoutDefaultAlignment = (node) => {
424
+ const { attrs, content, ...rest } = node;
425
+ const result = { ...rest };
426
+ if (attrs) {
427
+ const { textAlign, ...others } = attrs;
428
+ const kept = textAlign == null ? others : { ...others, textAlign };
429
+ if (Object.keys(kept).length > 0) result.attrs = kept;
430
+ }
431
+ if (content) result.content = content.map(withoutDefaultAlignment);
432
+ return result;
433
+ };
284
434
  var directionOf2 = (element, read = (el) => getComputedStyle(el)) => element && read(element).direction === "rtl" ? "rtl" : "ltr";
285
435
 
286
436
  // src/ui/mergeRefs.ts
@@ -304,6 +454,139 @@ function mergeRefs(...refs) {
304
454
  };
305
455
  }
306
456
 
457
+ // src/editor/imageUrl.ts
458
+ var normalizeImageUrl = (input) => {
459
+ const value = input.trim();
460
+ return isHttpUrl(value) ? value : void 0;
461
+ };
462
+ var LABEL = "text-xs font-medium text-gray-700 dark:text-gray-300";
463
+ var ERROR = "text-xs text-red-700 dark:text-red-400";
464
+ function ImageForm({ editor, labels, onDone, className }) {
465
+ const urlId = useId();
466
+ const altId = useId();
467
+ const urlErrorId = useId();
468
+ const altErrorId = useId();
469
+ const urlRef = useRef(null);
470
+ const altRef = useRef(null);
471
+ const [selected] = useState(() => selectedImage(editor));
472
+ const [src, setSrc] = useState(selected?.src ?? "");
473
+ const [alt, setAlt] = useState(selected?.alt ?? "");
474
+ const [decorative, setDecorative] = useState(selected?.alt === "");
475
+ const [invalid, setInvalid] = useState(null);
476
+ const apply = () => {
477
+ const address = normalizeImageUrl(src);
478
+ if (!address) {
479
+ setInvalid("url");
480
+ urlRef.current?.focus();
481
+ return;
482
+ }
483
+ const description = decorative ? "" : alt.trim();
484
+ if (!decorative && !description) {
485
+ setInvalid("alt");
486
+ altRef.current?.focus();
487
+ return;
488
+ }
489
+ const attrs = { src: address, alt: description };
490
+ const chain = editor.chain().focus();
491
+ const at = imagePosition(editor);
492
+ if (selected) chain.updateAttributes("image", attrs);
493
+ else if (at !== void 0) chain.insertContentAt(at, { type: "image", attrs });
494
+ chain.run();
495
+ onDone();
496
+ };
497
+ const onKeyDown = (e) => {
498
+ if (e.key === "Enter") {
499
+ e.preventDefault();
500
+ apply();
501
+ } else if (e.key === "Escape") {
502
+ e.preventDefault();
503
+ e.stopPropagation();
504
+ onDone();
505
+ editor.commands.focus();
506
+ }
507
+ };
508
+ const input = (side) => clsx(
509
+ "block w-full",
510
+ fieldClass({ size: "sm", invalid: invalid === side, fixedHeight: false }),
511
+ FIELD_PAD_X.sm,
512
+ FIELD_PAD_Y.sm,
513
+ FIELD_PLACEHOLDER
514
+ );
515
+ return /* @__PURE__ */ jsxs(
516
+ "div",
517
+ {
518
+ role: "group",
519
+ "aria-label": labels.imageDialog,
520
+ className: clsx("flex flex-col gap-3", className),
521
+ children: [
522
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
523
+ /* @__PURE__ */ jsx("label", { htmlFor: urlId, className: LABEL, children: labels.imageUrl }),
524
+ /* @__PURE__ */ jsx(
525
+ "input",
526
+ {
527
+ ref: urlRef,
528
+ id: urlId,
529
+ type: "text",
530
+ inputMode: "url",
531
+ autoFocus: true,
532
+ autoComplete: "off",
533
+ spellCheck: false,
534
+ dir: "ltr",
535
+ "aria-invalid": invalid === "url" || void 0,
536
+ "aria-describedby": invalid === "url" ? urlErrorId : void 0,
537
+ placeholder: "https://",
538
+ value: src,
539
+ onChange: (e) => {
540
+ setSrc(e.target.value);
541
+ setInvalid(null);
542
+ },
543
+ onKeyDown,
544
+ className: input("url")
545
+ }
546
+ ),
547
+ invalid === "url" && /* @__PURE__ */ jsx("span", { id: urlErrorId, className: ERROR, children: labels.imageInvalid })
548
+ ] }),
549
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
550
+ /* @__PURE__ */ jsx("label", { htmlFor: altId, className: LABEL, children: labels.imageAlt }),
551
+ /* @__PURE__ */ jsx(
552
+ "input",
553
+ {
554
+ ref: altRef,
555
+ id: altId,
556
+ type: "text",
557
+ autoComplete: "off",
558
+ disabled: decorative,
559
+ "aria-invalid": invalid === "alt" || void 0,
560
+ "aria-describedby": invalid === "alt" ? altErrorId : void 0,
561
+ value: alt,
562
+ onChange: (e) => {
563
+ setAlt(e.target.value);
564
+ setInvalid(null);
565
+ },
566
+ onKeyDown,
567
+ className: clsx(input("alt"), "disabled:cursor-not-allowed disabled:opacity-50")
568
+ }
569
+ ),
570
+ invalid === "alt" && /* @__PURE__ */ jsx("span", { id: altErrorId, className: ERROR, children: labels.imageAltRequired })
571
+ ] }),
572
+ /* @__PURE__ */ jsx(
573
+ Checkbox,
574
+ {
575
+ size: "sm",
576
+ label: labels.imageDecorative,
577
+ checked: decorative,
578
+ onChange: (checked) => {
579
+ setDecorative(checked);
580
+ setInvalid((was) => was === "alt" ? null : was);
581
+ }
582
+ }
583
+ ),
584
+ /* @__PURE__ */ jsx("div", { className: "flex items-center justify-end", children: /* @__PURE__ */ jsx(Button, { type: "button", size: "xs", onClick: apply, children: labels.imageApply }) })
585
+ ]
586
+ }
587
+ );
588
+ }
589
+
307
590
  // src/editor/linkUrl.ts
308
591
  var BARE_HOST = /^(?:[a-z0-9-]+\.)+[a-z]{2,}(?:[/?#]\S*)?$/i;
309
592
  var EMAIL = /^[^\s@<>"'\\:/]+@(?:[a-z0-9-]+\.)+[a-z]{2,}$/i;
@@ -400,9 +683,9 @@ function FormattingToolbar({
400
683
  actions,
401
684
  labels,
402
685
  disabled,
403
- linkOpen,
404
- onLinkOpenChange,
405
- linkMode,
686
+ form,
687
+ onFormChange,
688
+ formMode,
406
689
  onEscape,
407
690
  className,
408
691
  ref
@@ -416,7 +699,8 @@ function FormattingToolbar({
416
699
  action,
417
700
  {
418
701
  active: ACTION_SPECS[action].isActive(current),
419
- idle: ACTION_SPECS[action].isIdle?.(current) ?? false
702
+ idle: ACTION_SPECS[action].isIdle?.(current) ?? false,
703
+ choice: ACTION_SPECS[action].menu?.current(current)
420
704
  }
421
705
  ])
422
706
  )
@@ -425,6 +709,7 @@ function FormattingToolbar({
425
709
  const tabStop = Math.min(stop, actions.length - 1);
426
710
  const buttons = () => Array.from(root.current?.querySelectorAll("button[data-rt-action]") ?? []);
427
711
  const onKeyDown = (event) => {
712
+ if (!(event.target instanceof Node) || !root.current?.contains(event.target)) return;
428
713
  if (event.key === "Escape" && onEscape) {
429
714
  event.preventDefault();
430
715
  onEscape();
@@ -439,15 +724,15 @@ function FormattingToolbar({
439
724
  event.preventDefault();
440
725
  list[next]?.focus();
441
726
  };
442
- const linkField = /* @__PURE__ */ jsx(
443
- LinkForm,
444
- {
727
+ const field = (which) => {
728
+ const props = {
445
729
  editor,
446
730
  labels,
447
- onDone: () => onLinkOpenChange(false),
448
- className: linkMode === "inline" ? "w-64 p-2" : void 0
449
- }
450
- );
731
+ onDone: () => onFormChange(null),
732
+ className: formMode === "inline" ? "w-64 p-2" : void 0
733
+ };
734
+ return which === "link" ? /* @__PURE__ */ jsx(LinkForm, { ...props }) : /* @__PURE__ */ jsx(ImageForm, { ...props });
735
+ };
451
736
  return /* @__PURE__ */ jsx(
452
737
  "div",
453
738
  {
@@ -457,15 +742,16 @@ function FormattingToolbar({
457
742
  "aria-orientation": "horizontal",
458
743
  onKeyDown,
459
744
  className: clsx("flex flex-wrap items-center gap-0.5", className),
460
- children: linkMode === "inline" && linkOpen ? linkField : actions.map((action, index) => {
745
+ children: formMode === "inline" && form ? field(form) : actions.map((action, index) => {
461
746
  const spec = ACTION_SPECS[action];
462
- const { active, idle } = state[action];
463
- const Icon = spec.icon;
747
+ const { active, idle, choice } = state[action];
464
748
  const inert = disabled || idle;
749
+ const shown = active && spec.menu ? spec.menu.choices.find(({ id }) => id === choice) : void 0;
750
+ const Icon = shown?.icon ?? spec.icon;
465
751
  const common = {
466
752
  "data-rt-action": action,
467
753
  "aria-label": labels[action],
468
- "aria-keyshortcuts": ariaShortcut(spec.shortcut),
754
+ "aria-keyshortcuts": spec.shortcut && ariaShortcut(spec.shortcut),
469
755
  "aria-pressed": active,
470
756
  "aria-disabled": inert || void 0,
471
757
  tabIndex: index === tabStop ? 0 : -1,
@@ -474,43 +760,82 @@ function FormattingToolbar({
474
760
  onMouseDown: (e) => e.preventDefault(),
475
761
  className: clsx(BUTTON, active ? PRESSED : IDLE)
476
762
  };
477
- const icon = /* @__PURE__ */ jsx(Icon, { "aria-hidden": true, className: clsx("h-4 w-4", spec.mirror && "rtl:-scale-x-100") });
763
+ const icon = /* @__PURE__ */ jsx(
764
+ Icon,
765
+ {
766
+ "aria-hidden": true,
767
+ className: clsx(
768
+ "h-4 w-4",
769
+ (shown ? shown.mirror : spec.mirror) && "rtl:-scale-x-100"
770
+ )
771
+ }
772
+ );
478
773
  const tooltip = (child) => /* @__PURE__ */ jsx(
479
774
  Tooltip,
480
775
  {
481
776
  content: labels[action],
482
- shortcut: shortcutKeys(spec.shortcut),
777
+ shortcut: spec.shortcut && shortcutKeys(spec.shortcut),
483
778
  position: "top",
484
779
  children: child
485
780
  },
486
781
  action
487
782
  );
488
- if (action === "link" && linkMode === "popover") {
489
- return /* @__PURE__ */ jsxs(Popover, { open: linkOpen && !disabled, onOpenChange: onLinkOpenChange, children: [
490
- tooltip(/* @__PURE__ */ jsx(Popover.Trigger, { ...common, children: icon })),
491
- /* @__PURE__ */ jsx(
492
- Popover.Content,
783
+ if (spec.menu) {
784
+ return /* @__PURE__ */ jsxs(Menu, { children: [
785
+ tooltip(
786
+ /* @__PURE__ */ jsx(
787
+ Menu.Trigger,
788
+ {
789
+ ...common,
790
+ onClick: (e) => inert && e.preventDefault(),
791
+ onKeyDown: (e) => {
792
+ if (inert && (e.key === "ArrowDown" || e.key === "ArrowUp")) {
793
+ e.preventDefault();
794
+ }
795
+ },
796
+ children: icon
797
+ }
798
+ )
799
+ ),
800
+ /* @__PURE__ */ jsx(Menu.Content, { width: "w-44", children: spec.menu.choices.map((item) => /* @__PURE__ */ jsx(
801
+ Menu.RadioItem,
493
802
  {
494
- "aria-label": labels.linkDialog,
495
- position: "bottom",
496
- width: "w-72",
497
- padding: "p-3",
498
- children: linkField
499
- }
500
- )
803
+ checked: item.id === choice,
804
+ "aria-keyshortcuts": item.shortcut && ariaShortcut(item.shortcut),
805
+ onSelect: () => item.run(editor),
806
+ children: labels[item.id]
807
+ },
808
+ item.id
809
+ )) })
501
810
  ] }, action);
502
811
  }
503
- if (action === "link") {
812
+ if (spec.form && formMode === "popover") {
813
+ return /* @__PURE__ */ jsxs(
814
+ Popover,
815
+ {
816
+ open: form === spec.form && !inert,
817
+ onOpenChange: (open) => onFormChange(open ? spec.form ?? null : null),
818
+ children: [
819
+ tooltip(/* @__PURE__ */ jsx(Popover.Trigger, { ...common, children: icon })),
820
+ /* @__PURE__ */ jsx(
821
+ Popover.Content,
822
+ {
823
+ "aria-label": spec.form === "image" ? labels.imageDialog : labels.linkDialog,
824
+ position: "bottom",
825
+ width: spec.form === "image" ? "w-80" : "w-72",
826
+ padding: "p-3",
827
+ children: field(spec.form)
828
+ }
829
+ )
830
+ ]
831
+ },
832
+ action
833
+ );
834
+ }
835
+ if (spec.form) {
836
+ const opens = spec.form;
504
837
  return tooltip(
505
- /* @__PURE__ */ jsx(
506
- "button",
507
- {
508
- type: "button",
509
- ...common,
510
- onClick: () => !disabled && onLinkOpenChange(true),
511
- children: icon
512
- }
513
- )
838
+ /* @__PURE__ */ jsx("button", { type: "button", ...common, onClick: () => !inert && onFormChange(opens), children: icon })
514
839
  );
515
840
  }
516
841
  return tooltip(
@@ -528,23 +853,40 @@ var DEFAULTS = {
528
853
  linkDialog: "Edit link",
529
854
  linkApply: "Apply",
530
855
  linkRemove: "Remove link",
531
- linkInvalid: "Enter an http(s), mailto or relative link."
856
+ linkInvalid: "Enter an http(s), mailto or relative link.",
857
+ imageDialog: "Image details",
858
+ imageUrl: "Image URL",
859
+ imageAlt: "Description (alt text)",
860
+ imageDecorative: "Decorative image, no description",
861
+ imageApply: "Apply",
862
+ imageInvalid: "Enter an http(s) image address.",
863
+ imageAltRequired: "Describe the image, or mark it decorative."
532
864
  };
865
+ var NAMED_DEFAULTS = Object.fromEntries([
866
+ ...Object.entries(ACTION_SPECS).map(([action, spec]) => [action, spec.label]),
867
+ ...Object.values(ACTION_SPECS).flatMap(
868
+ ({ menu }) => (menu?.choices ?? []).map((choice) => [choice.id, choice.label])
869
+ )
870
+ ]);
533
871
  var resolveLabels = (labels = {}) => {
534
- const actions = Object.fromEntries(
535
- Object.keys(ACTION_SPECS).map((action) => [
536
- action,
537
- labels[action] ?? ACTION_SPECS[action].label
538
- ])
872
+ const named = Object.fromEntries(
873
+ Object.keys(NAMED_DEFAULTS).map((key2) => [key2, labels[key2] ?? NAMED_DEFAULTS[key2]])
539
874
  );
540
875
  return {
541
- ...actions,
876
+ ...named,
542
877
  toolbar: labels.toolbar ?? DEFAULTS.toolbar,
543
878
  linkUrl: labels.linkUrl ?? DEFAULTS.linkUrl,
544
879
  linkDialog: labels.linkDialog ?? DEFAULTS.linkDialog,
545
880
  linkApply: labels.linkApply ?? DEFAULTS.linkApply,
546
881
  linkRemove: labels.linkRemove ?? DEFAULTS.linkRemove,
547
- linkInvalid: labels.linkInvalid ?? DEFAULTS.linkInvalid
882
+ linkInvalid: labels.linkInvalid ?? DEFAULTS.linkInvalid,
883
+ imageDialog: labels.imageDialog ?? DEFAULTS.imageDialog,
884
+ imageUrl: labels.imageUrl ?? DEFAULTS.imageUrl,
885
+ imageAlt: labels.imageAlt ?? DEFAULTS.imageAlt,
886
+ imageDecorative: labels.imageDecorative ?? DEFAULTS.imageDecorative,
887
+ imageApply: labels.imageApply ?? DEFAULTS.imageApply,
888
+ imageInvalid: labels.imageInvalid ?? DEFAULTS.imageInvalid,
889
+ imageAltRequired: labels.imageAltRequired ?? DEFAULTS.imageAltRequired
548
890
  };
549
891
  };
550
892
  var NO_PROPS = { format: "html", setCurrent: () => {
@@ -559,7 +901,7 @@ var createStore = (initial) => {
559
901
  };
560
902
  };
561
903
  var EMPTY_DOC = { type: "doc", content: [{ type: "paragraph" }] };
562
- var readContent = (editor, format) => format === "json" ? editor.getJSON() : editor.isEmpty ? "" : editor.getHTML();
904
+ var readContent = (editor, format) => format === "json" ? withoutDefaultAlignment(editor.getJSON()) : editor.isEmpty ? "" : editor.getHTML();
563
905
  var countCharacters = (editor) => editor.storage.characterCount?.characters() ?? 0;
564
906
  var sameContent = (a, b) => typeof a === "string" || typeof b === "string" ? a === b : JSON.stringify(a) === JSON.stringify(b);
565
907
  var LinkShortcut = Extension.create({
@@ -569,12 +911,14 @@ var LinkShortcut = Extension.create({
569
911
  return { "Mod-k": () => this.options.open() };
570
912
  }
571
913
  });
914
+ var BlockAlign = TextAlign.extend({ addKeyboardShortcuts: () => ({}) });
572
915
  var CONTENT_CLASS = [
573
916
  "min-w-0 outline-hidden break-words",
574
917
  "[&>*+*]:mt-2",
575
918
  // Each block follows the direction of what is typed in it, as a Textarea's paragraphs do
576
919
  // (`FIELD_PLAINTEXT`); lists and quotes take a `dir` from `BlockDirection`.
577
- "[&_p]:[unicode-bidi:plaintext] [&_h2]:[unicode-bidi:plaintext] [&_h3]:[unicode-bidi:plaintext]",
920
+ "[&_p]:[unicode-bidi:plaintext] [&_h1]:[unicode-bidi:plaintext] [&_h2]:[unicode-bidi:plaintext] [&_h3]:[unicode-bidi:plaintext]",
921
+ "[&_h1]:text-xl [&_h1]:leading-tight [&_h1]:font-semibold",
578
922
  "[&_h2]:text-lg [&_h2]:leading-snug [&_h2]:font-semibold",
579
923
  "[&_h3]:text-base [&_h3]:leading-snug [&_h3]:font-semibold",
580
924
  "[&_ul]:list-disc [&_ul]:ps-5 [&_ol]:list-decimal [&_ol]:ps-5 [&_li>p]:my-0",
@@ -582,6 +926,13 @@ var CONTENT_CLASS = [
582
926
  "dark:[&_blockquote]:border-gray-600 dark:[&_blockquote]:text-gray-300",
583
927
  "[&_code]:rounded-sm [&_code]:bg-gray-100 [&_code]:px-1 [&_code]:font-mono [&_code]:text-[0.9em]",
584
928
  "dark:[&_code]:bg-gray-800",
929
+ // A code block reads left to right in any page, as `CodeBlock` does; its `code` drops the pill.
930
+ "[&_pre]:overflow-x-auto [&_pre]:rounded-md [&_pre]:bg-gray-100 [&_pre]:p-3 [&_pre]:font-mono [&_pre]:text-[0.9em] [&_pre]:leading-snug",
931
+ "dark:[&_pre]:bg-gray-800 [&_pre]:[direction:ltr] [&_pre]:text-start",
932
+ "[&_pre_code]:bg-transparent [&_pre_code]:p-0 [&_pre_code]:text-[1em]",
933
+ // An image is in the line: it never grows past it, and a selected one shows it.
934
+ "[&_img[src]]:inline-block [&_img[src]]:h-auto [&_img[src]]:max-w-full [&_img[src]]:align-middle",
935
+ "[&_img.ProseMirror-selectednode]:outline-brand-500 [&_img.ProseMirror-selectednode]:outline-2",
585
936
  "[&_a]:text-brand-600 dark:[&_a]:text-brand-400 [&_a]:underline [&_a]:underline-offset-2",
586
937
  "[&_p.is-editor-empty:first-child]:before:pointer-events-none [&_p.is-editor-empty:first-child]:before:float-start [&_p.is-editor-empty:first-child]:before:h-0",
587
938
  "[&_p.is-editor-empty:first-child]:before:text-gray-500 dark:[&_p.is-editor-empty:first-child]:before:text-gray-400",
@@ -626,7 +977,7 @@ function RichTextEditor({
626
977
  onChange: (next) => onChange?.(next),
627
978
  name: "RichTextEditor.value"
628
979
  });
629
- const [linkOpen, setLinkOpen] = useState(false);
980
+ const [form, setForm] = useState(null);
630
981
  const [characters, setCharacters] = useState(0);
631
982
  const [latest] = useState(() => createStore(NO_PROPS));
632
983
  const [echoes] = useState(() => createStore([]));
@@ -637,7 +988,7 @@ function RichTextEditor({
637
988
  setCurrent,
638
989
  openLink: () => {
639
990
  if (!hasLinkField || !editable) return false;
640
- setLinkOpen(true);
991
+ setForm("link");
641
992
  return true;
642
993
  }
643
994
  });
@@ -652,11 +1003,9 @@ function RichTextEditor({
652
1003
  content: current,
653
1004
  extensions: [
654
1005
  StarterKit.configure({
655
- heading: { levels: [2, 3] },
1006
+ heading: { levels: [1, 2, 3] },
656
1007
  // What the editor doesn't offer is out of the schema, so no paste or value brings it in.
657
- codeBlock: false,
658
1008
  horizontalRule: false,
659
- underline: false,
660
1009
  trailingNode: false,
661
1010
  link: {
662
1011
  openOnClick: false,
@@ -665,6 +1014,10 @@ function RichTextEditor({
665
1014
  isAllowedUri: (url) => isSafeLinkUrl(url)
666
1015
  }
667
1016
  }),
1017
+ // Center and end only, as the sides of the line: a pasted `left`, `right` or `justify` is
1018
+ // dropped, and the start is a block with nothing set.
1019
+ BlockAlign.configure({ types: ["paragraph", "heading"], alignments: ["center", "end"] }),
1020
+ RichTextImage,
668
1021
  Placeholder.configure({ placeholder: () => latest.get().placeholder ?? "" }),
669
1022
  ...maxLength !== void 0 ? [CharacterCount.configure({ limit: maxLength })] : [],
670
1023
  LinkShortcut.configure({ open: () => latest.get().openLink() }),
@@ -784,9 +1137,9 @@ function RichTextEditor({
784
1137
  actions,
785
1138
  labels,
786
1139
  disabled,
787
- linkOpen,
788
- onLinkOpenChange: setLinkOpen,
789
- linkMode: "popover",
1140
+ form,
1141
+ onFormChange: setForm,
1142
+ formMode: "popover",
790
1143
  className: "border-b border-gray-200 p-1 dark:border-gray-700"
791
1144
  }
792
1145
  ),
@@ -800,7 +1153,7 @@ function RichTextEditor({
800
1153
  strategy: "fixed",
801
1154
  placement: "top",
802
1155
  offset: 8,
803
- onHide: () => setLinkOpen(false)
1156
+ onHide: () => setForm(null)
804
1157
  },
805
1158
  className: "z-[9999]",
806
1159
  children: /* @__PURE__ */ jsx(
@@ -811,11 +1164,11 @@ function RichTextEditor({
811
1164
  actions,
812
1165
  labels,
813
1166
  disabled,
814
- linkOpen,
815
- onLinkOpenChange: setLinkOpen,
816
- linkMode: "inline",
1167
+ form,
1168
+ onFormChange: setForm,
1169
+ formMode: "inline",
817
1170
  onEscape: () => {
818
- setLinkOpen(false);
1171
+ setForm(null);
819
1172
  editor.commands.focus();
820
1173
  },
821
1174
  className: "rounded-lg border border-gray-200 bg-white p-1 shadow-lg dark:border-gray-700 dark:bg-gray-900"
@@ -861,6 +1214,7 @@ function RichTextEditor({
861
1214
  {
862
1215
  type: "hidden",
863
1216
  name,
1217
+ disabled,
864
1218
  value: typeof current === "string" ? current : JSON.stringify(current)
865
1219
  }
866
1220
  )