@blue-patterns/richtext 0.0.0-stage → 0.7.18

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.
Files changed (201) hide show
  1. package/CHANGELOG.md +275 -0
  2. package/LICENSE +22 -0
  3. package/README.md +88 -2
  4. package/astryx.integration.mjs +11 -0
  5. package/blocks/RichTextEditorShowcase.template.mjs +13 -0
  6. package/blocks/RichTextEditorShowcase.tsx +19 -0
  7. package/dist/RichTextEditor.d.ts +282 -0
  8. package/dist/RichTextEditor.d.ts.map +1 -0
  9. package/dist/RichTextEditor.js +657 -0
  10. package/dist/RichTextEditorAutoLinkPlugin.d.ts +83 -0
  11. package/dist/RichTextEditorAutoLinkPlugin.d.ts.map +1 -0
  12. package/dist/RichTextEditorAutoLinkPlugin.js +99 -0
  13. package/dist/RichTextEditorToolbar.d.ts +186 -0
  14. package/dist/RichTextEditorToolbar.d.ts.map +1 -0
  15. package/dist/RichTextEditorToolbar.js +688 -0
  16. package/dist/RichTextView.d.ts +92 -0
  17. package/dist/RichTextView.d.ts.map +1 -0
  18. package/dist/RichTextView.js +267 -0
  19. package/dist/codeLanguages.d.ts +14 -0
  20. package/dist/codeLanguages.d.ts.map +1 -0
  21. package/dist/codeLanguages.js +34 -0
  22. package/dist/codeLineTokens.d.ts +53 -0
  23. package/dist/codeLineTokens.d.ts.map +1 -0
  24. package/dist/codeLineTokens.js +188 -0
  25. package/dist/editorNodes.d.ts +8 -0
  26. package/dist/editorNodes.d.ts.map +1 -0
  27. package/dist/editorNodes.js +30 -0
  28. package/dist/editorTheme.d.ts +7 -0
  29. package/dist/editorTheme.d.ts.map +1 -0
  30. package/dist/editorTheme.js +328 -0
  31. package/dist/index.d.ts +31 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +28 -0
  34. package/dist/internal/CodeBackspacePlugin.d.ts +2 -0
  35. package/dist/internal/CodeBackspacePlugin.d.ts.map +1 -0
  36. package/dist/internal/CodeBackspacePlugin.js +70 -0
  37. package/dist/internal/CodeBlockHeaderPlugin.d.ts +11 -0
  38. package/dist/internal/CodeBlockHeaderPlugin.d.ts.map +1 -0
  39. package/dist/internal/CodeBlockHeaderPlugin.js +204 -0
  40. package/dist/internal/CodePastePlugin.d.ts +5 -0
  41. package/dist/internal/CodePastePlugin.d.ts.map +1 -0
  42. package/dist/internal/CodePastePlugin.js +104 -0
  43. package/dist/internal/CodeSyntaxPlugin.d.ts +8 -0
  44. package/dist/internal/CodeSyntaxPlugin.d.ts.map +1 -0
  45. package/dist/internal/CodeSyntaxPlugin.js +397 -0
  46. package/dist/internal/CodeTabPlugin.d.ts +2 -0
  47. package/dist/internal/CodeTabPlugin.d.ts.map +1 -0
  48. package/dist/internal/CodeTabPlugin.js +256 -0
  49. package/dist/internal/DisabledLinkGuardPlugin.d.ts +4 -0
  50. package/dist/internal/DisabledLinkGuardPlugin.d.ts.map +1 -0
  51. package/dist/internal/DisabledLinkGuardPlugin.js +93 -0
  52. package/dist/internal/ImageSourceCaretPlugin.d.ts +3 -0
  53. package/dist/internal/ImageSourceCaretPlugin.d.ts.map +1 -0
  54. package/dist/internal/ImageSourceCaretPlugin.js +105 -0
  55. package/dist/internal/ListItemShortcutsPlugin.d.ts +6 -0
  56. package/dist/internal/ListItemShortcutsPlugin.d.ts.map +1 -0
  57. package/dist/internal/ListItemShortcutsPlugin.js +314 -0
  58. package/dist/internal/MarkdownExtensionsPlugin.d.ts +17 -0
  59. package/dist/internal/MarkdownExtensionsPlugin.d.ts.map +1 -0
  60. package/dist/internal/MarkdownExtensionsPlugin.js +185 -0
  61. package/dist/internal/SelectAllScopePlugin.d.ts +2 -0
  62. package/dist/internal/SelectAllScopePlugin.d.ts.map +1 -0
  63. package/dist/internal/SelectAllScopePlugin.js +67 -0
  64. package/dist/internal/TableColumnFloorPlugin.d.ts +12 -0
  65. package/dist/internal/TableColumnFloorPlugin.d.ts.map +1 -0
  66. package/dist/internal/TableColumnFloorPlugin.js +84 -0
  67. package/dist/internal/TablePastePlugin.d.ts +9 -0
  68. package/dist/internal/TablePastePlugin.d.ts.map +1 -0
  69. package/dist/internal/TablePastePlugin.js +183 -0
  70. package/dist/internal/TableScrollRegionPlugin.d.ts +9 -0
  71. package/dist/internal/TableScrollRegionPlugin.d.ts.map +1 -0
  72. package/dist/internal/TableScrollRegionPlugin.js +95 -0
  73. package/dist/internal/TaskCheckboxPlugin.d.ts +7 -0
  74. package/dist/internal/TaskCheckboxPlugin.d.ts.map +1 -0
  75. package/dist/internal/TaskCheckboxPlugin.js +193 -0
  76. package/dist/internal/ToolbarLinkPopover.d.ts +74 -0
  77. package/dist/internal/ToolbarLinkPopover.d.ts.map +1 -0
  78. package/dist/internal/ToolbarLinkPopover.js +292 -0
  79. package/dist/linkEditing.d.ts +6 -0
  80. package/dist/linkEditing.d.ts.map +1 -0
  81. package/dist/linkEditing.js +20 -0
  82. package/dist/linkForm.d.ts +28 -0
  83. package/dist/linkForm.d.ts.map +1 -0
  84. package/dist/linkForm.js +137 -0
  85. package/dist/linkUtils.d.ts +27 -0
  86. package/dist/linkUtils.d.ts.map +1 -0
  87. package/dist/linkUtils.js +101 -0
  88. package/dist/markdown.d.ts +17 -0
  89. package/dist/markdown.d.ts.map +1 -0
  90. package/dist/markdown.js +17 -0
  91. package/dist/markdownCharacterReferences.d.ts +135 -0
  92. package/dist/markdownCharacterReferences.d.ts.map +1 -0
  93. package/dist/markdownCharacterReferences.js +1127 -0
  94. package/dist/markdownCodeFence.d.ts +4 -0
  95. package/dist/markdownCodeFence.d.ts.map +1 -0
  96. package/dist/markdownCodeFence.js +167 -0
  97. package/dist/markdownExtensionNode.d.ts +88 -0
  98. package/dist/markdownExtensionNode.d.ts.map +1 -0
  99. package/dist/markdownExtensionNode.js +248 -0
  100. package/dist/markdownExtensions.d.ts +115 -0
  101. package/dist/markdownExtensions.d.ts.map +1 -0
  102. package/dist/markdownExtensions.js +392 -0
  103. package/dist/markdownHardLineBreak.d.ts +25 -0
  104. package/dist/markdownHardLineBreak.d.ts.map +1 -0
  105. package/dist/markdownHardLineBreak.js +59 -0
  106. package/dist/markdownHeading.d.ts +20 -0
  107. package/dist/markdownHeading.d.ts.map +1 -0
  108. package/dist/markdownHeading.js +30 -0
  109. package/dist/markdownLink.d.ts +5 -0
  110. package/dist/markdownLink.d.ts.map +1 -0
  111. package/dist/markdownLink.js +80 -0
  112. package/dist/markdownListExport.d.ts +3 -0
  113. package/dist/markdownListExport.d.ts.map +1 -0
  114. package/dist/markdownListExport.js +68 -0
  115. package/dist/markdownListIndentation.d.ts +8 -0
  116. package/dist/markdownListIndentation.d.ts.map +1 -0
  117. package/dist/markdownListIndentation.js +101 -0
  118. package/dist/markdownOrderedList.d.ts +3 -0
  119. package/dist/markdownOrderedList.d.ts.map +1 -0
  120. package/dist/markdownOrderedList.js +51 -0
  121. package/dist/markdownQuote.d.ts +22 -0
  122. package/dist/markdownQuote.d.ts.map +1 -0
  123. package/dist/markdownQuote.js +32 -0
  124. package/dist/markdownSerializers.d.ts +49 -0
  125. package/dist/markdownSerializers.d.ts.map +1 -0
  126. package/dist/markdownSerializers.js +83 -0
  127. package/dist/markdownSource.d.ts +71 -0
  128. package/dist/markdownSource.d.ts.map +1 -0
  129. package/dist/markdownSource.js +804 -0
  130. package/dist/markdownSpaceRuns.d.ts +24 -0
  131. package/dist/markdownSpaceRuns.d.ts.map +1 -0
  132. package/dist/markdownSpaceRuns.js +147 -0
  133. package/dist/markdownTable.d.ts +35 -0
  134. package/dist/markdownTable.d.ts.map +1 -0
  135. package/dist/markdownTable.js +227 -0
  136. package/dist/markdownTaskList.d.ts +48 -0
  137. package/dist/markdownTaskList.d.ts.map +1 -0
  138. package/dist/markdownTaskList.js +166 -0
  139. package/dist/markdownThematicBreak.d.ts +3 -0
  140. package/dist/markdownThematicBreak.d.ts.map +1 -0
  141. package/dist/markdownThematicBreak.js +55 -0
  142. package/dist/privateAttributes/layerPortalBoundary.stylex.d.ts +12 -0
  143. package/dist/privateAttributes/layerPortalBoundary.stylex.d.ts.map +1 -0
  144. package/dist/privateAttributes/layerPortalBoundary.stylex.js +16 -0
  145. package/dist/richtext.css +213 -0
  146. package/dist/textSemantics.d.ts +3 -0
  147. package/dist/textSemantics.d.ts.map +1 -0
  148. package/dist/textSemantics.js +87 -0
  149. package/package.json +158 -3
  150. package/src/RichTextEditor.doc.mjs +308 -0
  151. package/src/RichTextEditor.tsx +1109 -0
  152. package/src/RichTextEditorAutoLinkPlugin.doc.mjs +43 -0
  153. package/src/RichTextEditorAutoLinkPlugin.tsx +126 -0
  154. package/src/RichTextEditorToolbar.doc.mjs +92 -0
  155. package/src/RichTextEditorToolbar.tsx +871 -0
  156. package/src/RichTextView.doc.mjs +101 -0
  157. package/src/RichTextView.tsx +351 -0
  158. package/src/codeLanguages.ts +42 -0
  159. package/src/codeLineTokens.ts +241 -0
  160. package/src/doc-mjs.d.ts +23 -0
  161. package/src/editorNodes.ts +44 -0
  162. package/src/editorTheme.ts +550 -0
  163. package/src/index.ts +59 -0
  164. package/src/internal/CodeBackspacePlugin.tsx +96 -0
  165. package/src/internal/CodeBlockHeaderPlugin.tsx +282 -0
  166. package/src/internal/CodePastePlugin.tsx +144 -0
  167. package/src/internal/CodeSyntaxPlugin.tsx +493 -0
  168. package/src/internal/CodeTabPlugin.tsx +298 -0
  169. package/src/internal/DisabledLinkGuardPlugin.tsx +93 -0
  170. package/src/internal/ImageSourceCaretPlugin.tsx +159 -0
  171. package/src/internal/ListItemShortcutsPlugin.tsx +404 -0
  172. package/src/internal/MarkdownExtensionsPlugin.tsx +289 -0
  173. package/src/internal/SelectAllScopePlugin.tsx +86 -0
  174. package/src/internal/TableColumnFloorPlugin.tsx +99 -0
  175. package/src/internal/TablePastePlugin.tsx +223 -0
  176. package/src/internal/TableScrollRegionPlugin.tsx +107 -0
  177. package/src/internal/TaskCheckboxPlugin.tsx +246 -0
  178. package/src/internal/ToolbarLinkPopover.tsx +392 -0
  179. package/src/linkEditing.ts +20 -0
  180. package/src/linkForm.tsx +156 -0
  181. package/src/linkUtils.ts +106 -0
  182. package/src/markdown.ts +25 -0
  183. package/src/markdownCharacterReferences.ts +1137 -0
  184. package/src/markdownCodeFence.ts +216 -0
  185. package/src/markdownExtensionNode.ts +339 -0
  186. package/src/markdownExtensions.ts +496 -0
  187. package/src/markdownHardLineBreak.ts +73 -0
  188. package/src/markdownHeading.ts +33 -0
  189. package/src/markdownLink.ts +86 -0
  190. package/src/markdownListExport.ts +85 -0
  191. package/src/markdownListIndentation.ts +105 -0
  192. package/src/markdownOrderedList.ts +60 -0
  193. package/src/markdownQuote.ts +35 -0
  194. package/src/markdownSerializers.ts +108 -0
  195. package/src/markdownSource.ts +1045 -0
  196. package/src/markdownSpaceRuns.ts +164 -0
  197. package/src/markdownTable.ts +309 -0
  198. package/src/markdownTaskList.ts +222 -0
  199. package/src/markdownThematicBreak.ts +66 -0
  200. package/src/privateAttributes/layerPortalBoundary.stylex.ts +18 -0
  201. package/src/textSemantics.ts +104 -0
