@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.
@@ -8,8 +8,11 @@ var react$1 = require('@tiptap/react');
8
8
  var menus = require('@tiptap/react/menus');
9
9
  var extensions = require('@tiptap/extensions');
10
10
  var StarterKit = require('@tiptap/starter-kit');
11
+ var TextAlign = require('@tiptap/extension-text-align');
11
12
  var jsxRuntime = require('react/jsx-runtime');
12
13
  var lucideReact = require('lucide-react');
14
+ var extensionImage = require('@tiptap/extension-image');
15
+ var model = require('@tiptap/pm/model');
13
16
  var state = require('@tiptap/pm/state');
14
17
  var view = require('@tiptap/pm/view');
15
18
  var cosmosReact = require('@sckyzo/cosmos-react');
@@ -17,6 +20,7 @@ var cosmosReact = require('@sckyzo/cosmos-react');
17
20
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
18
21
 
19
22
  var StarterKit__default = /*#__PURE__*/_interopDefault(StarterKit);
23
+ var TextAlign__default = /*#__PURE__*/_interopDefault(TextAlign);
20
24
 
21
25
  // src/editor/RichTextEditor.tsx
22
26
  function useControllableState({
@@ -130,26 +134,86 @@ var RequiredMark = ({ srText }) => /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fr
130
134
  /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": true, className: "ms-0.5 text-red-700 dark:text-red-400", children: "*" }),
131
135
  srText && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "sr-only", children: srText })
132
136
  ] });
137
+ var isSafeSource = (src) => typeof src === "string" && isHttpUrl(src);
138
+ var RichTextImage = extensionImage.Image.extend({
139
+ // Written out in full: `extend` calls `addOptions` again, and a `configure` before it is lost.
140
+ addOptions() {
141
+ return { inline: true, allowBase64: false, HTMLAttributes: {}, resize: false };
142
+ },
143
+ addAttributes() {
144
+ return {
145
+ src: {
146
+ default: null,
147
+ renderHTML: ({ src }) => isSafeSource(src) ? { src } : {}
148
+ },
149
+ // `alt=""` is kept, as it says the image is decorative: no `alt` says nothing.
150
+ alt: { default: null }
151
+ };
152
+ },
153
+ parseHTML() {
154
+ return [
155
+ {
156
+ tag: "img[src]",
157
+ getAttrs: (element) => isSafeSource(element.getAttribute("src")) ? null : false
158
+ }
159
+ ];
160
+ },
161
+ addInputRules() {
162
+ return [];
163
+ },
164
+ addProseMirrorPlugins() {
165
+ const { schema } = this.editor;
166
+ const rules = model.DOMParser.fromSchema(schema).rules.filter(
167
+ (rule) => !("node" in rule && rule.node === this.name)
168
+ );
169
+ return [new state.Plugin({ props: { clipboardParser: new model.DOMParser(schema, rules) } })];
170
+ }
171
+ });
172
+ var selectedImage = (editor) => {
173
+ const { selection } = editor.state;
174
+ if (!(selection instanceof state.NodeSelection) || selection.node.type.name !== RichTextImage.name) {
175
+ return void 0;
176
+ }
177
+ const { src, alt } = selection.node.attrs;
178
+ return { src: src ?? "", alt };
179
+ };
180
+ var imagePosition = (editor) => {
181
+ const end = state.Selection.near(editor.state.selection.$to, -1).$to;
182
+ const image = editor.schema.nodes[RichTextImage.name];
183
+ return image && end.parent.canReplaceWith(end.index(), end.index(), image) ? end.pos : void 0;
184
+ };
185
+
186
+ // src/editor/actions.ts
133
187
  var RICH_TEXT_ACTIONS = [
134
188
  "bold",
135
189
  "italic",
190
+ "underline",
136
191
  "strike",
137
192
  "heading",
138
193
  "bulletList",
139
194
  "orderedList",
140
195
  "blockquote",
141
196
  "code",
197
+ "codeBlock",
198
+ "align",
142
199
  "link",
200
+ "image",
143
201
  "undo",
144
202
  "redo"
145
203
  ];
146
204
  var BUBBLE_ACTIONS = [
147
205
  "bold",
148
206
  "italic",
207
+ "underline",
149
208
  "strike",
150
209
  "code",
151
210
  "link"
152
211
  ];
