@crystallize/design-system 1.9.0 → 1.11.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.
Files changed (30) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/index.css +15 -17
  3. package/dist/index.d.ts +10 -3
  4. package/dist/index.js +528 -470
  5. package/dist/index.mjs +477 -423
  6. package/package.json +1 -1
  7. package/src/rich-text-editor/i18n/i18n.test.ts +14 -0
  8. package/src/rich-text-editor/i18n/index.tsx +64 -0
  9. package/src/rich-text-editor/i18n/translations/en.ts +66 -0
  10. package/src/rich-text-editor/i18n/types.ts +62 -0
  11. package/src/rich-text-editor/model/crystallize-to-lexical.ts +29 -1
  12. package/src/rich-text-editor/model/lexical-to-crystallize.ts +54 -29
  13. package/src/rich-text-editor/plugins/ActionsPlugin/index.tsx +5 -22
  14. package/src/rich-text-editor/plugins/CodeActionMenuPlugin/components/CopyButton/index.tsx +4 -1
  15. package/src/rich-text-editor/plugins/CodeActionMenuPlugin/components/PrettierButton/index.tsx +11 -1
  16. package/src/rich-text-editor/plugins/CodeActionMenuPlugin/index.tsx +2 -1
  17. package/src/rich-text-editor/plugins/FloatingLinkEditorPlugin/index.tsx +23 -5
  18. package/src/rich-text-editor/plugins/FloatingTextFormatToolbarPlugin/index.tsx +21 -10
  19. package/src/rich-text-editor/plugins/TabFocusPlugin/index.tsx +4 -12
  20. package/src/rich-text-editor/plugins/TableActionMenuPlugin/index.tsx +23 -14
  21. package/src/rich-text-editor/plugins/ToolbarPlugin/index.tsx +33 -33
  22. package/src/rich-text-editor/plugins/ToolbarPlugin/insert-table.tsx +6 -4
  23. package/src/rich-text-editor/rich-text-editor.css +6 -0
  24. package/src/rich-text-editor/rich-text-editor.stories.tsx +38 -0
  25. package/src/rich-text-editor/rich-text-editor.tsx +15 -9
  26. package/src/rich-text-editor/tests/rich-text-editor-model-conversions.test.tsx +23 -1
  27. package/src/rich-text-editor/types/crystallize-rich-text-types/index.ts +2 -4
  28. package/src/rich-text-editor/ui/LinkPreview.tsx +3 -1
  29. package/src/rich-text-editor/ui/ContentEditable.css +0 -13
  30. package/src/rich-text-editor/ui/ContentEditable.tsx +0 -15
@@ -25,6 +25,8 @@ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext
25
25
  import { mergeRegister } from '@lexical/utils';
26
26
 
27
27
  import { IconButton } from '../../../icon-button';
28
+ import { useTr } from '../../i18n';
29
+ import { IS_APPLE } from '../../utils/environment';
28
30
  import { getDOMRangeRect } from '../../utils/getDOMRangeRect';
29
31
  import { getSelectedNode } from '../../utils/getSelectedNode';
30
32
  import { setFloatingElemPosition } from '../../utils/setFloatingElemPosition';
