@riducms/plugin-richtext 0.1.4 → 0.2.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.
@@ -1,187 +1,65 @@
1
1
  <script lang="ts">
2
- import type { FieldComponentProps } from "@riducms/plugin";
3
- import { CodeNode } from "@lexical/code";
4
- import { HistoryExtension } from "@lexical/history";
5
- import { LinkNode } from "@lexical/link";
6
- import { ListExtension } from "@lexical/list";
7
- import { RichTextExtension } from "@lexical/rich-text";
8
- import {
9
- ContentEditable,
10
- HorizontalRuleNode,
11
- HorizontalRulePlugin,
12
- LexicalExtensionComposer,
13
- LinkPlugin,
14
- OnChangePlugin,
15
- } from "@hvniel/lexical-svelte";
16
- import { defineExtension, type EditorState } from "lexical";
17
- import { onDestroy } from "svelte";
18
- import { FieldFrame } from "@riducms/ui";
2
+ import type { PluginFieldProps, PluginFieldBinding } from "@riducms/plugin";
3
+ import type { RichTextDocument } from "@plugin-richtext/document";
4
+ import type { RichTextConfig } from "@plugin-richtext/field/rich-text-config";
5
+ import RichTextEditor from "@plugin-richtext/field/rich-text-editor.svelte";
19
6
 