212
+ var HEADINGS = [
213
+ { id: "heading1", level: 1, icon: lucideReact.Heading1 },
214
+ { id: "heading2", level: 2, icon: lucideReact.Heading2 },
215
+ { id: "heading3", level: 3, icon: lucideReact.Heading3 }
216
+ ];
153
217
  var ACTION_SPECS = {
154
218
  bold: {
155
219
  icon: lucideReact.Bold,
@@ -165,6 +229,13 @@ var ACTION_SPECS = {
165
229
  isActive: (editor) => editor.isActive("italic"),
166
230
  run: (editor) => editor.chain().toggleItalic().run()
167
231
  },
232
+ underline: {
233
+ icon: lucideReact.Underline,
234
+ label: "Underline",
235
+ shortcut: ["Mod", "U"],
236
+ isActive: (editor) => editor.isActive("underline"),
237
+ run: (editor) => editor.chain().toggleUnderline().run()
238
+ },
168
239
  strike: {
169
240
  icon: lucideReact.Strikethrough,
170
241
  label: "Strikethrough",
@@ -175,9 +246,27 @@ var ACTION_SPECS = {
175
246
  heading: {
176
247
  icon: lucideReact.Heading2,
177
248
  label: "Heading",
178
- shortcut: ["Mod", "Alt", "2"],
179
- isActive: (editor) => editor.isActive("heading", { level: 2 }),
180
- run: (editor) => editor.chain().toggleHeading({ level: 2 }).run()
249
+ isActive: (editor) => editor.isActive("heading"),
250
+ menu: {
251
+ rest: "paragraph",
252
+ current: (editor) => HEADINGS.find(({ level }) => editor.isActive("heading", { level }))?.id ?? "paragraph",
253
+ choices: [
254
+ {
255
+ id: "paragraph",
256
+ label: "Paragraph",
257
+ icon: lucideReact.Pilcrow,
258
+ shortcut: ["Mod", "Alt", "0"],
259
+ run: (editor) => editor.chain().focus().setParagraph().run()
260
+ },
261
+ ...HEADINGS.map(({ id, level, icon }) => ({
262
+ id,
263
+ label: `Heading ${level}`,
264
+ icon,
265
+ shortcut: ["Mod", "Alt", String(level)],
266
+ run: (editor) => editor.chain().focus().setHeading({ level }).run()
267
+ }))
268
+ ]
269
+ }
181
270
  },
182
271
  bulletList: {
183
272
  icon: lucideReact.List,
@@ -210,11 +299,60 @@ var ACTION_SPECS = {
210
299
  isActive: (editor) => editor.isActive("code"),
211
300
  run: (editor) => editor.chain().toggleCode().run()
212
301
  },
302
+ codeBlock: {
303
+ icon: lucideReact.SquareCode,
304
+ label: "Code block",
305
+ shortcut: ["Mod", "Alt", "C"],
306
+ isActive: (editor) => editor.isActive("codeBlock"),
307
+ run: (editor) => editor.chain().toggleCodeBlock().run()
308
+ },
309
+ align: {
310
+ icon: lucideReact.TextAlignStart,
311
+ mirror: true,
312
+ label: "Text alignment",
313
+ isActive: (editor) => editor.isActive({ textAlign: "center" }) || editor.isActive({ textAlign: "end" }),
314
+ menu: {
315
+ rest: "alignStart",
316
+ current: (editor) => editor.isActive({ textAlign: "center" }) ? "alignCenter" : editor.isActive({ textAlign: "end" }) ? "alignEnd" : "alignStart",
317
+ choices: [
318
+ {
319
+ id: "alignStart",
320
+ label: "Align start",
321
+ icon: lucideReact.TextAlignStart,
322
+ mirror: true,
323
+ // The start is what a block has when nothing is set.
324
+ run: (editor) => editor.chain().focus().unsetTextAlign().run()
325
+ },
326
+ {
327
+ id: "alignCenter",
328
+ label: "Align center",
329
+ icon: lucideReact.TextAlignCenter,
330
+ run: (editor) => editor.chain().focus().setTextAlign("center").run()
331
+ },
332
+ {
333
+ id: "alignEnd",
334
+ label: "Align end",
335
+ icon: lucideReact.TextAlignEnd,
336
+ mirror: true,
337
+ run: (editor) => editor.chain().focus().setTextAlign("end").run()
338
+ }
339
+ ]
340
+ }
341
+ },
213
342
  link: {
214
343
  icon: lucideReact.Link,
215
344
  label: "Link",
216
345
  shortcut: ["Mod", "K"],
217
- isActive: (editor) => editor.isActive("link")
346
+ isActive: (editor) => editor.isActive("link"),
347
+ form: "link"
348
+ },
349
+ image: {
350
+ icon: lucideReact.Image,
351
+ label: "Image",
352
+ isActive: (editor) => selectedImage(editor) !== void 0,
353
+ // An image lives in a line of text, and a code block has none.
354
+ isIdle: (editor) => selectedImage(editor) === void 0 && imagePosition(editor) === void 0,
355
+ form: "image"
218
356
  },
219
357
  undo: {
220
358
  icon: lucideReact.Undo2,
@@ -287,6 +425,19 @@ var BlockDirection = core.Extension.create({
287
425
  ];
288
426
  }
289
427
  });
428
+
429
+ // src/editor/documentJson.ts
430
+ var withoutDefaultAlignment = (node) => {
431
+ const { attrs, content, ...rest } = node;
432
+ const result = { ...rest };
433
+ if (attrs) {
434
+ const { textAlign, ...others } = attrs;
435
+ const kept = textAlign == null ? others : { ...others, textAlign };
436
+ if (Object.keys(kept).length > 0) result.attrs = kept;
437
+ }
438
+ if (content) result.content = content.map(withoutDefaultAlignment);
439
+ return result;
440
+ };
290
441
  var directionOf2 = (element, read = (el) => getComputedStyle(el)) => element && read(element).direction === "rtl" ? "rtl" : "ltr";
291
442
 
292
443
  // src/ui/mergeRefs.ts
@@ -310,6 +461,139 @@ function mergeRefs(...refs) {
310
461
  };
311
462
  }
312
463
 
464
+ // src/editor/imageUrl.ts
465
+ var normalizeImageUrl = (input) => {
466
+ const value = input.trim();
467
+ return isHttpUrl(value) ? value : void 0;
468
+ };
469
+ var LABEL = "text-xs font-medium text-gray-700 dark:text-gray-300";
470
+ var ERROR = "text-xs text-red-700 dark:text-red-400";
471
+ function ImageForm({ editor, labels, onDone, className }) {
472
+ const urlId = react.useId();
473
+ const altId = react.useId();
474
+ const urlErrorId = react.useId();
475
+ const altErrorId = react.useId();
476
+ const urlRef = react.useRef(null);
477
+ const altRef = react.useRef(null);
478
+ const [selected] = react.useState(() => selectedImage(editor));
479
+ const [src, setSrc] = react.useState(selected?.src ?? "");
480
+ const [alt, setAlt] = react.useState(selected?.alt ?? "");
481
+ const [decorative, setDecorative] = react.useState(selected?.alt === "");
482
+ const [invalid, setInvalid] = react.useState(null);
483
+ const apply = () => {
484
+ const address = normalizeImageUrl(src);
485
+ if (!address) {
486
+ setInvalid("url");
487
+ urlRef.current?.focus();
488
+ return;
489
+ }
490
+ const description = decorative ? "" : alt.trim();
491
+ if (!decorative && !description) {
492
+ setInvalid("alt");
493
+ altRef.current?.focus();
494
+ return;
495
+ }
496
+ const attrs = { src: address, alt: description };
497
+ const chain = editor.chain().focus();
498
+ const at = imagePosition(editor);
499
+ if (selected) chain.updateAttributes("image", attrs);
500
+ else if (at !== void 0) chain.insertContentAt(at, { type: "image", attrs });
501
+ chain.run();
502
+ onDone();
503
+ };
504
+ const onKeyDown = (e) => {
505
+ if (e.key === "Enter") {
506
+ e.preventDefault();
507
+ apply();
508
+ } else if (e.key === "Escape") {
509
+ e.preventDefault();
510
+ e.stopPropagation();
511
+ onDone();
512
+ editor.commands.focus();
513
+ }
514
+ };
515
+ const input = (side) => clsx.clsx(
516
+ "block w-full",
517
+ fieldClass({ size: "sm", invalid: invalid === side, fixedHeight: false }),
518
+ FIELD_PAD_X.sm,
519
+ FIELD_PAD_Y.sm,
520
+ FIELD_PLACEHOLDER
521
+ );
522
+ return /* @__PURE__ */ jsxRuntime.jsxs(
523
+ "div",
524
+ {
525
+ role: "group",
526
+ "aria-label": labels.imageDialog,
527
+ className: clsx.clsx("flex flex-col gap-3", className),
528
+ children: [
529
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-1", children: [
530
+ /* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: urlId, className: LABEL, children: labels.imageUrl }),
531
+ /* @__PURE__ */ jsxRuntime.jsx(
532
+ "input",
533
+ {
534
+ ref: urlRef,
535
+ id: urlId,
536
+ type: "text",
537
+ inputMode: "url",
538
+ autoFocus: true,
539
+ autoComplete: "off",
540
+ spellCheck: false,
541
+ dir: "ltr",
542
+ "aria-invalid": invalid === "url" || void 0,
543
+ "aria-describedby": invalid === "url" ? urlErrorId : void 0,
544
+ placeholder: "https://",
545
+ value: src,
546
+ onChange: (e) => {
547
+ setSrc(e.target.value);
548
+ setInvalid(null);
549
+ },
550
+ onKeyDown,
551
+ className: input("url")
552
+ }
553
+ ),
554
+ invalid === "url" && /* @__PURE__ */ jsxRuntime.jsx("span", { id: urlErrorId, className: ERROR, children: labels.imageInvalid })
555
+ ] }),
556
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-1", children: [
557
+ /* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: altId, className: LABEL, children: labels.imageAlt }),
558
+ /* @__PURE__ */ jsxRuntime.jsx(
559
+ "input",
560
+ {
561
+ ref: altRef,
562
+ id: altId,
563
+ type: "text",
564
+ autoComplete: "off",
565
+ disabled: decorative,
566
+ "aria-invalid": invalid === "alt" || void 0,
567
+ "aria-describedby": invalid === "alt" ? altErrorId : void 0,
568
+ value: alt,
569
+ onChange: (e) => {
570
+ setAlt(e.target.value);
571
+ setInvalid(null);
572
+ },
573
+ onKeyDown,
574
+ className: clsx.clsx(input("alt"), "disabled:cursor-not-allowed disabled:opacity-50")
575
+ }
576
+ ),
577
+ invalid === "alt" && /* @__PURE__ */ jsxRuntime.jsx("span", { id: altErrorId, className: ERROR, children: labels.imageAltRequired })
578
+ ] }),
579
+ /* @__PURE__ */ jsxRuntime.jsx(
580
+ cosmosReact.Checkbox,
581
+ {
582
+ size: "sm",
583
+ label: labels.imageDecorative,
584
+ checked: decorative,
585
+ onChange: (checked) => {
586
+ setDecorative(checked);
587
+ setInvalid((was) => was === "alt" ? null : was);
588
+ }
589
+ }
590
+ ),
591
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-end", children: /* @__PURE__ */ jsxRuntime.jsx(cosmosReact.Button, { type: "button", size: "xs", onClick: apply, children: labels.imageApply }) })
592
+ ]
593
+ }
594
+ );
595
+ }
596
+
313
597
  // src/editor/linkUrl.ts