@@ -0,0 +1,688 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ 'use client';
4
+
5
+ /**
6
+ * @file RichTextEditorToolbar.tsx
7
+ * @input Uses React, @lexical/react (composer context), @lexical/rich-text,
8
+ * @lexical/selection, @lexical/list, @lexical/utils, and the lexical core
9
+ * command constants, plus Astryx Toolbar / IconButton / Selector /
10
+ * ToggleButton / Divider / Layout primitives, and the link popover in
11
+ * ToolbarLinkPopover.
12
+ * @output Exports RichTextEditorToolbar (a compact formatting toolbar with a
13
+ * RichText-owned 4px control gutter, a horizontally scrollable action row,
14
+ * and a link popover anchored to its Link button) and
15
+ * RichTextEditorToolbarProps, plus format and glyph helpers RichText shares
16
+ * internally.
17
+ * @position @blue-patterns/richtext. Drop into RichTextEditor's `toolbar` slot to
18
+ * add a flush top formatting toolbar. Themed via Astryx Toolbar, Selector,
19
+ * IconButton, and ToggleButton, so it inherits the active theme. Base props
20
+ * pass through to the Astryx Toolbar exactly as Toolbar places them
21
+ * (spec:AST-061/DEC-8).
22
+ *
23
+ * SYNC: When modified, update:
24
+ * - /packages/richtext/src/index.ts (exports)
25
+ * - /packages/richtext/src/RichTextEditor.doc.mjs (usage notes)
26
+ * - /packages/richtext/src/RichTextEditor.test.tsx (tests)
27
+ * - /apps/storybook/stories/RichTextEditor.stories.tsx (WithToolbar story)
28
+ *
29
+ * NOTE: `lexical` and `@lexical/*` are OPTIONAL peer dependencies — install them
30
+ * to use this.
31
+ * Behavior mirrors the Lexical playground toolbar (selection sync + format
32
+ * commands); the UI is built from Astryx primitives so it matches the theme.
33
+ *
34
+ * ICONS: Each control resolves its glyph through the core icon registry under a
35
+ * stable `richtext:*` key (see {@link RICHTEXT_ICON_KEYS}) for the active
36
+ * theme, falling back to the bundled inline SVGs below — the library seam
37
+ * `getExtendedIcon` documents. A theme restyles any glyph by naming its key:
38
+ * defineTheme({icons: {'richtext:bold': <MyBoldIcon />}});
39
+ * Undo and redo point backward and forward in time, so they mirror under RTL
40
+ * whichever glyph draws them.
41
+ */
42
+ import { useCallback, useEffect, useRef, useState } from 'react';
43
+ import * as stylex from '@stylexjs/stylex';
44
+ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
45
+ import { $setBlocksType } from '@lexical/selection';
46
+ import { $createHeadingNode, $createQuoteNode, $isHeadingNode, $isQuoteNode } from '@lexical/rich-text';
47
+ import { $isListNode, ListNode, INSERT_ORDERED_LIST_COMMAND, INSERT_UNORDERED_LIST_COMMAND } from '@lexical/list';
48
+ import { $getNearestNodeOfType, mergeRegister } from '@lexical/utils';
49
+ import { $isLinkNode } from '@lexical/link';
50
+ import { Toolbar } from '@blue-patterns/core/Toolbar';
51
+ import { IconButton } from '@blue-patterns/core/IconButton';
52
+ import { Selector } from '@blue-patterns/core/Selector';
53
+ import { ToggleButton } from '@blue-patterns/core/ToggleButton';
54
+ import { Divider } from '@blue-patterns/core/Divider';
55
+ import { HStack } from '@blue-patterns/core/Layout';
56
+ import { getExtendedIcon } from '@blue-patterns/core/Icon';
57
+ import { useThemeName } from '@blue-patterns/core/theme';
58
+ import { isRenderable, rtlStyles } from '@blue-patterns/core/utils';
59
+ import '@blue-patterns/core/theme/tokens.stylex';
60
+ import { fontWeightVars, spacingVars, typeScaleVars } from '@blue-patterns/core/theme/tokens.stylex';
61
+ import { useTranslator } from '@blue-patterns/core/i18n';
62
+ import { FORMAT_TEXT_COMMAND, UNDO_COMMAND, REDO_COMMAND, CAN_UNDO_COMMAND, CAN_REDO_COMMAND, SELECTION_CHANGE_COMMAND, KEY_DOWN_COMMAND, COMMAND_PRIORITY_CRITICAL, COMMAND_PRIORITY_NORMAL, $getSelection, $isNodeSelection, $isRangeSelection, $createParagraphNode } from 'lexical';
63
+ import { ToolbarLinkPopover, isInsertLink, useToolbarLink } from "./internal/ToolbarLinkPopover.js";
64
+ import { $selectedExtensionNodesOnly } from "./markdownExtensionNode.js";
65
+
66
+ /** Block types exposed by the toolbar's format selector. */
67
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; // The link popover's anchor: a box around the Link button that takes no
68
+ // space of its own.
69
+ // A vertical Divider's default 100% height needs a definite parent height.
70
+ // Toolbar slots are sized by their controls instead, so let flexbox stretch
71
+ // these group separators across the slot's resolved height.
72
+ const toolbarDividerStyles = {
73
+ vertical: {
74
+ kSGwAc: "xkh2ocl",
75
+ kZKoxP: "xt7dq6l",
76
+ $$css: true
77
+ }
78
+ };
79
+ const glyphStyles = {
80
+ directional: {
81
+ k1xSpc: "x3nfvp2",
82
+ $$css: true
83
+ }
84
+ };
85
+ const toolbarScrollStyles = {
86
+ compactToolbar: {
87
+ "--astryx-section-padding-inline-start": "xcfn0br",
88
+ "--astryx-section-padding-inline-end": "x1qgjean",
89
+ keoZOQ: "x1uy92hs",
90
+ k1K539: "x199tud",
91
+ $$css: true
92
+ },
93
+ actions: {
94
+ kUk6DE: "x845mor",
95
+ kzqmXN: "xnalus7",
96
+ ks0D6T: "x193iq5w",
97
+ k7Eaqz: "xeuugli",
98
+ kXHlph: "xw2csxc",
99
+ kORKVm: "x10wlt62",
100
+ kvh3hx: "x7p5m3t",
101
+ k5wCbM: "x1597r2g",
102
+ $$css: true
103
+ }
104
+ };
105
+
106
+ /**
107
+ * Stable icon-registry keys for the toolbar's controls. A theme overrides any
108
+ * of these by key: `defineTheme({icons: {'richtext:bold': <MyIcon />}})`.
109
+ * Keys are namespaced (`richtext:*`) to avoid collisions with the core
110
+ * semantic icon set.
111
+ */
112
+ export const RICHTEXT_ICON_KEYS = {
113
+ bold: 'richtext:bold',
114
+ italic: 'richtext:italic',
115
+ underline: 'richtext:underline',
116
+ strikethrough: 'richtext:strikethrough',
117
+ code: 'richtext:code',
118
+ link: 'richtext:link',
119
+ paragraph: 'richtext:paragraph',
120
+ h1: 'richtext:h1',
121
+ h2: 'richtext:h2',
122
+ h3: 'richtext:h3',
123
+ quote: 'richtext:quote',
124
+ bullet: 'richtext:bullet',
125
+ number: 'richtext:number',
126
+ undo: 'richtext:undo',
127
+ redo: 'richtext:redo'
128
+ };
129
+
130
+ /** The inline formats, in the order a toolbar offers them. */
131
+ export const INLINE_FORMAT_ACTIONS = [{
132
+ format: 'bold',
133
+ icon: 'bold'
134
+ }, {
135
+ format: 'italic',
136
+ icon: 'italic'
137
+ }, {
138
+ format: 'underline',
139
+ icon: 'underline'
140
+ }, {
141
+ format: 'strikethrough',
142
+ icon: 'strikethrough'
143
+ }, {
144
+ format: 'code',
145
+ icon: 'code'
146
+ }];
147
+ /** The inline formats' button labels. */
148
+ export function inlineFormatLabels(t) {
149
+ // Literal keys, so the catalog gate can verify every one.
150
+ return {
151
+ bold: t('@astryx.richTextEditor.toolbar.bold'),
152
+ italic: t('@astryx.richTextEditor.toolbar.italic'),
153
+ underline: t('@astryx.richTextEditor.toolbar.underline'),
154
+ strikethrough: t('@astryx.richTextEditor.toolbar.strikethrough'),
155
+ code: t('@astryx.richTextEditor.toolbar.inlineCode')
156
+ };
157
+ }
158
+
159
+ /**
160
+ * The inline formats a range or node selection carries, which their buttons
161
+ * show pressed. A selection of Markdown plugin nodes alone (a range around
162
+ * them, or a node selection from a click) shows their formats. Any other
163
+ * node selection, such as a rule, has none.
164
+ */
165
+ export function $inlineFormatsOf(selection) {
166
+ const formats = new Set();
167
+ const nodesOnly = $selectedExtensionNodesOnly(selection);
168
+ for (const {
169
+ format
170
+ } of INLINE_FORMAT_ACTIONS) {
171
+ if (nodesOnly != null ? nodesOnly.every(node => node.hasFormat(format)) : $isRangeSelection(selection) && selection.hasFormat(format)) {
172
+ formats.add(format);
173
+ }
174
+ }
175
+ return formats;
176
+ }
177
+
178
+ /** Toggles an inline format over the editor's selection. */
179
+ export function formatSelection(editor, format) {
180
+ editor.dispatchCommand(FORMAT_TEXT_COMMAND, format);
181
+ }
182
+
183
+ /**
184
+ * Bundled 16px inline default icons — no external icon dependency. These are
185
+ * the fallbacks used when a theme hasn't registered an override for the
186
+ * corresponding {@link RICHTEXT_ICON_KEYS} entry.
187
+ */
188
+ const defaultToolbarIcons = {
189
+ bold: /*#__PURE__*/_jsx("svg", {
190
+ width: "16",
191
+ height: "16",
192
+ viewBox: "0 0 24 24",
193
+ fill: "none",
194
+ "aria-hidden": "true",
195
+ children: /*#__PURE__*/_jsx("path", {
196
+ d: "M6 4h7a4 4 0 0 1 0 8H6zM6 12h8a4 4 0 0 1 0 8H6z",
197
+ stroke: "currentColor",
198
+ strokeWidth: "2",
199
+ strokeLinejoin: "round"
200
+ })
201
+ }),
202
+ italic: /*#__PURE__*/_jsx("svg", {
203
+ width: "16",
204
+ height: "16",
205
+ viewBox: "0 0 24 24",
206
+ fill: "none",
207
+ "aria-hidden": "true",
208
+ children: /*#__PURE__*/_jsx("path", {
209
+ d: "M10 4h6M8 20h6M14 4l-4 16",
210
+ stroke: "currentColor",
211
+ strokeWidth: "2",
212
+ strokeLinecap: "round"
213
+ })
214
+ }),
215
+ underline: /*#__PURE__*/_jsx("svg", {
216
+ width: "16",
217
+ height: "16",
218
+ viewBox: "0 0 24 24",
219
+ fill: "none",
220
+ "aria-hidden": "true",
221
+ children: /*#__PURE__*/_jsx("path", {
222
+ d: "M6 4v6a6 6 0 0 0 12 0V4M5 21h14",
223
+ stroke: "currentColor",
224
+ strokeWidth: "2",
225
+ strokeLinecap: "round"
226
+ })
227
+ }),
228
+ strikethrough: /*#__PURE__*/_jsx("svg", {
229
+ width: "16",
230
+ height: "16",
231
+ viewBox: "0 0 24 24",
232
+ fill: "none",
233
+ "aria-hidden": "true",
234
+ children: /*#__PURE__*/_jsx("path", {
235
+ d: "M4 12h16M8 6a4 3 0 0 1 8 0M8 16a4 3 0 0 0 8 0",
236
+ stroke: "currentColor",
237
+ strokeWidth: "2",
238
+ strokeLinecap: "round"
239
+ })
240
+ }),
241
+ code: /*#__PURE__*/_jsx("svg", {
242
+ width: "16",
243
+ height: "16",
244
+ viewBox: "0 0 24 24",
245
+ fill: "none",
246
+ "aria-hidden": "true",
247
+ children: /*#__PURE__*/_jsx("path", {
248
+ d: "M9 8l-4 4 4 4M15 8l4 4-4 4",
249
+ stroke: "currentColor",
250
+ strokeWidth: "2",
251
+ strokeLinecap: "round",
252
+ strokeLinejoin: "round"
253
+ })
254
+ }),
255
+ link: /*#__PURE__*/_jsx("svg", {
256
+ width: "16",
257
+ height: "16",
258
+ viewBox: "0 0 24 24",
259
+ fill: "none",
260
+ "aria-hidden": "true",
261
+ children: /*#__PURE__*/_jsx("path", {
262
+ d: "M10 13a5 5 0 0 0 7 0l3-3a5 5 0 0 0-7-7l-1.5 1.5M14 11a5 5 0 0 0-7 0l-3 3a5 5 0 0 0 7 7l1.5-1.5",
263
+ stroke: "currentColor",
264
+ strokeWidth: "2",
265
+ strokeLinecap: "round",
266
+ strokeLinejoin: "round"
267
+ })
268
+ }),
269
+ paragraph: /*#__PURE__*/_jsx(TextGlyph, {
270
+ label: "\xB6"
271
+ }),
272
+ h1: /*#__PURE__*/_jsx(TextGlyph, {
273
+ label: "H1"
274
+ }),
275
+ h2: /*#__PURE__*/_jsx(TextGlyph, {
276
+ label: "H2"
277
+ }),
278
+ h3: /*#__PURE__*/_jsx(TextGlyph, {
279
+ label: "H3"
280
+ }),
281
+ quote: /*#__PURE__*/_jsx("svg", {
282
+ width: "16",
283
+ height: "16",
284
+ viewBox: "0 0 24 24",
285
+ fill: "none",
286
+ "aria-hidden": "true",
287
+ children: /*#__PURE__*/_jsx("path", {
288
+ d: "M6 17h3l2-4V7H5v6h3zM15 17h3l2-4V7h-6v6h3z",
289
+ stroke: "currentColor",
290
+ strokeWidth: "2",
291
+ strokeLinejoin: "round"
292
+ })
293
+ }),
294
+ bullet: /*#__PURE__*/_jsxs("svg", {
295
+ width: "16",
296
+ height: "16",
297
+ viewBox: "0 0 24 24",
298
+ fill: "none",
299
+ "aria-hidden": "true",
300
+ children: [/*#__PURE__*/_jsx("path", {
301
+ d: "M9 6h11M9 12h11M9 18h11",
302
+ stroke: "currentColor",
303
+ strokeWidth: "2",
304
+ strokeLinecap: "round"
305
+ }), /*#__PURE__*/_jsx("circle", {
306
+ cx: "4.5",
307
+ cy: "6",
308
+ r: "1.5",
309
+ fill: "currentColor"
310
+ }), /*#__PURE__*/_jsx("circle", {
311
+ cx: "4.5",
312
+ cy: "12",
313
+ r: "1.5",
314
+ fill: "currentColor"
315
+ }), /*#__PURE__*/_jsx("circle", {
316
+ cx: "4.5",
317
+ cy: "18",
318
+ r: "1.5",
319
+ fill: "currentColor"
320
+ })]
321
+ }),
322
+ number: /*#__PURE__*/_jsxs("svg", {
323
+ width: "16",
324
+ height: "16",
325
+ viewBox: "0 0 24 24",
326
+ fill: "none",
327
+ "aria-hidden": "true",
328
+ children: [/*#__PURE__*/_jsx("path", {
329
+ d: "M10 6h10M10 12h10M10 18h10",
330
+ stroke: "currentColor",
331
+ strokeWidth: "2",
332
+ strokeLinecap: "round"
333
+ }), /*#__PURE__*/_jsx("text", {
334
+ x: "2",
335
+ y: "8",
336
+ fontSize: "6",
337
+ fill: "currentColor",
338
+ children: "1"
339
+ }), /*#__PURE__*/_jsx("text", {
340
+ x: "2",
341
+ y: "14",
342
+ fontSize: "6",
343
+ fill: "currentColor",
344
+ children: "2"
345
+ }), /*#__PURE__*/_jsx("text", {
346
+ x: "2",
347
+ y: "20",
348
+ fontSize: "6",
349
+ fill: "currentColor",
350
+ children: "3"
351
+ })]
352
+ }),
353
+ undo: /*#__PURE__*/_jsx("svg", {
354
+ width: "16",
355
+ height: "16",
356
+ viewBox: "0 0 24 24",
357
+ fill: "none",
358
+ "aria-hidden": "true",
359
+ children: /*#__PURE__*/_jsx("path", {
360
+ d: "M9 7L4 12l5 5M4 12h11a5 5 0 0 1 0 10",
361
+ stroke: "currentColor",
362
+ strokeWidth: "2",
363
+ strokeLinecap: "round",
364
+ strokeLinejoin: "round"
365
+ })
366
+ }),
367
+ redo: /*#__PURE__*/_jsx("svg", {
368
+ width: "16",
369
+ height: "16",
370
+ viewBox: "0 0 24 24",
371
+ fill: "none",
372
+ "aria-hidden": "true",
373
+ children: /*#__PURE__*/_jsx("path", {
374
+ d: "M15 7l5 5-5 5M20 12H9a5 5 0 0 0 0 10",
375
+ stroke: "currentColor",
376
+ strokeWidth: "2",
377
+ strokeLinecap: "round",
378
+ strokeLinejoin: "round"
379
+ })
380
+ })
381
+ };
382
+
383
+ /** Renders a short text label as a toolbar glyph (for heading buttons). */
384
+ function TextGlyph({
385
+ label
386
+ }) {
387
+ return /*#__PURE__*/_jsx("span", {
388
+ "aria-hidden": "true",
389
+ ...{
390
+ className: "x141an7d x1lvx875 xss6m8b"
391
+ },
392
+ children: label
393
+ });
394
+ }
395
+ /**
396
+ * Resolve a toolbar glyph for the active theme: the theme's icon for the
397
+ * stable `richtext:*` key, then a globally registered one, then the bundled
398
+ * inline default.
399
+ */
400
+ function resolveIcon(name, themeName) {
401
+ return getExtendedIcon(RICHTEXT_ICON_KEYS[name], defaultToolbarIcons[name], themeName);
402
+ }
403
+
404
+ /** The glyph resolver for the active theme, as the toolbars draw glyphs. */
405
+ export function useToolbarIcon() {
406
+ const themeName = useThemeName();
407
+ return name => resolveIcon(name, themeName);
408
+ }
409
+
410
+ /** A glyph that points along the reading direction, mirrored under RTL. */
411
+ function DirectionalGlyph({
412
+ children
413
+ }) {
414
+ return /*#__PURE__*/_jsx("span", {
415
+ "aria-hidden": "true",
416
+ ...stylex.props(glyphStyles.directional, rtlStyles.mirror),
417
+ children: children
418
+ });
419
+ }
420
+ /**
421
+ * A composable formatting toolbar for {@link RichTextEditor}, built from Astryx
422
+ * `Toolbar` / `ToggleButton` primitives so it matches the active theme. Reads
423
+ * the current selection to keep the active states in sync and dispatches the
424
+ * standard Lexical formatting commands.
425
+ *
426
+ * Render it inside the editor's `toolbar` slot — it uses
427
+ * `useLexicalComposerContext()` to reach the editor, so it must live within the
428
+ * `LexicalComposer` the editor sets up.
429
+ *
430
+ * @example
431
+ * ```
432
+ * import {RichTextEditor, RichTextEditorToolbar} from '@blue-patterns/richtext';
433
+ *
434
+ * <RichTextEditor
435
+ * label="Notes"
436
+ * toolbar={<RichTextEditorToolbar />}
437
+ * />
438
+ * ```
439
+ */
440
+ export function RichTextEditorToolbar({
441
+ label: labelFromProps,
442
+ headingLevels = ['h1', 'h2', 'h3'],
443
+ size = 'sm',
444
+ hasLink = true,
445
+ promptForUrl,
446
+ hasNewTabLinks = true,
447
+ endContent,
448
+ xstyle,
449
+ ref,
450
+ ...rest
451
+ }) {
452
+ const [editor] = useLexicalComposerContext();
453
+ const icon = useToolbarIcon();
454
+ const t = useTranslator();
455
+ const label = labelFromProps ?? t('@astryx.richTextEditor.toolbar.label');
456
+ // Literal keys, so the catalog gate can verify every one.
457
+ const headingLabels = {
458
+ h1: t('@astryx.richTextEditor.toolbar.heading1'),
459
+ h2: t('@astryx.richTextEditor.toolbar.heading2'),
460
+ h3: t('@astryx.richTextEditor.toolbar.heading3')
461
+ };
462
+ const formatLabels = inlineFormatLabels(t);
463
+ const [activeFormats, setActiveFormats] = useState(() => new Set());
464
+ const [blockType, setBlockType] = useState('paragraph');
465
+ const [isLink, setIsLink] = useState(false);
466
+ const [canUndo, setCanUndo] = useState(false);
467
+ const [canRedo, setCanRedo] = useState(false);
468
+ const [isEditable, setIsEditable] = useState(() => editor.isEditable());
469
+ const linkButtonRef = useRef(null);
470
+ const linkAnchorRef = useRef(null);
471
+ const link = useToolbarLink(editor, {
472
+ buttonRef: linkButtonRef,
473
+ hasNewTabLinks,
474
+ promptForUrl
475
+ });
476
+ const {
477
+ toggle: toggleLink
478
+ } = link;
479
+ const $syncToolbar = useCallback(() => {
480
+ const selection = $getSelection();
481
+ if (!$isRangeSelection(selection) && !$isNodeSelection(selection)) {
482
+ return;
483
+ }
484
+ setActiveFormats($inlineFormatsOf(selection));
485
+ if (!$isRangeSelection(selection)) {
486
+ return;
487
+ }
488
+
489
+ // Link active state — a link is "active" when the caret/selection anchor
490
+ // sits inside a LinkNode (or its immediate parent is one):
491
+ // `$isLinkNode(parent) || $isLinkNode(node)`.
492
+ const node = selection.anchor.getNode();
493
+ setIsLink($isLinkNode(node) || $isLinkNode(node.getParent()));
494
+ const anchorNode = selection.anchor.getNode();
495
+ const element = anchorNode.getKey() === 'root' ? anchorNode : anchorNode.getTopLevelElementOrThrow();
496
+ if ($isListNode(element)) {
497
+ const parentList = $getNearestNodeOfType(anchorNode, ListNode);
498
+ const type = parentList ? parentList.getListType() : element.getListType();
499
+ setBlockType(type === 'number' ? 'number' : 'bullet');
500
+ } else if ($isHeadingNode(element)) {
501
+ setBlockType(element.getTag());
502
+ } else if ($isQuoteNode(element)) {
503
+ setBlockType('quote');
504
+ } else {
505
+ setBlockType('paragraph');
506
+ }
507
+ }, []);
508
+ useEffect(() => {
509
+ return mergeRegister(editor.registerUpdateListener(({
510
+ editorState
511
+ }) => {
512
+ editorState.read($syncToolbar);
513
+ }), editor.registerCommand(SELECTION_CHANGE_COMMAND, () => {
514
+ $syncToolbar();
515
+ return false;
516
+ }, COMMAND_PRIORITY_CRITICAL), editor.registerCommand(CAN_UNDO_COMMAND, payload => {
517
+ setCanUndo(payload);
518
+ return false;
519
+ }, COMMAND_PRIORITY_CRITICAL), editor.registerCommand(CAN_REDO_COMMAND, payload => {
520
+ setCanRedo(payload);
521
+ return false;
522
+ }, COMMAND_PRIORITY_CRITICAL), editor.registerEditableListener(editable => {
523
+ setIsEditable(editable);
524
+ }),
525
+ // Cmd/Ctrl+K opens link insertion. Detection mirrors the Lexical
526
+ // playground's ShortcutsPlugin (isInsertLink → isExactShortcutMatch with
527
+ // CONTROL_OR_META), so it fires only on the exact primary-modifier combo
528
+ // and ignores Cmd+Shift+K etc. Only registered when the link button is
529
+ // enabled. Returns true to consume the event so the browser's own
530
+ // shortcut doesn't also fire.
531
+ hasLink ? editor.registerCommand(KEY_DOWN_COMMAND, event => {
532
+ if (isInsertLink(event)) {
533
+ event.preventDefault();
534
+ toggleLink();
535
+ return true;
536
+ }
537
+ return false;
538
+ }, COMMAND_PRIORITY_NORMAL) : () => {});
539
+ }, [editor, $syncToolbar, hasLink, toggleLink]);
540
+ const setBlock = next => {
541
+ // Selector choices are idempotent: choosing the active block format keeps
542
+ // it active instead of toggling it back to paragraph.
543
+ if (!isEditable || blockType === next) {
544
+ return;
545
+ }
546
+ if (next === 'bullet') {
547
+ editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined);
548
+ return;
549
+ }
550
+ if (next === 'number') {
551
+ editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, undefined);
552
+ return;
553
+ }
554
+ editor.update(() => {
555
+ const selection = $getSelection();
556
+ if (!$isRangeSelection(selection)) {
557
+ return;
558
+ }
559
+ $setBlocksType(selection, () => {
560
+ if (next === 'quote') {
561
+ return $createQuoteNode();
562
+ }
563
+ if (next === 'h1' || next === 'h2' || next === 'h3') {
564
+ return $createHeadingNode(next);
565
+ }
566
+ return $createParagraphNode();
567
+ });
568
+ });
569
+ };
570
+ const blockOptions = [{
571
+ value: 'paragraph',
572
+ label: t('@astryx.richTextEditor.toolbar.paragraph'),
573
+ icon: icon('paragraph')
574
+ }, ...headingLevels.map(level => ({
575
+ value: level,
576
+ label: headingLabels[level],
577
+ icon: icon(level)
578
+ })), {
579
+ value: 'bullet',
580
+ label: t('@astryx.richTextEditor.toolbar.bulletedList'),
581
+ icon: icon('bullet')
582
+ }, {
583
+ value: 'number',
584
+ label: t('@astryx.richTextEditor.toolbar.numberedList'),
585
+ icon: icon('number')
586
+ }, {
587
+ value: 'quote',
588
+ label: t('@astryx.richTextEditor.toolbar.blockQuote'),
589
+ icon: icon('quote')
590
+ }];
591
+ return /*#__PURE__*/_jsxs(_Fragment, {
592
+ children: [/*#__PURE__*/_jsx(Toolbar, {
593
+ ...rest,
594
+ ref: ref,
595
+ label: label,
596
+ size: size,
597
+ xstyle: [toolbarScrollStyles.compactToolbar, xstyle],
598
+ startContent: /*#__PURE__*/_jsxs(HStack, {
599
+ gap: 1,
600
+ role: "group",
601
+ "aria-label": t('@astryx.richTextEditor.toolbar.actions'),
602
+ xstyle: toolbarScrollStyles.actions,
603
+ children: [/*#__PURE__*/_jsx(IconButton, {
604
+ label: t('@astryx.richTextEditor.toolbar.undo'),
605
+ icon: /*#__PURE__*/_jsx(DirectionalGlyph, {
606
+ children: icon('undo')
607
+ }),
608
+ variant: "ghost",
609
+ tooltip: t('@astryx.richTextEditor.toolbar.undo'),
610
+ isDisabled: !isEditable || !canUndo,
611
+ onClick: () => editor.dispatchCommand(UNDO_COMMAND, undefined)
612
+ }), /*#__PURE__*/_jsx(IconButton, {
613
+ label: t('@astryx.richTextEditor.toolbar.redo'),
614
+ icon: /*#__PURE__*/_jsx(DirectionalGlyph, {
615
+ children: icon('redo')
616
+ }),
617
+ variant: "ghost",
618
+ tooltip: t('@astryx.richTextEditor.toolbar.redo'),
619
+ isDisabled: !isEditable || !canRedo,
620
+ onClick: () => editor.dispatchCommand(REDO_COMMAND, undefined)
621
+ }), /*#__PURE__*/_jsx(Divider, {
622
+ orientation: "vertical",
623
+ "aria-label": t('@astryx.richTextEditor.toolbar.historyDivider'),
624
+ xstyle: toolbarDividerStyles.vertical
625
+ }), /*#__PURE__*/_jsx("div", {
626
+ ...{
627
+ className: "x78zum5 x2lah0s"
628
+ },
629
+ children: /*#__PURE__*/_jsx(Selector, {
630
+ label: t('@astryx.richTextEditor.toolbar.blockFormat'),
631
+ isLabelHidden: true,
632
+ variant: "ghost",
633
+ size: size,
634
+ value: blockType,
635
+ options: blockOptions,
636
+ startIcon: icon(blockType),
637
+ isDisabled: !isEditable,
638
+ onChange: value => setBlock(value)
639
+ })
640
+ }), /*#__PURE__*/_jsx(Divider, {
641
+ orientation: "vertical",
642
+ "aria-label": t('@astryx.richTextEditor.toolbar.formatDivider'),
643
+ xstyle: toolbarDividerStyles.vertical
644
+ }), INLINE_FORMAT_ACTIONS.map(action => /*#__PURE__*/_jsx(ToggleButton, {
645
+ label: formatLabels[action.format],
646
+ icon: icon(action.icon),
647
+ size: size,
648
+ isIconOnly: true,
649
+ isPressed: activeFormats.has(action.format),
650
+ isDisabled: !isEditable,
651
+ onPressedChange: () => formatSelection(editor, action.format)
652
+ }, action.format)), hasLink &&
653
+ /*#__PURE__*/
654
+ // The popover anchors to this wrapper, which a press never
655
+ // scales, and finds the button inside it for its handlers.
656
+ _jsx("span", {
657
+ ref: linkAnchorRef,
658
+ ...{
659
+ className: "x3nfvp2 x2lah0s"
660
+ },
661
+ children: /*#__PURE__*/_jsx(ToggleButton, {
662
+ ref: linkButtonRef,
663
+ label: t('@astryx.richTextEditor.toolbar.link'),
664
+ icon: icon('link'),
665
+ size: size,
666
+ isIconOnly: true,
667
+ isPressed: isLink || link.isOpen,
668
+ isDisabled: !isEditable,
669
+ onMouseDown: event => event.preventDefault()
670
+ // The link popover owns a press on the button, and with it
671
+ // the button's popup state; `promptForUrl` replaces both.
672
+ ,
673
+ onPressedChange: promptForUrl ? toggleLink : undefined
674
+ })
675
+ }, "link"), isRenderable(endContent) && /*#__PURE__*/_jsxs(_Fragment, {
676
+ children: [/*#__PURE__*/_jsx(Divider, {
677
+ orientation: "vertical"
678
+ }), endContent]
679
+ })]
680
+ })
681
+ }), hasLink && promptForUrl == null && /*#__PURE__*/_jsx(ToolbarLinkPopover, {
682
+ link: link,
683
+ anchorRef: linkAnchorRef,
684
+ size: size
685
+ })]
686
+ });
687
+ }
688
+ RichTextEditorToolbar.displayName = 'RichTextEditorToolbar';