20
- import "@plugin-richtext/styles/integration.css";
21
- import RichTextBlockToolbar from "@plugin-richtext/menu/rich-text-block-toolbar.svelte";
22
- import RichTextFooter from "@plugin-richtext/field/rich-text-footer.svelte";
23
- import RichTextFloatingToolbar from "@plugin-richtext/toolbar/rich-text-floating-toolbar.svelte";
24
- import RichTextSlashMenu from "@plugin-richtext/menu/rich-text-slash-menu.svelte";
25
- import { parseRichTextConfig, hasRichTextFeature } from "@plugin-richtext/field/rich-text-config";
26
- import { setRichTextAuthoringHost } from "@plugin-richtext/field/rich-text-context.svelte";
27
- import { initialEditorState } from "@plugin-richtext/field/rich-text-document";
28
- import { UploadNode } from "@plugin-richtext/upload/rich-text-upload-node";
29
- import RichTextUploadPlugin from "@plugin-richtext/upload/rich-text-upload-plugin.svelte";
30
- import { RelationshipNode } from "@plugin-richtext/relationship/rich-text-relationship-node";
31
- import RichTextRelationshipPlugin from "@plugin-richtext/relationship/rich-text-relationship-plugin.svelte";
32
-
33
- let { field, form, authoring, i18n }: FieldComponentProps = $props();
34
- const issues = $derived(form.issuesFor(field.path));
35
- const messageID = $derived(`${field.id}-message`);
36
- const hasMessage = $derived(issues.length > 0 || field.admin.description !== undefined);
37
- const placeholderText = $derived(i18n.t("plugin.richtext:editor.placeholder"));
38
- let canvasElement = $state<HTMLElement | null>(null);
39
- // Field identity and plugin config are fixed for this mounted component.
40
- // svelte-ignore state_referenced_locally
41
- const config = parseRichTextConfig(field.plugin?.config);
42
- // The host is stable for this mounted field and decorator descendants inherit it through portals.
43
- // svelte-ignore state_referenced_locally
44
- setRichTextAuthoringHost(authoring);
45
-
46
- const theme = {
47
- heading: {
48
- h1: "ridu-richtext-heading ridu-richtext-h1 mt-[2.1rem] mb-[0.85rem] font-serif text-[2rem] leading-[1.12] tracking-[-0.015em] text-foreground font-normal text-balance max-[34rem]:text-[1.75rem]",
49
- h2: "ridu-richtext-heading ridu-richtext-h2 mt-[2.1rem] mb-[0.85rem] font-serif text-[1.75rem] leading-[1.12] text-foreground font-normal text-balance",
50
- h3: "ridu-richtext-heading ridu-richtext-h3 mt-[2.1rem] mb-[0.85rem] font-sans text-lg leading-[1.35] text-foreground font-semibold text-balance",
7
+ let {
8
+ field,
9
+ form,
10
+ config,
11
+ authoring,
12
+ i18n,
13
+ }: PluginFieldProps<RichTextDocument<unknown>, RichTextConfig> = $props();
14
+ let replacement = $state(0);
15
+ function fingerprint() {
16
+ return JSON.stringify([
17
+ field.schema.id,
18
+ config,
19
+ form.contentLocale,
20
+ form.resource,
21
+ field.rawValue,
22
+ ]);
23
+ }
24
+ // Local editor writes retain Lexical selection/history. Only an external parent
25
+ // replacement (save normalization, locale, schema, group paste) starts a new editor.
26
+ let accepted = fingerprint();
27
+ $effect(() => {
28
+ const incoming = fingerprint();
29
+ if (incoming !== accepted) {
30
+ accepted = incoming;
31
+ replacement++;
32
+ }
33
+ });
34
+ const editorField: PluginFieldBinding<RichTextDocument<unknown>, "plugin"> = {
35
+ get schema() {
36
+ return field.schema;
51
37
  },
52
- code: "ridu-richtext-code-block my-6 block overflow-x-auto rounded-[3px] border border-control-border bg-control px-[1.1rem] py-4 font-mono text-[12.5px] leading-[1.7] text-foreground-strong [tab-size:2]",
53
- hr: "ridu-richtext-rule my-8 block cursor-pointer border-0 border-t border-border py-[0.65rem]",
54
- hrSelected: "is-selected",
55
- link: "ridu-richtext-link cursor-pointer text-brand-primary-soft underline decoration-primary/40 decoration-1 underline-offset-[0.18em] hover:text-primary-hover hover:decoration-current",
56
- list: {
57
- checklist:
58
- "ridu-richtext-checklist mt-1 mb-[1.35rem] list-none ps-0 text-[15.5px] leading-7 text-foreground-body",
59
- listitemChecked: "ridu-richtext-list-item-checked line-through opacity-70",
60
- listitemUnchecked: "ridu-richtext-list-item-unchecked",
61
- listitem: "ridu-richtext-list-item ps-1",
62
- nested: { listitem: "ridu-richtext-list-item-nested" },
63
- ol: "ridu-richtext-list-ordered mt-1 mb-[1.35rem] list-decimal ps-[1.55rem] text-[15.5px] leading-7 text-foreground-body",
64
- ul: "ridu-richtext-list-unordered mt-1 mb-[1.35rem] list-disc ps-[1.55rem] text-[15.5px] leading-7 text-foreground-body",
38
+ get value() {
39
+ return field.value;
65
40
  },
66
- paragraph:
67
- "ridu-richtext-paragraph relative mb-[0.9rem] text-[15.5px] leading-7 text-foreground-body text-pretty last:mb-0",
68
- quote:
69
- "ridu-richtext-quote my-[1.85rem] border-s-2 border-primary/55 py-[0.1rem] ps-5 font-serif text-[19px] italic leading-6 text-foreground-strong text-pretty",
70
- text: {
71
- bold: "ridu-richtext-bold font-semibold",
72
- code: "ridu-richtext-inline-code rounded-[3px] border border-control-border bg-control px-[0.32em] py-[0.12em] font-mono text-[0.86em] text-foreground-strong",
73
- italic: "ridu-richtext-italic italic",
74
- subscript: "ridu-richtext-subscript",
75
- superscript: "ridu-richtext-superscript",
76
- strikethrough: "ridu-richtext-strikethrough line-through",
77
- underline: "ridu-richtext-underline underline underline-offset-[0.15em]",
41
+ get rawValue() {
42
+ return field.rawValue;
78
43
  },
79
- upload: "ridu-richtext-upload m-0",
80
- };
81
-
82
- // Field identity is fixed for this mounted plugin component.
83
- // svelte-ignore state_referenced_locally
84
- const unregisterField = form.register(field.path);
85
- onDestroy(unregisterField);
86
-
87
- // The extension tree is identity-bearing and belongs to this field instance.
88
- // svelte-ignore state_referenced_locally
89
- const extension = defineExtension({
90
- $initialEditorState: initialEditorState(form.get(field.path)),
91
- dependencies: [
92
- RichTextExtension,
93
- HistoryExtension,
94
- ...(hasRichTextFeature(config, "lists") ? [ListExtension] : []),
95
- ],
96
- name: "@riducms/plugin-richtext/editor",
97
- namespace: `ridu-${field.id}`,
98
- nodes: [
99
- ...(hasRichTextFeature(config, "links") ? [LinkNode] : []),
100
- ...(hasRichTextFeature(config, "code") ? [CodeNode] : []),
101
- ...(hasRichTextFeature(config, "horizontal-rule") ? [HorizontalRuleNode] : []),
102
- ...(hasRichTextFeature(config, "uploads") ? [UploadNode] : []),
103
- ...(hasRichTextFeature(config, "relationships") ? [RelationshipNode] : []),
104
- ],
105
- editable: !field.admin.readOnly,
106
- theme,
107
- onError: (error) => {
108
- throw error;
44
+ get issues() {
45
+ return field.issues;
109
46
  },
110
- });
111
-
112
- function changed(editorState: EditorState) {
113
- form.set(field.path, { version: 1, root: editorState.toJSON().root });
114
- }
47
+ get liveValidation() {
48
+ return field.liveValidation;
49
+ },
50
+ get readOnly() {
51
+ return field.readOnly;
52
+ },
53
+ get stale() {
54
+ return field.stale;
55
+ },
56
+ set: (value) => {
57
+ field.set(value);
58
+ accepted = fingerprint();
59
+ },
60
+ };
115
61
  </script>
116
62
 
117
- <div data-field-path={field.path}>
118
- <FieldFrame
119
- controlID={field.id}
120
- label={field.admin.label}
121
- required={field.required}
122
- readOnly={field.admin.readOnly}
123
- description={field.admin.description}
124
- errors={issues.map((issue) => issue.message)}
125
- class="min-w-0"
126
- >
127
- <div
128
- class={[
129
- "ridu-richtext-editor min-w-0 border-b border-transparent pb-[0.65rem] transition-colors duration-150",
130
- issues.length > 0 && "border-b-destructive/22",
131
- field.admin.readOnly && "pt-[1.6rem]",
132
- issues.length > 0 && "has-error",
133
- field.admin.readOnly && "is-read-only",
134
- ]}
135
- aria-invalid={issues.length > 0}
136
- >
137
- <LexicalExtensionComposer {extension} contentEditable={null}>
138
- <div class="relative" bind:this={canvasElement}>
139
- <ContentEditable
140
- id={field.id}
141
- class={[
142
- "ridu-richtext-content relative z-1 min-h-44 pt-[1.35rem] pe-0 pb-3 ps-9 text-foreground-body caret-primary outline-none focus-visible:outline-none max-[34rem]:min-h-36 max-[34rem]:pt-4 max-[34rem]:ps-8",
143
- field.admin.readOnly
144
- ? "min-h-auto pt-0 ps-0 max-[34rem]:min-h-auto max-[34rem]:pt-0 max-[34rem]:ps-0"
145
- : "border-s border-border transition-colors duration-150",
146
- issues.length > 0 && !field.admin.readOnly && "border-s-destructive/72",
147
- ]}
148
- ariaLabel={field.admin.label}
149
- ariaDescribedBy={hasMessage ? messageID : undefined}
150
- ariaInvalid={issues.length > 0}
151
- ariaRequired={field.required}
152
- aria-keyshortcuts="Alt+Shift+ArrowUp Alt+Shift+ArrowDown"
153
- aria-placeholder={placeholderText}
154
- >
155
- {#snippet placeholder(editable)}
156
- <span
157
- class={[
158
- "pointer-events-none absolute top-[1.35rem] start-9 z-0 text-[15.5px] leading-7 text-foreground-placeholder max-[34rem]:top-4 max-[34rem]:start-8",
159
- !editable && "top-0 start-0 max-[34rem]:top-0 max-[34rem]:start-0",
160
- ]}
161
- >
162
- {editable ? placeholderText : i18n.t("plugin.richtext:editor.empty")}
163
- </span>
164
- {/snippet}
165
- </ContentEditable>
166
- </div>
167
- {#if hasRichTextFeature(config, "links")}<LinkPlugin />{/if}
168
- {#if hasRichTextFeature(config, "horizontal-rule")}<HorizontalRulePlugin />{/if}
169
- {#if !field.admin.readOnly}
170
- <RichTextSlashMenu {authoring} {config} />
171
- <RichTextFloatingToolbar />
172
- {#if canvasElement !== null}
173
- <RichTextBlockToolbar anchorElement={canvasElement} {authoring} {config} />
174
- {/if}
175
- {#if hasRichTextFeature(config, "uploads")}
176
- <RichTextUploadPlugin {authoring} {config} {field} />
177
- {/if}
178
- {#if hasRichTextFeature(config, "relationships")}
179
- <RichTextRelationshipPlugin {authoring} {config} {field} />
180
- {/if}
181
- {/if}
182
- <OnChangePlugin onChange={changed} ignoreSelectionChange />
183
- <RichTextFooter />
184
- </LexicalExtensionComposer>
185
- </div>
186
- </FieldFrame>
187
- </div>
63
+ {#key replacement}
64
+ <RichTextEditor field={editorField} {form} {config} {authoring} {i18n} />
65
+ {/key}
@@ -12,8 +12,10 @@
12
12
  const editor = useLexicalComposerContext()[0];
13
13
  const isEditable = useLexicalEditable();
14
14
  const i18n = getAdminI18n();
15
+ let { readOnly = false }: { readOnly?: boolean } = $props();
15
16
 
16
17
  function focusAtEnd() {
18
+ if (!editor.isEditable()) return;
17
19
  editor.focus(
18
20
  () => {
19
21
  editor.update(() => {
@@ -34,10 +36,11 @@
34
36
  }
35
37
  </script>
36
38
 
37
- {#if isEditable()}
39
+ {#if !readOnly}
38
40
  <button
39
- class="mt-[0.55rem] inline-flex cursor-text items-center gap-[0.35rem] border-0 bg-transparent py-[0.35rem] font-mono text-[11px] leading-[1.2] text-foreground-sub transition-colors duration-150 hover:text-foreground-muted focus-visible:rounded focus-visible:text-foreground-muted focus-visible:outline-2 focus-visible:outline-ring/60 focus-visible:outline-offset-3"
41
+ class="mt-[0.55rem] inline-flex cursor-text items-center gap-[0.35rem] border-0 bg-transparent py-[0.35rem] font-mono text-[11px] leading-[1.2] text-foreground-sub transition-colors duration-150 hover:text-foreground-muted focus-visible:rounded focus-visible:text-foreground-muted focus-visible:outline-2 focus-visible:outline-ring/60 focus-visible:outline-offset-3 disabled:cursor-not-allowed disabled:opacity-45"
40
42
  type="button"
43
+ disabled={!isEditable()}
41
44
  onclick={focusAtEnd}
42
45
  >
43
46
  <span aria-hidden="true">+</span>
package/src/index.ts CHANGED
@@ -1,4 +1,6 @@
1
- import { ADMIN_PLUGIN_API_VERSION, defineAdminPlugin, defineFieldPlugin } from "@riducms/plugin";
1
+ import { defineAdminPlugin, definePluginField } from "@riducms/plugin/authoring/v1";
2
+ import { decodeRichTextDocument } from "@plugin-richtext/document-validation";
3
+ import { decodeRichTextConfig } from "@plugin-richtext/field/rich-text-config";
2
4
  import Prism from "prismjs";
3
5
 
4
6
  import { richTextMessages } from "@plugin-richtext/messages";
@@ -8,25 +10,27 @@ browserGlobals.Prism ??= Prism;
8
10
 
9
11
  const { default: RichTextField } = await import("@plugin-richtext/field/rich-text-field.svelte");
10
12
 
11
- export const richTextFieldPlugin = defineFieldPlugin({
12
- type: "plugin",
13
- key: "richtext",
14
- component: RichTextField,
15
- canRender: (field) => field.plugin?.key === "richtext",
16
- });
17
-
18
13
  export const richTextAdminPlugin = defineAdminPlugin({
19
- apiVersion: ADMIN_PLUGIN_API_VERSION,
20
14
  key: "richtext",
21
15
  pairingVersion: 1,
22
- fields: [richTextFieldPlugin],
16
+ fields: {
17
+ richtext: definePluginField({
18
+ component: RichTextField,
19
+ decodeValue: decodeRichTextDocument,
20
+ decodeConfig: decodeRichTextConfig,
21
+ }),
22
+ },
23
23
  messages: richTextMessages,
24
24
  });
25
25
 
26
- export interface RichTextDocument {
27
- version: 1;
28
- root: Record<string, unknown>;
29
- }
26
+ export type {
27
+ RichTextDocument,
28
+ RichTextDocumentInput,
29
+ RichTextNode,
30
+ RichTextRootNode,
31
+ RichTextBlockNode,
32
+ RichTextBlockRenderers,
33
+ } from "#richtext/document";
30
34
 
31
35
  export { richTextMessages } from "@plugin-richtext/messages";
32
36
  export type { RichTextConfig, RichTextFeature } from "@plugin-richtext/field/rich-text-config";
@@ -30,7 +30,10 @@
30
30
  } from "lexical";
31
31
  import type { Attachment } from "svelte/attachments";
32
32
 
33
+ import { getRichTextField } from "@plugin-richtext/field/rich-text-context.svelte";
34
+ import { richTextBlockTypes } from "@plugin-richtext/field/rich-text-blocks";
33
35
  import type { RichTextConfig } from "@plugin-richtext/field/rich-text-config";
36
+ import { OPEN_BLOCK_EDITOR_COMMAND } from "@plugin-richtext/menu/rich-text-commands";
34
37
  import RichTextMenu from "@plugin-richtext/menu/rich-text-menu.svelte";
35
38
  import {
36
39
  buildRichTextOptions,
@@ -63,7 +66,13 @@
63
66
  let restorePickerFocus = true;
64
67
  // The mounted field's authoring surface and feature set are stable.
65
68
  // svelte-ignore state_referenced_locally
66
- const baseOptions = buildRichTextOptions(editor, config, i18n, authoring);
69
+ const baseOptions = buildRichTextOptions(
70
+ editor,
71
+ config,
72
+ i18n,
73
+ authoring,
74
+ richTextBlockTypes(getRichTextField().field)
75
+ );
67
76
  const options = $derived(filterRichTextOptions(baseOptions, query, i18n.language));
68
77
 
69
78
  function isOnToolbar(element: HTMLElement) {
@@ -164,6 +173,13 @@
164
173
  restorePickerFocus = option.restoreEditorFocus;
165
174
  closePicker();
166
175
  if (state === null) return;
176
+ if (option.blockType !== undefined) {
177
+ editor.dispatchCommand(OPEN_BLOCK_EDITOR_COMMAND, {
178
+ blockType: option.blockType,
179
+ position: state,
180
+ });
181
+ return;
182
+ }
167
183
 
168
184
  editor.update(() => {
169
185
  const target = $getNodeByKey(state.targetNodeKey);
@@ -30,3 +30,14 @@ export const REMOVE_RELATIONSHIP_COMMAND: LexicalCommand<NodeKey> = createComman
30
30
 
31
31
  export const UPDATE_UPLOAD_CAPTION_COMMAND: LexicalCommand<UpdateUploadCaptionPayload> =
32
32
  createCommand("UPDATE_UPLOAD_CAPTION_COMMAND");
33
+
34
+ export const OPEN_BLOCK_EDITOR_COMMAND: LexicalCommand<{
35
+ blockType?: string;
36
+ nodeKey?: NodeKey;
37
+ position?: { targetNodeKey: NodeKey; insertBefore: boolean };
38
+ }> = createCommand("OPEN_BLOCK_EDITOR_COMMAND");
39
+ export const DUPLICATE_BLOCK_COMMAND: LexicalCommand<NodeKey> =
40
+ createCommand("DUPLICATE_BLOCK_COMMAND");
41
+ export const REMOVE_BLOCK_COMMAND: LexicalCommand<NodeKey> = createCommand("REMOVE_BLOCK_COMMAND");
42
+ export const MOVE_BLOCK_COMMAND: LexicalCommand<{ nodeKey: NodeKey; direction: -1 | 1 }> =
43
+ createCommand("MOVE_BLOCK_COMMAND");
@@ -6,6 +6,7 @@ import {
6
6
  } from "@lexical/list";
7
7
  import { $createHeadingNode, $createQuoteNode } from "@lexical/rich-text";
8
8
  import { $setBlocksType } from "@lexical/selection";
9
+ import type { SchemaBlockType } from "@riducms/protocol";
9
10
  import type { AdminI18n, FieldAuthoringHost } from "@riducms/plugin";
10
11
  import { INSERT_HORIZONTAL_RULE_COMMAND, MenuOption } from "@hvniel/lexical-svelte";
11
12
  import {
@@ -18,6 +19,7 @@ import {
18
19
 
19
20
  import {
20
21
  OPEN_RELATIONSHIP_BROWSER_COMMAND,
22
+ OPEN_BLOCK_EDITOR_COMMAND,
21
23
  OPEN_UPLOAD_BROWSER_COMMAND,
22
24
  } from "@plugin-richtext/menu/rich-text-commands";
23
25
  import { hasRichTextFeature, type RichTextConfig } from "@plugin-richtext/field/rich-text-config";
@@ -31,6 +33,7 @@ export class RichTextMenuOption extends MenuOption {
31
33
  readonly keyHint: string;
32
34
  readonly preservePlaceholder: boolean;
33
35
  readonly restoreEditorFocus: boolean;
36
+ readonly blockType: string | undefined;
34
37
 
35
38
  constructor(
36
39
  key: string,
@@ -43,6 +46,7 @@ export class RichTextMenuOption extends MenuOption {
43
46
  keyHint?: string;
44
47
  preservePlaceholder?: boolean;
45
48
  restoreEditorFocus?: boolean;
49
+ blockType?: string;
46
50
  } = {}
47
51
  ) {
48
52
  super(key);
@@ -54,6 +58,7 @@ export class RichTextMenuOption extends MenuOption {
54
58
  this.keyHint = options.keyHint ?? "↵";
55
59
  this.preservePlaceholder = options.preservePlaceholder ?? true;
56
60
  this.restoreEditorFocus = options.restoreEditorFocus ?? true;
61
+ this.blockType = options.blockType;
57
62
  }
58
63
  }
59
64
 
@@ -61,7 +66,8 @@ export function buildRichTextOptions(
61
66
  editor: LexicalEditor,
62
67
  config: RichTextConfig,
63
68
  i18n: AdminI18n,
64
- authoring?: FieldAuthoringHost
69
+ authoring?: FieldAuthoringHost,
70
+ blockTypes: readonly SchemaBlockType[] = []
65
71
  ): RichTextMenuOption[] {
66
72
  const options = [
67
73
  new RichTextMenuOption(
@@ -215,6 +221,27 @@ export function buildRichTextOptions(
215
221
  options.push(...relationshipOptions);
216
222
  }
217
223
 
224
+ if (hasRichTextFeature(config, "blocks") && authoring?.beginSchemaDraft !== undefined) {
225
+ options.splice(
226
+ 4,
227
+ 0,
228
+ ...blockTypes.map(
229
+ (type) =>
230
+ new RichTextMenuOption(
231
+ `block-${type.slug}`,
232
+ type.labels.singular,
233
+ i18n.t("plugin.richtext:block.description", { key: type.slug }),
234
+ "▣",
235
+ ["block", type.slug, type.labels.singular],
236
+ () =>
237
+ queueMicrotask(() =>
238
+ editor.dispatchCommand(OPEN_BLOCK_EDITOR_COMMAND, { blockType: type.slug })
239
+ ),
240
+ { restoreEditorFocus: false, blockType: type.slug }
241
+ )
242
+ )
243
+ );
244
+ }
218
245
  return options;
219
246
  }
220
247
 
@@ -9,6 +9,8 @@
9
9
  } from "@hvniel/lexical-svelte";
10
10
  import { $isParagraphNode, type TextNode } from "lexical";
11
11
 
12
+ import { getRichTextField } from "@plugin-richtext/field/rich-text-context.svelte";
13
+ import { richTextBlockTypes } from "@plugin-richtext/field/rich-text-blocks";
12
14
  import type { RichTextConfig } from "@plugin-richtext/field/rich-text-config";
13
15
  import RichTextMenu from "@plugin-richtext/menu/rich-text-menu.svelte";
14
16
  import {
@@ -24,7 +26,13 @@
24
26
  const trigger = createBasicTypeaheadTriggerMatch("/", { allowWhitespace: true, minLength: 0 });
25
27
  // Menu options carry attachment identity and the mounted field's feature set is fixed.
26
28
  // svelte-ignore state_referenced_locally
27
- const baseOptions = buildRichTextOptions(editor, config, i18n, authoring);
29
+ const baseOptions = buildRichTextOptions(
30
+ editor,
31
+ config,
32
+ i18n,
33
+ authoring,
34
+ richTextBlockTypes(getRichTextField().field)
35
+ );
28
36
  let query = $state<string | null>(null);
29
37
  let menuAnchor = $state.raw<{
30
38
  contextElement: HTMLElement;
package/src/messages.ts CHANGED
@@ -2,6 +2,21 @@ import { defineAdminMessages } from "@riducms/plugin";
2
2
 
3
3
  export const richTextMessages = defineAdminMessages({
4
4
  fallback: {
5
+ "block.label": "{label} block",
6
+ "block.select": "Select {label} block",
7
+ "block.edit": "Edit",
8
+ "block.duplicate": "Duplicate",
9
+ "block.remove": "Remove",
10
+ "block.insertTitle": "Insert {label}",
11
+ "block.editTitle": "Edit {label}",
12
+ "block.description": "Structured block · {key}",
13
+ "block.noSummary": "No summary",
14
+ "block.unknown": "Unknown",
15
+ "block.recovery":
16
+ "This block type is no longer configured. Its content is preserved; export it before removing or migrating it.",
17
+ "block.export": "Export block JSON",
18
+ "block.issues": "{count} fields need attention. Edit this block to review.",
19
+ "block.pendingEdit": "Apply or cancel the open block before editing another.",
5
20
  "asset.actions": "Asset actions",
6
21
  "asset.addCaption": "Add a caption…",
7
22
  "asset.captionFor": "Caption for {filename}",
@@ -24,6 +39,9 @@ export const richTextMessages = defineAdminMessages({
24
39
  "editor.editLink": "Edit link",
25
40
  "editor.editNamed": "Edit {name}",
26
41
  "editor.empty": "No content",
42
+ "editor.recovery":
43
+ "Unsupported historical content at {path}. The original JSON is preserved. Export and migrate it before editing this field.",
44
+ "editor.exportDocument": "Export document JSON",
27
45
  "editor.filterBlocks": "Filter blocks",
28
46
  "editor.filterBlocksPlaceholder": "Filter blocks…",
29
47
  "editor.footerHint": "— type / for commands · move with Alt+Shift+↑/↓",
@@ -81,6 +99,23 @@ export const richTextMessages = defineAdminMessages({
81
99
  },
82
100
  translations: {
83
101
  fr: {
102
+ "block.label": "Bloc {label}",
103
+ "block.select": "Sélectionner le bloc {label}",
104
+ "block.edit": "Modifier",
105
+ "block.duplicate": "Dupliquer",
106
+ "block.remove": "Supprimer",
107
+ "block.insertTitle": "Insérer {label}",
108
+ "block.editTitle": "Modifier {label}",
109
+ "block.description": "Bloc structuré · {key}",
110
+ "block.noSummary": "Aucun résumé",
111
+ "block.unknown": "Inconnu",
112
+ "block.recovery":
113
+ "Ce type de bloc n’est plus configuré. Son contenu est conservé ; exportez-le avant de le supprimer ou de le migrer.",
114
+ "block.export": "Exporter le JSON du bloc",
115
+ "block.issues":
116
+ "{count} champs nécessitent votre attention. Modifiez ce bloc pour les vérifier.",
117
+ "block.pendingEdit":
118
+ "Appliquez ou annulez la modification en cours avant de modifier un autre bloc.",
84
119
  "asset.actions": "Actions sur le fichier",
85
120
  "asset.addCaption": "Ajouter une légende…",
86
121
  "asset.captionFor": "Légende de {filename}",
@@ -103,6 +138,9 @@ export const richTextMessages = defineAdminMessages({
103
138
  "editor.editLink": "Modifier le lien",
104
139
  "editor.editNamed": "Modifier {name}",
105
140
  "editor.empty": "Aucun contenu",
141
+ "editor.recovery":
142
+ "Contenu historique non pris en charge à {path}. Le JSON original est conservé. Exportez-le et migrez-le avant de modifier ce champ.",
143
+ "editor.exportDocument": "Exporter le document JSON",
106
144
  "editor.filterBlocks": "Filtrer les blocs",
107
145
  "editor.filterBlocksPlaceholder": "Filtrer les blocs…",
108
146
  "editor.footerHint": "— saisir / pour les commandes · déplacer avec Alt+Maj+↑/↓",
@@ -159,6 +197,21 @@ export const richTextMessages = defineAdminMessages({
159
197
  "relationship.unavailable": "Référence indisponible",
160
198
  },
161
199
  ar: {
200
+ "block.label": "كتلة {label}",
201
+ "block.select": "تحديد كتلة {label}",
202
+ "block.edit": "تعديل",
203
+ "block.duplicate": "تكرار",
204
+ "block.remove": "إزالة",
205
+ "block.insertTitle": "إدراج {label}",
206
+ "block.editTitle": "تعديل {label}",
207
+ "block.description": "كتلة منظمة · {key}",
208
+ "block.noSummary": "لا يوجد ملخص",
209
+ "block.unknown": "غير معروف",
210
+ "block.recovery":
211
+ "لم يعد نوع الكتلة هذا معرّفًا. تم الاحتفاظ بمحتواه؛ صدّره قبل إزالته أو ترحيله.",
212
+ "block.export": "تصدير JSON للكتلة",
213
+ "block.issues": "هناك {count} حقول تحتاج إلى مراجعة. عدّل هذه الكتلة لمراجعتها.",
214
+ "block.pendingEdit": "طبّق التعديل المفتوح أو ألغِه قبل تعديل كتلة أخرى.",
162
215
  "asset.actions": "إجراءات الملف",
163
216
  "asset.addCaption": "إضافة وصف…",
164
217
  "asset.captionFor": "وصف {filename}",
@@ -181,6 +234,9 @@ export const richTextMessages = defineAdminMessages({
181
234
  "editor.editLink": "تعديل الرابط",
182
235
  "editor.editNamed": "تعديل {name}",
183
236
  "editor.empty": "لا يوجد محتوى",
237
+ "editor.recovery":
238
+ "محتوى سابق غير مدعوم في {path}. تم الاحتفاظ ببيانات JSON الأصلية. صدّرها ورحّلها قبل تعديل هذا الحقل.",
239
+ "editor.exportDocument": "تصدير مستند JSON",
184
240
  "editor.filterBlocks": "تصفية الكتل",
185
241
  "editor.filterBlocksPlaceholder": "تصفية الكتل…",
186
242
  "editor.footerHint": "— اكتب / لعرض الأوامر · انقل باستخدام Alt+Shift+↑/↓",
@@ -0,0 +1,6 @@
1
+ import type { Component } from "svelte";
2
+
3
+ export { default as RichText } from "#richtext/rich-text.svelte";
4
+ export type RichTextBlockComponents<Payload extends { blockType: string }> = {
5
+ [Slug in Payload["blockType"]]: Component<{ block: Extract<Payload, { blockType: Slug }> }>;
6
+ };