314
598
  var BARE_HOST = /^(?:[a-z0-9-]+\.)+[a-z]{2,}(?:[/?#]\S*)?$/i;
315
599
  var EMAIL = /^[^\s@<>"'\\:/]+@(?:[a-z0-9-]+\.)+[a-z]{2,}$/i;
@@ -406,9 +690,9 @@ function FormattingToolbar({
406
690
  actions,
407
691
  labels,
408
692
  disabled,
409
- linkOpen,
410
- onLinkOpenChange,
411
- linkMode,
693
+ form,
694
+ onFormChange,
695
+ formMode,
412
696
  onEscape,
413
697
  className,
414
698
  ref
@@ -422,7 +706,8 @@ function FormattingToolbar({
422
706
  action,
423
707
  {
424
708
  active: ACTION_SPECS[action].isActive(current),
425
- idle: ACTION_SPECS[action].isIdle?.(current) ?? false
709
+ idle: ACTION_SPECS[action].isIdle?.(current) ?? false,
710
+ choice: ACTION_SPECS[action].menu?.current(current)
426
711
  }
427
712
  ])
428
713
  )
@@ -431,6 +716,7 @@ function FormattingToolbar({
431
716
  const tabStop = Math.min(stop, actions.length - 1);
432
717
  const buttons = () => Array.from(root.current?.querySelectorAll("button[data-rt-action]") ?? []);
433
718
  const onKeyDown = (event) => {
719
+ if (!(event.target instanceof Node) || !root.current?.contains(event.target)) return;
434
720
  if (event.key === "Escape" && onEscape) {
435
721
  event.preventDefault();
436
722
  onEscape();
@@ -445,15 +731,15 @@ function FormattingToolbar({
445
731
  event.preventDefault();
446
732
  list[next]?.focus();
447
733
  };
448
- const linkField = /* @__PURE__ */ jsxRuntime.jsx(
449
- LinkForm,
450
- {
734
+ const field = (which) => {
735
+ const props = {
451
736
  editor,
452
737
  labels,
453
- onDone: () => onLinkOpenChange(false),
454
- className: linkMode === "inline" ? "w-64 p-2" : void 0
455
- }
456
- );
738
+ onDone: () => onFormChange(null),
739
+ className: formMode === "inline" ? "w-64 p-2" : void 0
740
+ };
741
+ return which === "link" ? /* @__PURE__ */ jsxRuntime.jsx(LinkForm, { ...props }) : /* @__PURE__ */ jsxRuntime.jsx(ImageForm, { ...props });
742
+ };
457
743
  return /* @__PURE__ */ jsxRuntime.jsx(
458
744
  "div",
459
745
  {
@@ -463,15 +749,16 @@ function FormattingToolbar({
463
749
  "aria-orientation": "horizontal",
464
750
  onKeyDown,
465
751
  className: clsx.clsx("flex flex-wrap items-center gap-0.5", className),
466
- children: linkMode === "inline" && linkOpen ? linkField : actions.map((action, index) => {
752
+ children: formMode === "inline" && form ? field(form) : actions.map((action, index) => {
467
753
  const spec = ACTION_SPECS[action];
468
- const { active, idle } = state[action];
469
- const Icon = spec.icon;
754
+ const { active, idle, choice } = state[action];
470
755
  const inert = disabled || idle;
756
+ const shown = active && spec.menu ? spec.menu.choices.find(({ id }) => id === choice) : void 0;
757
+ const Icon = shown?.icon ?? spec.icon;
471
758
  const common = {
472
759
  "data-rt-action": action,
473
760
  "aria-label": labels[action],
474
- "aria-keyshortcuts": ariaShortcut(spec.shortcut),
761
+ "aria-keyshortcuts": spec.shortcut && ariaShortcut(spec.shortcut),
475
762
  "aria-pressed": active,
476
763
  "aria-disabled": inert || void 0,
477
764
  tabIndex: index === tabStop ? 0 : -1,
@@ -480,43 +767,82 @@ function FormattingToolbar({
480
767
  onMouseDown: (e) => e.preventDefault(),
481
768
  className: clsx.clsx(BUTTON, active ? PRESSED : IDLE)
482
769
  };
483
- const icon = /* @__PURE__ */ jsxRuntime.jsx(Icon, { "aria-hidden": true, className: clsx.clsx("h-4 w-4", spec.mirror && "rtl:-scale-x-100") });
770
+ const icon = /* @__PURE__ */ jsxRuntime.jsx(
771
+ Icon,
772
+ {
773
+ "aria-hidden": true,
774
+ className: clsx.clsx(
775
+ "h-4 w-4",
776
+ (shown ? shown.mirror : spec.mirror) && "rtl:-scale-x-100"
777
+ )
778
+ }
779
+ );
484
780
  const tooltip = (child) => /* @__PURE__ */ jsxRuntime.jsx(
485
781
  cosmosReact.Tooltip,
486
782
  {
487
783
  content: labels[action],
488
- shortcut: shortcutKeys(spec.shortcut),
784
+ shortcut: spec.shortcut && shortcutKeys(spec.shortcut),
489
785
  position: "top",
490
786
  children: child
491
787
  },
492
788
  action
493
789
  );
494
- if (action === "link" && linkMode === "popover") {
495
- return /* @__PURE__ */ jsxRuntime.jsxs(cosmosReact.Popover, { open: linkOpen && !disabled, onOpenChange: onLinkOpenChange, children: [
496
- tooltip(/* @__PURE__ */ jsxRuntime.jsx(cosmosReact.Popover.Trigger, { ...common, children: icon })),
497
- /* @__PURE__ */ jsxRuntime.jsx(
498
- cosmosReact.Popover.Content,
790
+ if (spec.menu) {
791
+ return /* @__PURE__ */ jsxRuntime.jsxs(cosmosReact.Menu, { children: [
792
+ tooltip(
793
+ /* @__PURE__ */ jsxRuntime.jsx(
794
+ cosmosReact.Menu.Trigger,
795
+ {
796
+ ...common,
797
+ onClick: (e) => inert && e.preventDefault(),
798
+ onKeyDown: (e) => {
799
+ if (inert && (e.key === "ArrowDown" || e.key === "ArrowUp")) {
800
+ e.preventDefault();
801
+ }
802
+ },
803
+ children: icon
804
+ }
805
+ )
806
+ ),
807
+ /* @__PURE__ */ jsxRuntime.jsx(cosmosReact.Menu.Content, { width: "w-44", children: spec.menu.choices.map((item) => /* @__PURE__ */ jsxRuntime.jsx(
808
+ cosmosReact.Menu.RadioItem,
499
809
  {
500
- "aria-label": labels.linkDialog,
501
- position: "bottom",
502
- width: "w-72",
503
- padding: "p-3",
504
- children: linkField
505
- }
506
- )
810
+ checked: item.id === choice,
811
+ "aria-keyshortcuts": item.shortcut && ariaShortcut(item.shortcut),
812
+ onSelect: () => item.run(editor),
813
+ children: labels[item.id]
814
+ },
815
+ item.id
816
+ )) })
507
817
  ] }, action);
508
818
  }
509
- if (action === "link") {
819
+ if (spec.form && formMode === "popover") {
820
+ return /* @__PURE__ */ jsxRuntime.jsxs(
821
+ cosmosReact.Popover,
822
+ {
823
+ open: form === spec.form && !inert,
824
+ onOpenChange: (open) => onFormChange(open ? spec.form ?? null : null),
825
+ children: [
826
+ tooltip(/* @__PURE__ */ jsxRuntime.jsx(cosmosReact.Popover.Trigger, { ...common, children: icon })),
827
+ /* @__PURE__ */ jsxRuntime.jsx(
828
+ cosmosReact.Popover.Content,
829
+ {
830
+ "aria-label": spec.form === "image" ? labels.imageDialog : labels.linkDialog,
831
+ position: "bottom",
832
+ width: spec.form === "image" ? "w-80" : "w-72",
833
+ padding: "p-3",
834
+ children: field(spec.form)
835
+ }
836
+ )
837
+ ]
838
+ },
839
+ action
840
+ );
841
+ }
842
+ if (spec.form) {
843
+ const opens = spec.form;
510
844
  return tooltip(
511
- /* @__PURE__ */ jsxRuntime.jsx(
512
- "button",
513
- {
514
- type: "button",
515
- ...common,
516
- onClick: () => !disabled && onLinkOpenChange(true),
517
- children: icon
518
- }
519
- )
845
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", ...common, onClick: () => !inert && onFormChange(opens), children: icon })
520
846
  );
521
847
  }
522
848
  return tooltip(
@@ -534,23 +860,40 @@ var DEFAULTS = {
534
860
  linkDialog: "Edit link",
535
861
  linkApply: "Apply",
536
862
  linkRemove: "Remove link",
537
- linkInvalid: "Enter an http(s), mailto or relative link."
863
+ linkInvalid: "Enter an http(s), mailto or relative link.",
864
+ imageDialog: "Image details",
865
+ imageUrl: "Image URL",
866
+ imageAlt: "Description (alt text)",
867
+ imageDecorative: "Decorative image, no description",
868
+ imageApply: "Apply",
869
+ imageInvalid: "Enter an http(s) image address.",
870
+ imageAltRequired: "Describe the image, or mark it decorative."
538
871
  };
872
+ var NAMED_DEFAULTS = Object.fromEntries([
873
+ ...Object.entries(ACTION_SPECS).map(([action, spec]) => [action, spec.label]),
874
+ ...Object.values(ACTION_SPECS).flatMap(
875
+ ({ menu }) => (menu?.choices ?? []).map((choice) => [choice.id, choice.label])
876
+ )
877
+ ]);
539
878
  var resolveLabels = (labels = {}) => {
540
- const actions = Object.fromEntries(
541
- Object.keys(ACTION_SPECS).map((action) => [
542
- action,
543
- labels[action] ?? ACTION_SPECS[action].label
544
- ])
879
+ const named = Object.fromEntries(
880
+ Object.keys(NAMED_DEFAULTS).map((key2) => [key2, labels[key2] ?? NAMED_DEFAULTS[key2]])
545
881
  );
546
882
  return {
547
- ...actions,
883
+ ...named,
548
884
  toolbar: labels.toolbar ?? DEFAULTS.toolbar,
549
885
  linkUrl: labels.linkUrl ?? DEFAULTS.linkUrl,
550
886
  linkDialog: labels.linkDialog ?? DEFAULTS.linkDialog,
551
887
  linkApply: labels.linkApply ?? DEFAULTS.linkApply,
552
888
  linkRemove: labels.linkRemove ?? DEFAULTS.linkRemove,
553
- linkInvalid: labels.linkInvalid ?? DEFAULTS.linkInvalid
889
+ linkInvalid: labels.linkInvalid ?? DEFAULTS.linkInvalid,
890
+ imageDialog: labels.imageDialog ?? DEFAULTS.imageDialog,
891
+ imageUrl: labels.imageUrl ?? DEFAULTS.imageUrl,
892
+ imageAlt: labels.imageAlt ?? DEFAULTS.imageAlt,
893
+ imageDecorative: labels.imageDecorative ?? DEFAULTS.imageDecorative,
894
+ imageApply: labels.imageApply ?? DEFAULTS.imageApply,
895
+ imageInvalid: labels.imageInvalid ?? DEFAULTS.imageInvalid,
896
+ imageAltRequired: labels.imageAltRequired ?? DEFAULTS.imageAltRequired
554
897
  };
555
898
  };
556
899
  var NO_PROPS = { format: "html", setCurrent: () => {
@@ -565,7 +908,7 @@ var createStore = (initial) => {
565
908
  };
566
909
  };
567
910
  var EMPTY_DOC = { type: "doc", content: [{ type: "paragraph" }] };
568
- var readContent = (editor, format) => format === "json" ? editor.getJSON() : editor.isEmpty ? "" : editor.getHTML();
911
+ var readContent = (editor, format) => format === "json" ? withoutDefaultAlignment(editor.getJSON()) : editor.isEmpty ? "" : editor.getHTML();
569
912
  var countCharacters = (editor) => editor.storage.characterCount?.characters() ?? 0;
570
913
  var sameContent = (a, b) => typeof a === "string" || typeof b === "string" ? a === b : JSON.stringify(a) === JSON.stringify(b);
571
914
  var LinkShortcut = core.Extension.create({
@@ -575,12 +918,14 @@ var LinkShortcut = core.Extension.create({
575
918
  return { "Mod-k": () => this.options.open() };
576
919
  }
577
920
  });
921
+ var BlockAlign = TextAlign__default.default.extend({ addKeyboardShortcuts: () => ({}) });
578
922
  var CONTENT_CLASS = [
579
923
  "min-w-0 outline-hidden break-words",
580
924
  "[&>*+*]:mt-2",
581
925
  // Each block follows the direction of what is typed in it, as a Textarea's paragraphs do
582
926
  // (`FIELD_PLAINTEXT`); lists and quotes take a `dir` from `BlockDirection`.
583
- "[&_p]:[unicode-bidi:plaintext] [&_h2]:[unicode-bidi:plaintext] [&_h3]:[unicode-bidi:plaintext]",
927
+ "[&_p]:[unicode-bidi:plaintext] [&_h1]:[unicode-bidi:plaintext] [&_h2]:[unicode-bidi:plaintext] [&_h3]:[unicode-bidi:plaintext]",
928
+ "[&_h1]:text-xl [&_h1]:leading-tight [&_h1]:font-semibold",
584
929
  "[&_h2]:text-lg [&_h2]:leading-snug [&_h2]:font-semibold",
585
930
  "[&_h3]:text-base [&_h3]:leading-snug [&_h3]:font-semibold",
586
931
  "[&_ul]:list-disc [&_ul]:ps-5 [&_ol]:list-decimal [&_ol]:ps-5 [&_li>p]:my-0",
@@ -588,6 +933,13 @@ var CONTENT_CLASS = [
588
933
  "dark:[&_blockquote]:border-gray-600 dark:[&_blockquote]:text-gray-300",
589
934
  "[&_code]:rounded-sm [&_code]:bg-gray-100 [&_code]:px-1 [&_code]:font-mono [&_code]:text-[0.9em]",
590
935
  "dark:[&_code]:bg-gray-800",
936
+ // A code block reads left to right in any page, as `CodeBlock` does; its `code` drops the pill.
937
+ "[&_pre]:overflow-x-auto [&_pre]:rounded-md [&_pre]:bg-gray-100 [&_pre]:p-3 [&_pre]:font-mono [&_pre]:text-[0.9em] [&_pre]:leading-snug",
938
+ "dark:[&_pre]:bg-gray-800 [&_pre]:[direction:ltr] [&_pre]:text-start",
939
+ "[&_pre_code]:bg-transparent [&_pre_code]:p-0 [&_pre_code]:text-[1em]",
940
+ // An image is in the line: it never grows past it, and a selected one shows it.
941
+ "[&_img[src]]:inline-block [&_img[src]]:h-auto [&_img[src]]:max-w-full [&_img[src]]:align-middle",
942
+ "[&_img.ProseMirror-selectednode]:outline-brand-500 [&_img.ProseMirror-selectednode]:outline-2",
591
943
  "[&_a]:text-brand-600 dark:[&_a]:text-brand-400 [&_a]:underline [&_a]:underline-offset-2",
592
944
  "[&_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",
593
945
  "[&_p.is-editor-empty:first-child]:before:text-gray-500 dark:[&_p.is-editor-empty:first-child]:before:text-gray-400",
@@ -632,7 +984,7 @@ function RichTextEditor({
632
984
  onChange: (next) => onChange?.(next),
633
985
  name: "RichTextEditor.value"
634
986
  });
635
- const [linkOpen, setLinkOpen] = react.useState(false);
987
+ const [form, setForm] = react.useState(null);
636
988
  const [characters, setCharacters] = react.useState(0);
637
989
  const [latest] = react.useState(() => createStore(NO_PROPS));
638
990
  const [echoes] = react.useState(() => createStore([]));
@@ -643,7 +995,7 @@ function RichTextEditor({
643
995
  setCurrent,
644
996
  openLink: () => {
645
997
  if (!hasLinkField || !editable) return false;
646
- setLinkOpen(true);
998
+ setForm("link");
647
999
  return true;
648
1000
  }
649
1001
  });
@@ -658,11 +1010,9 @@ function RichTextEditor({
658
1010
  content: current,
659
1011
  extensions: [
660
1012
  StarterKit__default.default.configure({
661
- heading: { levels: [2, 3] },
1013
+ heading: { levels: [1, 2, 3] },
662
1014
  // What the editor doesn't offer is out of the schema, so no paste or value brings it in.
663
- codeBlock: false,
664
1015
  horizontalRule: false,
665
- underline: false,
666
1016
  trailingNode: false,
667
1017
  link: {
668
1018
  openOnClick: false,
@@ -671,6 +1021,10 @@ function RichTextEditor({
671
1021
  isAllowedUri: (url) => isSafeLinkUrl(url)
672
1022
  }
673
1023
  }),
1024
+ // Center and end only, as the sides of the line: a pasted `left`, `right` or `justify` is
1025
+ // dropped, and the start is a block with nothing set.
1026
+ BlockAlign.configure({ types: ["paragraph", "heading"], alignments: ["center", "end"] }),
1027
+ RichTextImage,
674
1028
  extensions.Placeholder.configure({ placeholder: () => latest.get().placeholder ?? "" }),
675
1029
  ...maxLength !== void 0 ? [extensions.CharacterCount.configure({ limit: maxLength })] : [],
676
1030
  LinkShortcut.configure({ open: () => latest.get().openLink() }),
@@ -790,9 +1144,9 @@ function RichTextEditor({
790
1144
  actions,
791
1145
  labels,
792
1146
  disabled,
793
- linkOpen,
794
- onLinkOpenChange: setLinkOpen,
795
- linkMode: "popover",
1147
+ form,
1148
+ onFormChange: setForm,
1149
+ formMode: "popover",
796
1150
  className: "border-b border-gray-200 p-1 dark:border-gray-700"
797
1151
  }
798
1152
  ),
@@ -806,7 +1160,7 @@ function RichTextEditor({
806
1160
  strategy: "fixed",
807
1161
  placement: "top",
808
1162
  offset: 8,
809
- onHide: () => setLinkOpen(false)
1163
+ onHide: () => setForm(null)
810
1164
  },
811
1165
  className: "z-[9999]",
812
1166
  children: /* @__PURE__ */ jsxRuntime.jsx(
@@ -817,11 +1171,11 @@ function RichTextEditor({
817
1171
  actions,
818
1172
  labels,
819
1173
  disabled,
820
- linkOpen,
821
- onLinkOpenChange: setLinkOpen,
822
- linkMode: "inline",
1174
+ form,
1175
+ onFormChange: setForm,
1176
+ formMode: "inline",
823
1177
  onEscape: () => {
824
- setLinkOpen(false);
1178
+ setForm(null);
825
1179
  editor.commands.focus();
826
1180
  },
827
1181
  className: "rounded-lg border border-gray-200 bg-white p-1 shadow-lg dark:border-gray-700 dark:bg-gray-900"
@@ -867,6 +1221,7 @@ function RichTextEditor({
867
1221
  {
868
1222
  type: "hidden",
869
1223
  name,
1224
+ disabled,
870
1225
  value: typeof current === "string" ? current : JSON.stringify(current)
871
1226
  }
872
1227
  )