@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,550 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file editorTheme.ts
5
+ * @input Uses StyleX + Astryx design tokens
6
+ * @output Exports sharedEditorTheme(), which builds a Lexical EditorThemeClasses
7
+ * object mapping Lexical's theme slots to StyleX-generated class names, with
8
+ * document blocks styled like core Markdown (spec:AST-061 FR2–FR4).
9
+ * @position Shared by RichTextEditor.tsx and RichTextView.tsx so editor and
10
+ * read-only view render identically.
11
+ *
12
+ * SYNC: When modified, keep RichTextEditor.tsx and RichTextView.tsx in sync.
13
+ */
14
+
15
+ import * as stylex from '@stylexjs/stylex';
16
+ import {
17
+ colorVars,
18
+ spacingVars,
19
+ radiusVars,
20
+ typographyVars,
21
+ typeScaleVars,
22
+ fontWeightVars,
23
+ borderVars,
24
+ focusVars,
25
+ sizeVars,
26
+ } from '@blue-patterns/core/theme/tokens.stylex';
27
+ import {CODE_SYNTAX_CLASS} from './internal/CodeSyntaxPlugin';
28
+ import type {EditorThemeClasses} from 'lexical';
29
+
30
+ // Document blocks follow core Markdown's typography, spacing, and measure
31
+ // (spec:AST-061 FR2–FR4): type-scale tokens for text, one block spacing table
32
+ // with no margin before the first block or after the last, and prose capped
33
+ // at Markdown's default content width while code and tables span the editor.
34
+ const PROSE_MEASURE = '680px';
35
+
36
+ // Block margins wrap their spacing tokens in calc(). The value is unique to
37
+ // these rules, so its atomic class never matches a margin declared by other
38
+ // code; a stylesheet in a later cascade layer therefore cannot outrank the
39
+ // :first-child and :last-child rules that remove the outer margins.
40
+ const MAJOR_HEADING_BEFORE = `calc(${spacingVars['--spacing-6']})`;
41
+ const MAJOR_HEADING_AFTER = `calc(${spacingVars['--spacing-3']})`;
42
+ const MINOR_HEADING_BEFORE = `calc(${spacingVars['--spacing-4']})`;
43
+ const MINOR_HEADING_AFTER = `calc(${spacingVars['--spacing-2']})`;
44
+ const TEXT_BLOCK_SPACE = `calc(${spacingVars['--spacing-3']})`;
45
+ const WIDE_BLOCK_SPACE = `calc(${spacingVars['--spacing-4']})`;
46
+ const LIST_ITEM_SPACE = `calc(${spacingVars['--spacing-1']})`;
47
+ const TASK_BOX_SIZE = '20px';
48
+ const LIST_ROW_GAP = `calc(${spacingVars['--spacing-0-5']})`;
49
+ const LIST_ROW_FLUSH = `calc(${spacingVars['--spacing-0']})`;
50
+ const NESTED_LIST_SPACE = `calc(${spacingVars['--spacing-2']})`;
51
+ const RULE_SPACE = `calc(${spacingVars['--spacing-6']})`;
52
+ const CODE_SPACE = `calc(${spacingVars['--spacing-3']})`;
53
+ // Core CodeBlock's header (8px, a small control, 8px) overlaps the code's
54
+ // 12px top padding by 8px, so its code starts 8px + control + 12px down.
55
+ const CODE_HEADER_SPACE = `calc(${spacingVars['--spacing-2']} + ${sizeVars['--size-element-sm']} + ${spacingVars['--spacing-3']})`;
56
+ // A block that names no language has no header: core CodeBlock puts its copy
57
+ // button in a column of its own at the inline end (8px, a small control, 8px),
58
+ // beside the code rather than over it, so the code ends that much earlier.
59
+ const CODE_CORNER_SPACE = `calc(${spacingVars['--spacing-4']} + ${sizeVars['--size-element-sm']} + 2 * ${spacingVars['--spacing-2']})`;
60
+
61
+ const editorTheme = stylex.create({
62
+ // Lexical marks every top-level block `dir="auto"`, which picks each
63
+ // block's direction from its first strong character: a Hebrew paragraph in
64
+ // an English document flips to the right, an English one in an Arabic
65
+ // document to the left. Here blocks take the direction of the surface
66
+ // around them, and bidi inside a block still follows the Unicode
67
+ // algorithm. Core Markdown differs: it gives each block the direction of
68
+ // its own text.
69
+ providerDirection: {
70
+ direction: 'inherit',
71
+ },
72
+ paragraph: {
73
+ // The first block's leading margin would stack with the input inset, so
74
+ // the first line aligns with TextArea and the empty-editor placeholder.
75
+ // A Markdown plugin block takes this style and is a decorator, so at
76
+ // either edge of the document it sits beside Lexical's zero-size caret
77
+ // boundary element (see `hr`) and takes no outer margin there either.
78
+ marginBlockStart: {
79
+ default: TEXT_BLOCK_SPACE,
80
+ ':first-child': spacingVars['--spacing-0'],
81
+ ':is([data-lexical-decorator-boundary]:first-child + *)':
82
+ spacingVars['--spacing-0'],
83
+ },
84
+ marginBlockEnd: {
85
+ default: TEXT_BLOCK_SPACE,
86
+ ':last-child': spacingVars['--spacing-0'],
87
+ ':has(+ [data-lexical-decorator-boundary]:last-child)':
88
+ spacingVars['--spacing-0'],
89
+ },
90
+ maxWidth: PROSE_MEASURE,
91
+ // The base reset gives every <p> body weight and primary color; a
92
+ // paragraph inside a table header cell takes the cell's instead.
93
+ fontWeight: 'inherit',
94
+ color: 'inherit',
95
+ },
96
+ h1: {
97
+ fontFamily: typographyVars['--font-family-heading'],
98
+ fontSize: typeScaleVars['--text-heading-1-size'],
99
+ fontWeight: typeScaleVars['--text-heading-1-weight'],
100
+ lineHeight: typeScaleVars['--text-heading-1-leading'],
101
+ marginBlockStart: {
102
+ default: MAJOR_HEADING_BEFORE,
103
+ ':first-child': spacingVars['--spacing-0'],
104
+ },
105
+ marginBlockEnd: {
106
+ default: MAJOR_HEADING_AFTER,
107
+ ':last-child': spacingVars['--spacing-0'],
108
+ },
109
+ maxWidth: PROSE_MEASURE,
110
+ },
111
+ h2: {
112
+ fontFamily: typographyVars['--font-family-heading'],
113
+ fontSize: typeScaleVars['--text-heading-2-size'],
114
+ fontWeight: typeScaleVars['--text-heading-2-weight'],
115
+ lineHeight: typeScaleVars['--text-heading-2-leading'],
116
+ marginBlockStart: {
117
+ default: MAJOR_HEADING_BEFORE,
118
+ ':first-child': spacingVars['--spacing-0'],
119
+ },
120
+ marginBlockEnd: {
121
+ default: MAJOR_HEADING_AFTER,
122
+ ':last-child': spacingVars['--spacing-0'],
123
+ },
124
+ maxWidth: PROSE_MEASURE,
125
+ },
126
+ h3: {
127
+ fontFamily: typographyVars['--font-family-heading'],
128
+ fontSize: typeScaleVars['--text-heading-3-size'],
129
+ fontWeight: typeScaleVars['--text-heading-3-weight'],
130
+ lineHeight: typeScaleVars['--text-heading-3-leading'],
131
+ marginBlockStart: {
132
+ default: MAJOR_HEADING_BEFORE,
133
+ ':first-child': spacingVars['--spacing-0'],
134
+ },
135
+ marginBlockEnd: {
136
+ default: MAJOR_HEADING_AFTER,
137
+ ':last-child': spacingVars['--spacing-0'],
138
+ },
139
+ maxWidth: PROSE_MEASURE,
140
+ },
141
+ h4: {
142
+ fontFamily: typographyVars['--font-family-heading'],
143
+ fontSize: typeScaleVars['--text-heading-4-size'],
144
+ fontWeight: typeScaleVars['--text-heading-4-weight'],
145
+ lineHeight: typeScaleVars['--text-heading-4-leading'],
146
+ marginBlockStart: {
147
+ default: MINOR_HEADING_BEFORE,
148
+ ':first-child': spacingVars['--spacing-0'],
149
+ },
150
+ marginBlockEnd: {
151
+ default: MINOR_HEADING_AFTER,
152
+ ':last-child': spacingVars['--spacing-0'],
153
+ },
154
+ maxWidth: PROSE_MEASURE,
155
+ },
156
+ h5: {
157
+ fontFamily: typographyVars['--font-family-heading'],
158
+ fontSize: typeScaleVars['--text-heading-5-size'],
159
+ fontWeight: typeScaleVars['--text-heading-5-weight'],
160
+ lineHeight: typeScaleVars['--text-heading-5-leading'],
161
+ marginBlockStart: {
162
+ default: MINOR_HEADING_BEFORE,
163
+ ':first-child': spacingVars['--spacing-0'],
164
+ },
165
+ marginBlockEnd: {
166
+ default: MINOR_HEADING_AFTER,
167
+ ':last-child': spacingVars['--spacing-0'],
168
+ },
169
+ maxWidth: PROSE_MEASURE,
170
+ },
171
+ h6: {
172
+ fontFamily: typographyVars['--font-family-heading'],
173
+ fontSize: typeScaleVars['--text-heading-6-size'],
174
+ fontWeight: typeScaleVars['--text-heading-6-weight'],
175
+ lineHeight: typeScaleVars['--text-heading-6-leading'],
176
+ marginBlockStart: {
177
+ default: MINOR_HEADING_BEFORE,
178
+ ':first-child': spacingVars['--spacing-0'],
179
+ },
180
+ marginBlockEnd: {
181
+ default: MINOR_HEADING_AFTER,
182
+ ':last-child': spacingVars['--spacing-0'],
183
+ },
184
+ maxWidth: PROSE_MEASURE,
185
+ },
186
+ quote: {
187
+ marginBlockStart: {
188
+ default: WIDE_BLOCK_SPACE,
189
+ ':first-child': spacingVars['--spacing-0'],
190
+ },
191
+ marginBlockEnd: {
192
+ default: WIDE_BLOCK_SPACE,
193
+ ':last-child': spacingVars['--spacing-0'],
194
+ },
195
+ marginInline: 0,
196
+ maxWidth: PROSE_MEASURE,
197
+ paddingInlineStart: spacingVars['--spacing-4'],
198
+ borderInlineStartWidth: '2px',
199
+ borderInlineStartStyle: 'solid',
200
+ borderInlineStartColor: colorVars['--color-border-emphasized'],
201
+ color: colorVars['--color-text-secondary'],
202
+ },
203
+ ul: {
204
+ marginBlockStart: {
205
+ default: TEXT_BLOCK_SPACE,
206
+ ':first-child': spacingVars['--spacing-0'],
207
+ },
208
+ marginBlockEnd: {
209
+ default: TEXT_BLOCK_SPACE,
210
+ ':last-child': spacingVars['--spacing-0'],
211
+ },
212
+ maxWidth: PROSE_MEASURE,
213
+ paddingInlineStart: spacingVars['--spacing-6'],
214
+ listStylePosition: 'outside',
215
+ },
216
+ ol: {
217
+ marginBlockStart: {
218
+ default: TEXT_BLOCK_SPACE,
219
+ ':first-child': spacingVars['--spacing-0'],
220
+ },
221
+ marginBlockEnd: {
222
+ default: TEXT_BLOCK_SPACE,
223
+ ':last-child': spacingVars['--spacing-0'],
224
+ },
225
+ maxWidth: PROSE_MEASURE,
226
+ paddingInlineStart: spacingVars['--spacing-6'],
227
+ listStylePosition: 'outside',
228
+ },
229
+ // Markers by depth (spec:AST-061 DEC-6): a list nested inside n lists, of
230
+ // either kind, draws disc, circle, or square, or writes decimal,
231
+ // lower-alpha, or lower-roman numbers, for n modulo 3 = 0, 1, 2. Lexical
232
+ // gives every list its tag's class above and the depth class at
233
+ // `depth % 3`; only the depth class names a marker, so no two classes on a
234
+ // list compete for it. A nested list sits alone in its item, so the first-
235
+ // and last-child rules above already take its margins away.
236
+ ulMarker0: {listStyleType: 'disc'},
237
+ ulMarker1: {listStyleType: 'circle'},
238
+ ulMarker2: {listStyleType: 'square'},
239
+ olMarker0: {listStyleType: 'decimal'},
240
+ olMarker1: {listStyleType: 'lower-alpha'},
241
+ olMarker2: {listStyleType: 'lower-roman'},
242
+ // List items space like core Markdown's compact list rows: 4px padding and
243
+ // a 2px gap between rows. Lexical puts a nested list in an item of its own
244
+ // after its parent item; that wrapper adds no top padding and no marker,
245
+ // and its 8px top margin plus the parent row's padding give the 12px
246
+ // Markdown leaves between an item's text and its nested list.
247
+ listItem: {
248
+ paddingBlockStart: {
249
+ default: LIST_ITEM_SPACE,
250
+ ':has(ul)': spacingVars['--spacing-0'],
251
+ ':has(ol)': spacingVars['--spacing-0'],
252
+ },
253
+ paddingBlockEnd: LIST_ITEM_SPACE,
254
+ marginBlockStart: {
255
+ default: LIST_ROW_FLUSH,
256
+ ':has(ul)': NESTED_LIST_SPACE,
257
+ ':has(ol)': NESTED_LIST_SPACE,
258
+ },
259
+ marginBlockEnd: {
260
+ default: LIST_ROW_GAP,
261
+ ':last-child': spacingVars['--spacing-0'],
262
+ },
263
+ // Ensure the marker is shown (some CSS resets set list-style: none on li).
264
+ // A task item (TaskCheckboxPlugin marks it) shows a checkbox instead.
265
+ listStyleType: {
266
+ default: 'inherit',
267
+ ':has(ul)': 'none',
268
+ ':has(ol)': 'none',
269
+ ':is([data-richtext-task])': 'none',
270
+ },
271
+ // A task item's text starts 28px in from the list's edge, after core's
272
+ // small checkbox (20px) and its 8px gap, like core Markdown's task lists:
273
+ // the item cancels the list's bullet indent and pads for the checkbox.
274
+ marginInlineStart: {
275
+ default: LIST_ROW_FLUSH,
276
+ ':is([data-richtext-task])': `calc(-1 * ${spacingVars['--spacing-6']})`,
277
+ },
278
+ paddingInlineStart: {
279
+ default: LIST_ROW_FLUSH,
280
+ ':is([data-richtext-task])': `calc(${TASK_BOX_SIZE} + ${spacingVars['--spacing-2']})`,
281
+ },
282
+ },
283
+ link: {
284
+ color: colorVars['--color-text-accent'],
285
+ textDecoration: 'underline',
286
+ cursor: {
287
+ default: 'pointer',
288
+ ':is(:disabled,[aria-disabled="true"])': 'default',
289
+ },
290
+ },
291
+ textBold: {fontWeight: fontWeightVars['--font-weight-semibold']},
292
+ textItalic: {fontStyle: 'italic'},
293
+ textUnderline: {textDecoration: 'underline'},
294
+ textStrikethrough: {textDecoration: 'line-through'},
295
+ textCode: {
296
+ fontFamily: typographyVars['--font-family-code'],
297
+ backgroundColor: colorVars['--color-background-muted'],
298
+ paddingInline: spacingVars['--spacing-1'],
299
+ borderRadius: radiusVars['--radius-inner'],
300
+ },
301
+ // GFM tables, styled like core Markdown's table: semibold secondary header
302
+ // text, a divider under every row but the last, and a wide table that
303
+ // scrolls inside its own wrapper instead of widening the editor.
304
+ tableScrollableWrapper: {
305
+ overflowX: 'auto',
306
+ maxWidth: '100%',
307
+ // One grid track that may shrink to nothing: the wrapper asks its host
308
+ // for no minimum width, so a grid or flex host never grows to fit a wide
309
+ // table and the wrapper scrolls instead, while a shrink-to-fit host still
310
+ // sizes to the table's natural width.
311
+ display: 'grid',
312
+ gridTemplateColumns: 'minmax(0, 1fr)',
313
+ marginBlockStart: {
314
+ default: WIDE_BLOCK_SPACE,
315
+ ':first-child': spacingVars['--spacing-0'],
316
+ },
317
+ marginBlockEnd: {
318
+ default: WIDE_BLOCK_SPACE,
319
+ ':last-child': spacingVars['--spacing-0'],
320
+ },
321
+ },
322
+ table: {
323
+ borderCollapse: 'collapse',
324
+ width: '100%',
325
+ // The grid's outer edge, matching core Markdown's table: the row and
326
+ // column lines stop at the table's rim, so without this the top, bottom,
327
+ // and sides stay open. `border-collapse: collapse` merges it with the
328
+ // cell lines into one 1px rule rather than doubling up at the edge.
329
+ borderWidth: borderVars['--border-width'],
330
+ borderStyle: 'solid',
331
+ borderColor: colorVars['--color-border'],
332
+ },
333
+ tableRow: {
334
+ borderBottomWidth: '1px',
335
+ borderBottomStyle: 'solid',
336
+ borderBottomColor: {
337
+ default: colorVars['--color-border'],
338
+ ':last-child': 'transparent',
339
+ },
340
+ },
341
+ tableCell: {
342
+ // Column floors (TableColumnFloorPlugin) size the text box, not the
343
+ // padding, as in core Markdown's tables.
344
+ boxSizing: 'content-box',
345
+ paddingBlock: spacingVars['--spacing-2'],
346
+ paddingInline: spacingVars['--spacing-2'],
347
+ textAlign: 'start',
348
+ verticalAlign: 'middle',
349
+ overflowWrap: 'break-word',
350
+ // A line on every cell edge, as core Markdown's table draws with
351
+ // `dividers="grid"`. The row lines come from `tableRow`; this is the
352
+ // column half. The last column draws none, so the rim is the `table`
353
+ // rule's own border rather than two lines stacked at the edge — the
354
+ // same rule core `TableCell` follows (`:last-child` → 0).
355
+ borderInlineEndWidth: {
356
+ default: borderVars['--border-width'],
357
+ ':last-child': 0,
358
+ },
359
+ borderInlineEndStyle: 'solid',
360
+ borderInlineEndColor: colorVars['--color-border'],
361
+ },
362
+ tableCellHeader: {
363
+ fontWeight: fontWeightVars['--font-weight-semibold'],
364
+ color: colorVars['--color-text-secondary'],
365
+ },
366
+ // Cells in a table selection — dragging across cells, Shift+arrow between
367
+ // them, or selecting a row or column — are ringed, for the reason
368
+ // `hrSelected` gives: a person has to see what the next keystroke will
369
+ // replace. Lexical names this class on every highlighted cell and takes it
370
+ // off when the selection goes; left unnamed, a cell inside the selection is
371
+ // painted exactly like one outside it.
372
+ //
373
+ // A ring, not a background wash. A wash sits behind the cell's text and
374
+ // eats into its contrast, and no one token stays clear of the field in
375
+ // every theme: `--color-accent-muted` resolves to the field's own colour
376
+ // under Neutral dark, so the selection would be invisible there, and a
377
+ // 10% accent tint drops Neutral dark's header text from 5.65:1 to 4.25:1.
378
+ // The focus ring is drawn for exactly this job — it clears the field by
379
+ // 5.24:1 at worst across every shipped theme and mode — and sits beside
380
+ // the text rather than behind it, so no cell's contrast moves at all.
381
+ // `outline` takes no space, so the document's geometry is untouched
382
+ // (spec:AST-061 FR9), and the inset offset keeps each ring inside its own
383
+ // cell instead of bleeding over the neighbour it is not marking.
384
+ //
385
+ // `RichTextView` and a read-only editor ring a dragged cell selection too:
386
+ // Lexical's table pointer handler does not ask whether the surface is
387
+ // editable, and a person selects cells in a read surface to copy them, so
388
+ // seeing the selection is as right there. Both surfaces draw the same ring
389
+ // and neither moves a block, so they still render the same document the
390
+ // same way.
391
+ tableCellSelected: {
392
+ outlineWidth: focusVars['--focus-outline-width'],
393
+ outlineStyle: focusVars['--focus-outline-style'],
394
+ outlineColor: focusVars['--focus-outline-color'],
395
+ outlineOffset: `calc(-1 * ${focusVars['--focus-outline-width']})`,
396
+ },
397
+ // Thematic breaks, drawn like core Markdown's rule. A rule is a decorator,
398
+ // and Lexical puts a zero-size caret boundary element beside a decorator at
399
+ // either edge of the document, so a rule there is not literally the first
400
+ // or last child; it still takes no outer margin.
401
+ hr: {
402
+ borderWidth: 0,
403
+ borderTopWidth: borderVars['--border-width'],
404
+ borderTopStyle: 'solid',
405
+ borderTopColor: colorVars['--color-border'],
406
+ marginBlockStart: {
407
+ default: RULE_SPACE,
408
+ ':first-child': spacingVars['--spacing-0'],
409
+ ':is([data-lexical-decorator-boundary]:first-child + *)':
410
+ spacingVars['--spacing-0'],
411
+ },
412
+ marginBlockEnd: {
413
+ default: RULE_SPACE,
414
+ ':last-child': spacingVars['--spacing-0'],
415
+ ':has(+ [data-lexical-decorator-boundary]:last-child)':
416
+ spacingVars['--spacing-0'],
417
+ },
418
+ },
419
+ // A selected rule (click, or arrow onto it) or Markdown plugin node (click)
420
+ // shows the focus ring, so users can see what Backspace will delete.
421
+ hrSelected: {
422
+ outlineWidth: focusVars['--focus-outline-width'],
423
+ outlineStyle: focusVars['--focus-outline-style'],
424
+ outlineColor: focusVars['--focus-outline-color'],
425
+ outlineOffset: focusVars['--focus-outline-offset'],
426
+ },
427
+ // Fenced code in core CodeBlock's frame: syntax background, a border, the
428
+ // code type size, and room at the top for the header that
429
+ // CodeBlockHeaderPlugin draws over a block that names its language. Long
430
+ // lines wrap rather than scroll: a scrolling region inside the editable
431
+ // text cannot take keyboard focus of its own, so keyboard users could not
432
+ // scroll it (axe scrollable-region-focusable).
433
+ code: {
434
+ display: 'block',
435
+ fontFamily: typographyVars['--font-family-code'],
436
+ fontSize: typeScaleVars['--text-code-size'],
437
+ lineHeight: typeScaleVars['--text-code-leading'],
438
+ backgroundColor: 'var(--color-syntax-background)',
439
+ borderWidth: borderVars['--border-width'],
440
+ borderStyle: 'solid',
441
+ borderColor: colorVars['--color-border'],
442
+ borderRadius: radiusVars['--radius-element'],
443
+ paddingBlockStart: {
444
+ default: CODE_SPACE,
445
+ // Only a block whose header names a language; `plaintext` names none,
446
+ // as in core CodeBlock (CodeBlockHeaderPlugin).
447
+ ':is([data-language]:not([data-language="plaintext"]))':
448
+ CODE_HEADER_SPACE,
449
+ },
450
+ paddingBlockEnd: spacingVars['--spacing-3'],
451
+ paddingInlineStart: spacingVars['--spacing-4'],
452
+ paddingInlineEnd: {
453
+ // Room for the corner copy button CodeBlockHeaderPlugin draws over a
454
+ // block that names no language, so no line runs under it.
455
+ default: CODE_CORNER_SPACE,
456
+ ':is([data-language]:not([data-language="plaintext"]))':
457
+ spacingVars['--spacing-4'],
458
+ },
459
+ // Sized like core Markdown's code frame: as wide as its longest line, at
460
+ // least the prose measure (or the whole width when that is narrower), and
461
+ // never wider than the editor, where long lines wrap.
462
+ boxSizing: 'border-box',
463
+ width: 'fit-content',
464
+ minWidth: `min(${PROSE_MEASURE}, 100%)`,
465
+ maxWidth: '100%',
466
+ marginBlockStart: {
467
+ default: WIDE_BLOCK_SPACE,
468
+ ':first-child': spacingVars['--spacing-0'],
469
+ },
470
+ marginBlockEnd: {
471
+ default: WIDE_BLOCK_SPACE,
472
+ ':last-child': spacingVars['--spacing-0'],
473
+ },
474
+ // Long lines wrap exactly as core CodeBlock's wrapped lines do, so a code
475
+ // block is as tall here as in core Markdown (spec:AST-061 FR5, DEC-9).
476
+ whiteSpace: 'pre-wrap',
477
+ wordBreak: 'break-all',
478
+ overflowWrap: 'break-word',
479
+ },
480
+ });
481
+
482
+ /**
483
+ * Builds the Lexical EditorThemeClasses object. Lexical expects plain
484
+ * class-name strings, which `stylex.props(...).className` yields.
485
+ */
486
+ export function sharedEditorTheme(): EditorThemeClasses {
487
+ // A class for a block Lexical may mark `dir="auto"`.
488
+ const block = (...styles: Array<stylex.StyleXStyles>): string =>
489
+ stylex.props(...styles, editorTheme.providerDirection).className ?? '';
490
+ const className = (style: stylex.StyleXStyles): string =>
491
+ stylex.props(style).className ?? '';
492
+ return {
493
+ paragraph: block(editorTheme.paragraph),
494
+ // A Markdown plugin's block node is spaced and measured as a paragraph,
495
+ // as core Markdown spaces its block (RichTextExtensionNode reads it).
496
+ markdownExtensionBlock: block(editorTheme.paragraph),
497
+ heading: {
498
+ h1: block(editorTheme.h1),
499
+ h2: block(editorTheme.h2),
500
+ h3: block(editorTheme.h3),
501
+ h4: block(editorTheme.h4),
502
+ h5: block(editorTheme.h5),
503
+ h6: block(editorTheme.h6),
504
+ },
505
+ quote: block(editorTheme.quote),
506
+ list: {
507
+ ul: block(editorTheme.ul),
508
+ ol: block(editorTheme.ol),
509
+ listitem: stylex.props(editorTheme.listItem).className,
510
+ nested: {
511
+ listitem: stylex.props(editorTheme.listItem).className,
512
+ },
513
+ // Lexical picks the class at `depth % 3` (spec:AST-061 DEC-6).
514
+ ulDepth: [
515
+ className(editorTheme.ulMarker0),
516
+ className(editorTheme.ulMarker1),
517
+ className(editorTheme.ulMarker2),
518
+ ],
519
+ olDepth: [
520
+ className(editorTheme.olMarker0),
521
+ className(editorTheme.olMarker1),
522
+ className(editorTheme.olMarker2),
523
+ ],
524
+ },
525
+ link: stylex.props(editorTheme.link).className,
526
+ text: {
527
+ bold: stylex.props(editorTheme.textBold).className,
528
+ italic: stylex.props(editorTheme.textItalic).className,
529
+ underline: stylex.props(editorTheme.textUnderline).className,
530
+ strikethrough: stylex.props(editorTheme.textStrikethrough).className,
531
+ code: stylex.props(editorTheme.textCode).className,
532
+ },
533
+ // The plain class lets CodeSyntaxPlugin's highlight rules find code blocks.
534
+ code: `${block(editorTheme.code)} ${CODE_SYNTAX_CLASS}`,
535
+ hr: stylex.props(editorTheme.hr).className,
536
+ hrSelected: stylex.props(editorTheme.hrSelected).className,
537
+ // MarkdownExtensionsPlugin puts it on a selected plugin node.
538
+ markdownExtensionSelected: stylex.props(editorTheme.hrSelected).className,
539
+ table: block(editorTheme.table),
540
+ // Lexical stamps rows and cells `dir="auto"` too, which would set a cell
541
+ // of English text left-to-right inside a right-to-left document.
542
+ tableRow: block(editorTheme.tableRow),
543
+ tableCell: block(editorTheme.tableCell),
544
+ tableCellHeader: block(editorTheme.tableCellHeader),
545
+ // Added on top of the cell's own class by Lexical while the cell is in a
546
+ // table selection, so it carries no direction of its own.
547
+ tableCellSelected: className(editorTheme.tableCellSelected),
548
+ tableScrollableWrapper: block(editorTheme.tableScrollableWrapper),
549
+ };
550
+ }
package/src/index.ts ADDED
@@ -0,0 +1,59 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ 'use client';
4
+
5
+ /**
6
+ * @file index.ts
7
+ * @input Imports RichTextEditor and RichTextView components and their types
8
+ * @output Public API surface for @blue-patterns/richtext
9
+ * @position Package entry point (barrel) for @blue-patterns/richtext — the
10
+ * Lexical-based rich text editor and viewer, promoted out of
11
+ * @blue-patterns/lab so it can be canaried independently.
12
+ *
13
+ * Exports only what a caller builds with (spec:AST-061/DEC-8): the generated
14
+ * editor theme classes and the autolink regular expressions stay internal,
15
+ * and Lexical's `Transformer` type comes from `@lexical/markdown`.
16
+ * `publicSurface.test.ts` holds the runtime list.
17
+ *
18
+ * SYNC: When modified, update this header, /packages/richtext/README.md,
19
+ * /packages/richtext/src/publicSurface.test.ts, and the editor component doc
20
+ * files.
21
+ */
22
+
23
+ export {RichTextEditor} from './RichTextEditor';
24
+ export type {
25
+ RichTextEditorProps,
26
+ RichTextEditorRef,
27
+ RichTextEditorStatus,
28
+ RichTextEditorStatusType,
29
+ RichTextEditorSize,
30
+ } from './RichTextEditor';
31
+
32
+ export {RichTextView} from './RichTextView';
33
+ export type {RichTextViewProps} from './RichTextView';
34
+
35
+ export {
36
+ markdownToEditorStateJSON,
37
+ editorStateJSONToMarkdown,
38
+ } from './markdownSerializers';
39
+ export type {MarkdownSerializerOptions} from './markdownSerializers';
40
+ export {
41
+ createRichTextExtension,
42
+ RichTextExtensionError,
43
+ } from './markdownExtensions';
44
+ export type {RichTextMarkdownExtension} from './markdownExtensions';
45
+
46
+ export {
47
+ RichTextEditorToolbar,
48
+ RICHTEXT_ICON_KEYS,
49
+ } from './RichTextEditorToolbar';
50
+ export type {RichTextEditorToolbarProps} from './RichTextEditorToolbar';
51
+
52
+ export {
53
+ RichTextEditorAutoLinkPlugin,
54
+ DEFAULT_LINK_MATCHERS,
55
+ NEW_TAB_LINK_ATTRIBUTES,
56
+ } from './RichTextEditorAutoLinkPlugin';
57
+ export type {RichTextEditorAutoLinkPluginProps} from './RichTextEditorAutoLinkPlugin';
58
+
59
+ export {sanitizeUrl, validateUrl} from './linkUtils';