@@ -53,6 +55,7 @@ function TextFormatFloatingToolbar({
53
55
  isUnderline: boolean;
54
56
  }): JSX.Element {
55
57
  const popupCharStylesEditorRef = useRef<HTMLDivElement | null>(null);
58
+ const tr = useTr();
56
59
 
57
60
  const insertLink = useCallback(() => {
58
61
  if (!isLink) {
@@ -139,7 +142,8 @@ function TextFormatFloatingToolbar({
139
142
  editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'bold');
140
143
  }}
141
144
  style={{ padding: 0, overflow: 'hidden' }}
142
- aria-label="Format text as bold"
145
+ title={tr(IS_APPLE ? 'actionFormatAsStrongTitleApple' : 'actionFormatAsStrongTitle')}
146
+ aria-label={tr('actionFormatAsStrongLabel')}
143
147
  >
144
148
  <i
145
149
  className={`format bold w-full h-full bg-[length:18px_18px] bg-no-repeat bg-center ${
@@ -152,7 +156,8 @@ function TextFormatFloatingToolbar({
152
156
  onClick={() => {
153
157
  editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'italic');
154
158
  }}
155
- aria-label="Format text as italics"
159
+ title={tr('actionFormatAsEmphasizedTitle')}
160
+ aria-label={tr('actionFormatAsEmphasizedLabel')}
156
161
  >
157
162
  <i
158
163
  className={`format italic w-full h-full bg-[length:18px_18px] bg-no-repeat bg-center ${
@@ -165,7 +170,8 @@ function TextFormatFloatingToolbar({
165
170
  onClick={() => {
166
171
  editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'underline');
167
172
  }}
168
- aria-label="Format text to underlined"
173
+ title={tr('actionFormatAsUnderlinedTitle')}
174
+ aria-label={tr('actionFormatAsUnderlinedLabel')}
169
175
  >
170
176
  <i
171
177
  className={`format underline w-full h-full bg-[length:18px_18px] bg-no-repeat bg-center ${
@@ -178,7 +184,8 @@ function TextFormatFloatingToolbar({
178
184
  onClick={() => {
179
185
  editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'strikethrough');
180
186
  }}
181
- aria-label="Format text with a strikethrough"
187
+ title={tr('actionFormatWithStrikethroughTitle')}
188
+ aria-label={tr('actionFormatWithStrikethroughLabel')}
182
189
  >
183
190
  <i
184
191
  className={`format strikethrough w-full h-full bg-[length:18px_18px] bg-no-repeat bg-center ${
@@ -191,8 +198,8 @@ function TextFormatFloatingToolbar({
191
198
  onClick={() => {
192
199
  editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'subscript');
193
200
  }}
194
- title="Subscript"
195
- aria-label="Format Subscript"
201
+ title={tr('actionFormatWithSubscriptTitle')}
202
+ aria-label={tr('actionFormatWithSubscriptLabel')}
196
203
  >
197
204
  <i
198
205
  className={`format subscript w-full h-full bg-[length:18px_18px] bg-no-repeat bg-center ${
@@ -205,8 +212,8 @@ function TextFormatFloatingToolbar({
205
212
  onClick={() => {
206
213
  editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'superscript');
207
214
  }}
208
- title="Superscript"
209
- aria-label="Format Superscript"
215
+ title={tr('actionFormatWithSuperscriptTitle')}
216
+ aria-label={tr('actionFormatWithSuperscriptLabel')}
210
217
  >
211
218
  <i
212
219
  className={`format superscript w-full h-full bg-[length:18px_18px] bg-no-repeat bg-center ${
@@ -219,7 +226,7 @@ function TextFormatFloatingToolbar({
219
226
  onClick={() => {
220
227
  editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'code');
221
228
  }}
222
- aria-label="Insert code block"
229
+ aria-label={tr('actionInsertCodeBlock')}
223
230
  >
224
231
  <i
225
232
  className={`format code w-full h-full bg-[length:18px_18px] bg-no-repeat bg-center ${
@@ -227,7 +234,11 @@ function TextFormatFloatingToolbar({
227
234
  }`}
228
235
  />
229
236
  </IconButton>
230
- <IconButton style={{ padding: 0, overflow: 'hidden' }} onClick={insertLink} aria-label="Insert link">
237
+ <IconButton
238
+ style={{ padding: 0, overflow: 'hidden' }}
239
+ onClick={insertLink}
240
+ aria-label={tr('actionInsertlink')}
241
+ >
231
242
  <i
232
243
  className={`format link w-full h-full bg-[length:18px_18px] bg-no-repeat bg-center ${
233
244
  isLink ? 'bg-purple-50-900 opacity-100' : 'opacity-60'
@@ -6,14 +6,9 @@
6
6
  *
7
7
  */
8
8
 
9
- import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
10
- import {
11
- $getSelection,
12
- $isRangeSelection,
13
- $setSelection,
14
- FOCUS_COMMAND,
15
- } from 'lexical';
16
- import {useEffect} from 'react';
9
+ import { useEffect } from 'react';
10
+ import { $getSelection, $isRangeSelection, $setSelection, FOCUS_COMMAND } from 'lexical';
11
+ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
17
12
 
18
13
  const COMMAND_PRIORITY_LOW = 1;
19
14
  const TAB_TO_FOCUS_INTERVAL = 100;
@@ -48,10 +43,7 @@ export default function TabFocusPlugin(): null {
48
43
  (event: FocusEvent) => {
49
44
  const selection = $getSelection();
50
45
  if ($isRangeSelection(selection)) {
51
- if (
52
- lastTabKeyDownTimestamp + TAB_TO_FOCUS_INTERVAL >
53
- event.timeStamp
54
- ) {
46
+ if (lastTabKeyDownTimestamp + TAB_TO_FOCUS_INTERVAL > event.timeStamp) {
55
47
  $setSelection(selection.clone());
56
48
  }
57
49
  }
@@ -33,6 +33,7 @@ import {
33
33
  import { DropdownMenu } from '../../../dropdown-menu';
34
34
  import { IconButton } from '../../../icon-button';
35
35
  import { Icon } from '../../../iconography';
36
+ import { useTr } from '../../i18n';
36
37
 
37
38
  type TableStats = {
38
39
  rows: number;
@@ -51,6 +52,7 @@ function TableActionMenu({ tableCellNode: _tableCellNode, tableStats }: TableCel
51
52
  columns: 1,
52
53
  rows: 1,
53
54
  });
55
+ const tr = useTr();
54
56
 
55
57
  useEffect(() => {
56
58
  return editor.registerMutationListener(TableCellNode, nodeMutations => {
@@ -254,41 +256,48 @@ function TableActionMenu({ tableCellNode: _tableCellNode, tableStats }: TableCel
254
256
  return (
255
257
  <>
256
258
  <DropdownMenu.Item onSelect={() => insertTableRowAtSelection(false)}>
257
- Insert {selectionCounts.rows === 1 ? 'row' : `${selectionCounts.rows} rows`} above
259
+ {tr('actionTableInsertRowsAbove', selectionCounts.rows)}
258
260
  </DropdownMenu.Item>
259
261
  <DropdownMenu.Item onSelect={() => insertTableRowAtSelection(true)}>
260
- Insert {selectionCounts.rows === 1 ? 'row' : `${selectionCounts.rows} rows`} below
262
+ {tr('actionTableInsertRowsBelow', selectionCounts.rows)}
261
263
  </DropdownMenu.Item>
262
264
  <DropdownMenu.Item onSelect={() => insertTableColumnAtSelection(false)}>
263
- Insert {selectionCounts.columns === 1 ? 'column' : `${selectionCounts.columns} columns`} left
265
+ {tr('actionTableInsertColumnsBefore', selectionCounts.columns)}
264
266
  </DropdownMenu.Item>
265
267
  <DropdownMenu.Item onSelect={() => insertTableColumnAtSelection(true)}>
266
- Insert {selectionCounts.columns === 1 ? 'column' : `${selectionCounts.columns} columns`} right
268
+ {tr('actionTableInsertColumnsAfter', selectionCounts.columns)}
267
269
  </DropdownMenu.Item>
268
270
  <DropdownMenu.Item onSelect={() => toggleTableRowIsHeader()}>
269
- {(tableCellNode.__headerState & TableCellHeaderStates.ROW) === TableCellHeaderStates.ROW ? 'Remove' : 'Add'} row
270
- header
271
+ {tr(
272
+ (tableCellNode.__headerState & TableCellHeaderStates.ROW) === TableCellHeaderStates.ROW
273
+ ? 'actionTableRemoveRowHeader'
274
+ : 'actionTableAddRowHeader',
275
+ )}
271
276
  </DropdownMenu.Item>
272
277
  <DropdownMenu.Item onSelect={() => toggleTableColumnIsHeader()}>
273
- {(tableCellNode.__headerState & TableCellHeaderStates.COLUMN) === TableCellHeaderStates.COLUMN
274
- ? 'Remove'
275
- : 'Add'}{' '}
276
- column header
278
+ {tr(
279
+ (tableCellNode.__headerState & TableCellHeaderStates.COLUMN) === TableCellHeaderStates.COLUMN
280
+ ? 'actionTableRemoveColumnHeader'
281
+ : 'actionTableAddColumnHeader',
282
+ )}
277
283
  </DropdownMenu.Item>
278
284
  <DropdownMenu.Separator />
279
285
  {tableStats.columns > 1 && (
280
- <DropdownMenu.Item onSelect={() => deleteTableColumnAtSelection()}>Delete column</DropdownMenu.Item>
286
+ <DropdownMenu.Item onSelect={() => deleteTableColumnAtSelection()}>
287
+ {tr('actionTableDeleteColumn')}
288
+ </DropdownMenu.Item>
281
289
  )}
282
290
  {tableStats.rows > 1 && (
283
- <DropdownMenu.Item onSelect={() => deleteTableRowAtSelection()}>Delete row</DropdownMenu.Item>
291
+ <DropdownMenu.Item onSelect={() => deleteTableRowAtSelection()}>{tr('actionTableDeleteRow')}</DropdownMenu.Item>
284
292
  )}
285
- <DropdownMenu.Item onSelect={() => deleteTableAtSelection()}>Delete table</DropdownMenu.Item>
293
+ <DropdownMenu.Item onSelect={() => deleteTableAtSelection()}>{tr('actionTableDeleteTable')}</DropdownMenu.Item>
286
294
  </>
287
295
  );
288
296
  }
289
297
 
290
298
  function TableCellActionMenuContainer({ anchorElem }: { anchorElem: HTMLElement }) {
291
299
  const [editor] = useLexicalComposerContext();
300
+ const tr = useTr();
292
301
 
293
302
  const menuButtonRef = useRef(null);
294
303
 
@@ -393,7 +402,7 @@ function TableCellActionMenuContainer({ anchorElem }: { anchorElem: HTMLElement
393
402
  onOpenChange={isOpen => setIsMenuOpen(isOpen)}
394
403
  content={<TableActionMenu tableCellNode={tableCellNode} tableStats={tableStats} />}
395
404
  >
396
- <IconButton size="xs" className="table-cell-action-button">
405
+ <IconButton size="xs" className="table-cell-action-button" aria-label={tr('actionTableOpenOptions')}>
397
406
  <Icon.Arrow />
398
407
  </IconButton>
399
408
  </DropdownMenu.Root>
@@ -58,6 +58,7 @@ import { Dialog } from '../../../dialog';
58
58
  import { DropdownMenu } from '../../../dropdown-menu';
59
59
  import { IconButton } from '../../../icon-button';
60
60
  import { Icon } from '../../../iconography';
61
+ import { useTr } from '../../i18n';
61
62
  import type { CrystallizeRichTextActionMenuItem } from '../../types/types';
62
63
  import { IS_APPLE } from '../../utils/environment';
63
64
  import { getSelectedNode } from '../../utils/getSelectedNode';
@@ -301,6 +302,7 @@ export default function ToolbarPlugin({
301
302
  const [isCode, setIsCode] = useState(false);
302
303
  const [canUndo, setCanUndo] = useState(false);
303
304
  const [canRedo, setCanRedo] = useState(false);
305
+ const tr = useTr();
304
306
 
305
307
  const [codeLanguage, setCodeLanguage] = useState<string>('');
306
308
  const [isEditable, setIsEditable] = useState(() => editor.isEditable());
@@ -453,9 +455,9 @@ export default function ToolbarPlugin({
453
455
  onClick={() => {
454
456
  activeEditor.dispatchCommand(UNDO_COMMAND, undefined);
455
457
  }}
456
- title={IS_APPLE ? 'Undo (⌘Z)' : 'Undo (Ctrl+Z)'}
457
458
  type="button"
458
- aria-label="Undo"
459
+ title={tr(IS_APPLE ? 'actionUndoTitleApple' : 'actionUndoTitle')}
460
+ aria-label={tr('actionUndoLabel')}
459
461
  >
460
462
  <i
461
463
  className={`format icon undo border w-4 h-6 bg-no-repeat bg-center bg-[length:17px_17px] ${
@@ -469,9 +471,9 @@ export default function ToolbarPlugin({
469
471
  onClick={() => {
470
472
  activeEditor.dispatchCommand(REDO_COMMAND, undefined);
471
473
  }}
472
- title={IS_APPLE ? 'Redo (⌘Y)' : 'Redo (Ctrl+Y)'}
473
474
  type="button"
474
- aria-label="Redo"
475
+ title={tr(IS_APPLE ? 'actionRedoTitleApple' : 'actionRedoTitle')}
476
+ aria-label={tr('actionRedoLabel')}
475
477
  >
476
478
  <i
477
479
  className={`format icon redo border w-4 h-6 bg-no-repeat bg-center bg-[length:17px_17px] ${
@@ -515,7 +517,7 @@ export default function ToolbarPlugin({
515
517
  </>
516
518
  }
517
519
  >
518
- <Button aria-label="Select language" append={<Icon.Arrow />}>
520
+ <Button aria-label={tr('codeSelectLanguage')} append={<Icon.Arrow />}>
519
521
  <span className="font-medium text-sm">{getLanguageFriendlyName(codeLanguage)}</span>
520
522
  </Button>
521
523
  </DropdownMenu.Root>
@@ -525,10 +527,10 @@ export default function ToolbarPlugin({
525
527
  <div className="flex gap-1">
526
528
  <IconButton
527
529
  disabled={!isEditable}
528
- title={IS_APPLE ? 'Bold (⌘B)' : 'Bold (Ctrl+B)'}
529
530
  className={`${isBold ? 'opacity-100 !bg-purple-50-900' : 'opacity-60'}`}
530
531
  type="button"
531
- aria-label={`Format text as bold. Shortcut: ${IS_APPLE ? '⌘B' : 'Ctrl+B'}`}
532
+ title={tr(IS_APPLE ? 'actionFormatAsStrongTitleApple' : 'actionFormatAsStrongTitle')}
533
+ aria-label={tr('actionFormatAsStrongLabel')}
532
534
  data-testid="toggle-format-bold"
533
535
  onClick={() => {
534
536
  activeEditor.dispatchCommand(FORMAT_TEXT_COMMAND, 'bold');
@@ -542,9 +544,9 @@ export default function ToolbarPlugin({
542
544
  onClick={() => {
543
545
  activeEditor.dispatchCommand(FORMAT_TEXT_COMMAND, 'italic');
544
546
  }}
545
- title={IS_APPLE ? 'Italic (⌘I)' : 'Italic (Ctrl+I)'}
546
547
  type="button"
547
- aria-label={`Format text as italics. Shortcut: ${IS_APPLE ? '⌘I' : 'Ctrl+I'}`}
548
+ title={tr(IS_APPLE ? 'actionFormatAsEmphasizedTitleApple' : 'actionFormatAsEmphasizedTitle')}
549
+ aria-label={tr('actionFormatAsEmphasizedLabel')}
548
550
  data-testid="toggle-format-emphasized"
549
551
  >
550
552
  <i className={`format icon italic border w-full h-full bg-no-repeat bg-center bg-[length:18px_18px]`} />
@@ -556,9 +558,9 @@ export default function ToolbarPlugin({
556
558
  onClick={() => {
557
559
  activeEditor.dispatchCommand(FORMAT_TEXT_COMMAND, 'underline');
558
560
  }}
559
- title={IS_APPLE ? 'Underline (⌘U)' : 'Underline (Ctrl+U)'}
560
561
  type="button"
561
- aria-label={`Format text to underlined. Shortcut: ${IS_APPLE ? '⌘U' : 'Ctrl+U'}`}
562
+ title={tr(IS_APPLE ? 'actionFormatAsUnderlinedTitleApple' : 'actionFormatAsUnderlinedTitle')}
563
+ aria-label={tr('actionFormatAsUnderlinedLabel')}
562
564
  data-testid="toggle-format-underlined"
563
565
  >
564
566
  <i
@@ -572,9 +574,9 @@ export default function ToolbarPlugin({
572
574
  onClick={() => {
573
575
  activeEditor.dispatchCommand(FORMAT_TEXT_COMMAND, 'code');
574
576
  }}
575
- title="Insert code block"
576
577
  type="button"
577
- aria-label="Insert code block"
578
+ title={tr('actionInsertCodeBlock')}
579
+ aria-label={tr('actionInsertCodeBlock')}
578
580
  >
579
581
  <i className={`format icon code border w-full h-full bg-no-repeat bg-center bg-[length:18px_18px]`} />
580
582
  </IconButton>
@@ -583,9 +585,9 @@ export default function ToolbarPlugin({
583
585
  className={`${isLink ? 'opacity-100 !bg-purple-50-900' : 'opacity-60'}`}
584
586
  disabled={!isEditable}
585
587
  onClick={insertLink}
586
- aria-label="Insert link"
587
- title="Insert link"
588
588
  type="button"
589
+ aria-label={tr('actionInsertlink')}
590
+ title={tr('actionInsertlink')}
589
591
  >
590
592
  <i className={`format icon link border w-full h-full bg-no-repeat bg-center bg-[length:18px_18px]`} />
591
593
  </IconButton>
@@ -599,8 +601,8 @@ export default function ToolbarPlugin({
599
601
  onClick={() => {
600
602
  activeEditor.dispatchCommand(FORMAT_TEXT_COMMAND, 'strikethrough');
601
603
  }}
602
- title="Strikethrough"
603
- aria-label="Format text with a strikethrough"
604
+ title={tr('actionFormatWithStrikethroughTitle')}
605
+ aria-label={tr('actionFormatWithStrikethroughLabel')}
604
606
  >
605
607
  <i
606
608
  className={`icon w-6 h-6 strikethrough border bg-no-repeat bg-center bg-[length:16px_16px] rounded-sm ${
@@ -608,15 +610,15 @@ export default function ToolbarPlugin({
608
610
  }`}
609
611
  />
610
612
  <span className={`px-3 text-sm font-sans ${isStrikethrough ? 'font-medium' : 'font-normal'}`}>
611
- Strikethrough
613
+ {tr('actionFormatAsStrongTitle')}
612
614
  </span>
613
615
  </DropdownMenu.Item>
614
616
  <DropdownMenu.Item
615
617
  onClick={() => {
616
618
  activeEditor.dispatchCommand(FORMAT_TEXT_COMMAND, 'subscript');
617
619
  }}
618
- title="Subscript"
619
- aria-label="Format text with a subscript"
620
+ title={tr('actionFormatWithSubscriptTitle')}
621
+ aria-label={tr('actionFormatWithSubscriptLabel')}
620
622
  >
621
623
  <i
622
624
  className={`icon w-6 h-6 subscript border bg-no-repeat bg-center bg-[length:16px_16px] rounded-sm ${
@@ -624,15 +626,15 @@ export default function ToolbarPlugin({
624
626
  }`}
625
627
  />
626
628
  <span className={`px-3 text-sm font-sans ${isSubscript ? 'font-medium' : 'font-normal'}`}>
627
- Subscript
629
+ {tr('actionFormatWithSubscriptTitle')}
628
630
  </span>
629
631
  </DropdownMenu.Item>
630
632
  <DropdownMenu.Item
631
633
  onClick={() => {
632
634
  activeEditor.dispatchCommand(FORMAT_TEXT_COMMAND, 'superscript');
633
635
  }}
634
- title="Superscript"
635
- aria-label="Format text with a superscript"
636
+ title={tr('actionFormatWithSuperscriptTitle')}
637
+ aria-label={tr('actionFormatWithSuperscriptLabel')}
636
638
  >
637
639
  <i
638
640
  className={`icon w-6 h-6 superscript border bg-no-repeat bg-center bg-[length:16px_16px] rounded-sm ${
@@ -640,14 +642,14 @@ export default function ToolbarPlugin({
640
642
  }`}
641
643
  />
642
644
  <span className={`px-3 text-sm font-sans ${isSuperscript ? 'bg-purple-50-900' : 'font-normal'}`}>
643
- Superscript
645
+ {tr('actionFormatWithSuperscriptTitle')}
644
646
  </span>
645
647
  </DropdownMenu.Item>
646
648
  <DropdownMenu.Item
647
649
  onClick={clearFormatting}
648
650
  className="item"
649
- title="Clear text formatting"
650
- aria-label="Clear all text formatting"
651
+ title={tr('actionClearTextFormatting')}
652
+ aria-label={tr('actionClearTextFormatting')}
651
653
  >
652
654
  <i className="icon w-6 h-6 clear border bg-no-repeat bg-center bg-[length:16px_16px] opacity-60" />
653
655
  <span className="px-3 text-sm text-pink-600-300 font-sans font-normal">Clear Formatting</span>
@@ -657,7 +659,7 @@ export default function ToolbarPlugin({
657
659
  >
658
660
  <Button
659
661
  style={{ backgroundColor: 'transparent', padding: '0 8px' }}
660
- aria-label="Formatting options for additional text styles"
662
+ aria-label={tr('actionTextFormattingOptions')}
661
663
  >
662
664
  <i className={`icon dropdown-more border bg-no-repeat bg-center bg-[length:18px_18px] w-6 h-6`} />
663
665
  <Icon.Arrow />
@@ -676,14 +678,14 @@ export default function ToolbarPlugin({
676
678
  >
677
679
  <div className="flex items-center font-sans font-normal">
678
680
  <i className="icon w-5 h-5 horizontal-rule border bg-no-repeat bg-center bg-[length:16px_16px] opacity-60" />
679
- <span className="px-3 text-sm">Horizontal rule</span>
681
+ <span className="px-3 text-sm">{tr('horizontalRule')}</span>
680
682
  </div>
681
683
  </DropdownMenu.Item>
682
684
  <DropdownMenu.Item>
683
685
  <Dialog.Trigger asChild>
684
686
  <div className="flex items-center font-sans font-normal">
685
687
  <i className="icon w-5 h-5 table border bg-no-repeat bg-center bg-[length:16px_16px] opacity-60" />
686
- <span className="px-3 text-sm">Table</span>
688
+ <span className="px-3 text-sm">{tr('table')}</span>
687
689
  </div>
688
690
  </Dialog.Trigger>
689
691
  </DropdownMenu.Item>
@@ -695,10 +697,8 @@ export default function ToolbarPlugin({
695
697
  </IconButton>
696
698
  </DropdownMenu.Root>
697
699
  <Dialog.Content>
698
- <Dialog.Title>Insert table</Dialog.Title>
699
- <Dialog.Description>
700
- Define your starting point of a table, you can add and remove columns and rows after creation.
701
- </Dialog.Description>
700
+ <Dialog.Title>{tr('insertTableTitle')}</Dialog.Title>
701
+ <Dialog.Description>{tr('insertTableDescription')}</Dialog.Description>
702
702
  <div className="items-center justify-between">
703
703
  <InsertTableDialog activeEditor={activeEditor} />
704
704
  </div>
@@ -5,10 +5,12 @@ import { INSERT_TABLE_COMMAND } from '@lexical/table';
5
5
  import { Button } from '../../../button';
6
6
  import { Dialog } from '../../../dialog';
7
7
  import { InputWithLabel } from '../../../input-with-label';
8
+ import { useTr } from '../../i18n';
8
9
 
9
10
  export function InsertTableDialog({ activeEditor }: { activeEditor: LexicalEditor }) {
10
11
  const [rows, setRows] = useState('5');
11
12
  const [columns, setColumns] = useState('5');
13
+ const tr = useTr();
12
14
 
13
15
  const onClick = () => {
14
16
  if (parseInt(rows) < 1 || parseInt(columns) < 1) {
@@ -28,7 +30,7 @@ export function InsertTableDialog({ activeEditor }: { activeEditor: LexicalEdito
28
30
  <>
29
31
  <div className="grid grid-cols-[1fr_1px_1fr] border border-gray-100-800 border-solid shadow-sm rounded-md ">
30
32
  <InputWithLabel
31
- label="Rows"
33
+ label={tr('insertTableRows')}
32
34
  value={rows}
33
35
  placeholder="0"
34
36
  type="text"
@@ -38,7 +40,7 @@ export function InsertTableDialog({ activeEditor }: { activeEditor: LexicalEdito
38
40
  <span className="h-full bg-gray-100-800" />
39
41
  <InputWithLabel
40
42
  type="text"
41
- label="Columns"
43
+ label={tr('insertTableColumns')}
42
44
  placeholder="0"
43
45
  value={columns}
44
46
  inputMode="numeric"
@@ -46,8 +48,8 @@ export function InsertTableDialog({ activeEditor }: { activeEditor: LexicalEdito
46
48
  />
47
49
  </div>
48
50
  <div className="flex justify-end mt-3">
49
- <Button as={Dialog.Close} size="sm" intent="action" aria-label="Confirm" onClick={onClick}>
50
- Confirm
51
+ <Button as={Dialog.Close} size="sm" intent="action" aria-label={tr('insertTableCommit')} onClick={onClick}>
52
+ {tr('insertTableCommit')}
51
53
  </Button>
52
54
  </div>
53
55
  </>
@@ -997,6 +997,12 @@
997
997
  user-select: none;
998
998
  }
999
999
 
1000
+ .ContentEditable__root {
1001
+ tab-size: 1;
1002
+ min-height: calc(100% - 16px);
1003
+ @apply relative block border-0 px-6 py-2 !pt-0 text-sm outline-0;
1004
+ }
1005
+
1000
1006
  .TableNode__contentEditable {
1001
1007
  min-height: 20px;
1002
1008
  border: 0px;
@@ -1,6 +1,7 @@
1
1
  /* eslint-disable no-console */
2
2
  import type { Meta, StoryObj } from '@storybook/react';
3
3
 
4
+ import { labelTranslationEn } from './i18n';
4
5
  import { RichTextEditor } from './rich-text-editor';
5
6
  import type { CrystallizeRichText, CrystallizeRichTextNode } from './types/crystallize-rich-text-types';
6
7
 
@@ -421,3 +422,40 @@ export const Quote: Story = {
421
422
  },
422
423
  },
423
424
  };
425
+
426
+ export const MultipleInlineFormats: Story = {
427
+ args: {
428
+ initialData: [
429
+ {
430
+ type: 'paragraph',
431
+ kind: 'block',
432
+ children: [
433
+ {
434
+ kind: 'inline',
435
+ type: 'strong',
436
+ children: [
437
+ {
438
+ kind: 'inline',
439
+ textContent: 'placed',
440
+ type: 'emphasized',
441
+ },
442
+ ],
443
+ },
444
+ ],
445
+ },
446
+ ],
447
+ onChange(data) {
448
+ console.log('onChange');
449
+ console.log(JSON.stringify(data, null, 1));
450
+ },
451
+ },
452
+ };
453
+
454
+ export const CustomTranslation: Story = {
455
+ args: {
456
+ labelTranslations: {
457
+ ...labelTranslationEn,
458
+ actionClear: '💥 Kaboom',
459
+ },
460
+ },
461
+ };
@@ -14,8 +14,11 @@ import { TablePlugin } from '@lexical/react/LexicalTablePlugin';
14
14
 
15
15
  import './rich-text-editor.css';
16
16
  import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
17
+ import { ContentEditable } from '@lexical/react/LexicalContentEditable';
17
18
 
18
19
  import { SharedHistoryContext, useSharedHistoryContext } from './context/SharedHistoryContext';
20
+ import { I18nProvider, SupportedLanguages } from './i18n';
21
+ import type { I18N } from './i18n/types';
19
22
  import { composeInitialState } from './model/crystallize-to-lexical';
20
23
  import { lexicalToCrystallizeRichText } from './model/lexical-to-crystallize';
21
24
  import { BaseNodes } from './nodes/BaseNodes';
@@ -33,7 +36,6 @@ import ToolbarPlugin from './plugins/ToolbarPlugin';
33
36
  import CrystallizeRTEditorTheme from './themes/CrystallizeRTEditorTheme';
34
37
  import type { CrystallizeRichText } from './types/crystallize-rich-text-types';
35
38
  import type { CrystallizeRichTextActionMenuItem } from './types/types';
36
- import ContentEditable from './ui/ContentEditable';
37
39
 
38
40
  /**
39
41
  * Currently disabling this as there is a conflict with someting within
@@ -56,11 +58,15 @@ type TRichTextBase = {
56
58
  slotPreContent?: ReactNode;
57
59
  maxLength?: number;
58
60
  editable?: boolean;
61
+ language?: SupportedLanguages;
62
+ labelTranslations?: I18N;
59
63
  };
60
64
 
61
65
  export function RichTextEditor({
62
66
  initialData,
63
67
  editable = true,
68
+ language = 'en',
69
+ labelTranslations,
64
70
  ...rest
65
71
  }: TRichTextBase & {
66
72
  initialData?: CrystallizeRichText;
@@ -82,13 +88,13 @@ export function RichTextEditor({
82
88
  : undefined,
83
89
  }}
84
90
  >
85
- <SharedHistoryContext>
86
- {/* <TableContext> */}
87
- <div className="c-rich-text-editor">
88
- <RichTextEditorWithoutContext {...rest} editable={editable} />
89
- </div>
90
- {/* </TableContext> */}
91
- </SharedHistoryContext>
91
+ <I18nProvider language={language} labelTranslations={labelTranslations}>
92
+ <SharedHistoryContext>
93
+ <div className="c-rich-text-editor">
94
+ <RichTextEditorWithoutContext {...rest} editable={editable} />
95
+ </div>
96
+ </SharedHistoryContext>
97
+ </I18nProvider>
92
98
  </LexicalComposer>
93
99
  );
94
100
  }
@@ -171,7 +177,7 @@ function RichTextEditorWithoutContext({
171
177
  contentEditable={
172
178
  <div className="editor-scroller">
173
179
  <div className="editor" ref={onRef}>
174
- <ContentEditable />
180
+ <ContentEditable className="ContentEditable__root" />
175
181
  </div>
176
182
  </div>
177
183
  }
@@ -189,11 +189,33 @@ const dataTypes: { name: string; model: CrystallizeRichText }[] = [
189
189
  },
190
190
  ],
191
191
  },
192
+ {
193
+ name: 'Multiple inline formats',
194
+ model: [
195
+ {
196
+ type: 'paragraph',
197
+ kind: 'block',
198
+ children: [
199
+ {
200
+ kind: 'inline',
201
+ type: 'strong',
202
+ children: [
203
+ {
204
+ kind: 'inline',
205
+ textContent: 'placed',
206
+ type: 'emphasized',
207
+ },
208
+ ],
209
+ },
210
+ ],
211
+ },
212
+ ],
213
+ },
192
214
  ];
193
215
 
194
216
  describe('RichTextEditor model conversions', () => {
195
217
  dataTypes.forEach(dataType => {
196
- it('correctly converts an ' + dataType.name, async () => {
218
+ it('correctly converts ' + dataType.name, async () => {
197
219
  const onChange = vi.fn();
198
220
 
199
221
  render(<RichTextEditor initialData={dataType.model} onChange={onChange} triggerOnChangeOnAutoFocus autoFocus />);
@@ -34,10 +34,8 @@ type CrystallizeRichTextGenericNode = {
34
34
  | 'aside'
35
35
  | 'container'
36
36
  | 'details'
37
- | 'emphasized'
38
37
  | 'figcaption'
39
38
  | 'figure'
40
- | 'highlight'
41
39
  | 'image'
42
40
  | 'line-break'
43
41
  | 'unordered-list'
@@ -51,9 +49,9 @@ type CrystallizeRichTextGenericNode = {
51
49
  | 'title-of-a-work';
52
50
  };
53
51
 
54
- type CrystallizeRichTextInlineFormattedNodes = {
52
+ export type CrystallizeRichTextInlineFormattedNodes = {
55
53
  kind: 'inline';
56
- type: 'deleted' | 'strong' | 'subscripted' | 'superscripted' | 'underlined';
54
+ type: 'deleted' | 'strong' | 'subscripted' | 'superscripted' | 'underlined' | 'emphasized' | 'code' | 'highlight';
57
55
  };
58
56
 
59
57
  export type CrystallizeRichTextNode = CrystallizeRichTextNodeBase &