@crystallize/design-system 1.10.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 (26) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/index.css +15 -17
  3. package/dist/index.d.ts +8 -1
  4. package/dist/index.js +469 -442
  5. package/dist/index.mjs +407 -385
  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/plugins/ActionsPlugin/index.tsx +5 -22
  12. package/src/rich-text-editor/plugins/CodeActionMenuPlugin/components/CopyButton/index.tsx +4 -1
  13. package/src/rich-text-editor/plugins/CodeActionMenuPlugin/components/PrettierButton/index.tsx +11 -1
  14. package/src/rich-text-editor/plugins/CodeActionMenuPlugin/index.tsx +2 -1
  15. package/src/rich-text-editor/plugins/FloatingLinkEditorPlugin/index.tsx +23 -5
  16. package/src/rich-text-editor/plugins/FloatingTextFormatToolbarPlugin/index.tsx +21 -10
  17. package/src/rich-text-editor/plugins/TabFocusPlugin/index.tsx +4 -12
  18. package/src/rich-text-editor/plugins/TableActionMenuPlugin/index.tsx +23 -14
  19. package/src/rich-text-editor/plugins/ToolbarPlugin/index.tsx +33 -33
  20. package/src/rich-text-editor/plugins/ToolbarPlugin/insert-table.tsx +6 -4
  21. package/src/rich-text-editor/rich-text-editor.css +6 -0
  22. package/src/rich-text-editor/rich-text-editor.stories.tsx +10 -0
  23. package/src/rich-text-editor/rich-text-editor.tsx +15 -9
  24. package/src/rich-text-editor/ui/LinkPreview.tsx +3 -1
  25. package/src/rich-text-editor/ui/ContentEditable.css +0 -13
  26. package/src/rich-text-editor/ui/ContentEditable.tsx +0 -15
@@ -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
 
@@ -449,3 +450,12 @@ export const MultipleInlineFormats: Story = {
449
450
  },
450
451
  },
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
  }
@@ -12,6 +12,7 @@ import { $getSelection, $isTextNode } from 'lexical';
12
12
  import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
13
13
 
14
14
  import { Button } from '../../button';
15
+ import { useTr } from '../i18n';
15
16
 
16
17
  type MetaTagBaseShared = {
17
18
  description?: string;
@@ -77,6 +78,7 @@ function LinkPreviewContent({
77
78
  const [textContent, setTextContent] = useState('');
78
79
  const { preview } = useSuspenseRequest(url);
79
80
  const [editor] = useLexicalComposerContext();
81
+ const tr = useTr();
80
82
 
81
83
  const hasPreview = preview !== null && preview.google?.title;
82
84
 
@@ -128,7 +130,7 @@ function LinkPreviewContent({
128
130
  {preview.google.description && <div className="LinkPreview__description">{preview.google.description}</div>}
129
131
  {textContent && textContent !== preview.google.title ? (
130
132
  <Button className="mb-4 ml-5" onClick={useTitleForText}>
131
- Replace link text with its title
133
+ {tr('linkPreviewReplaceTextWithTitle')}
132
134
  </Button>
133
135
  ) : null}
134
136
  </div>
@@ -1,13 +0,0 @@
1
- /**
2
- * Copyright (c) Meta Platforms, Inc. and affiliates.
3
- *
4
- * This source code is licensed under the MIT license found in the
5
- * LICENSE file in the root directory of this source tree.
6
- *
7
- *
8
- */
9
- .ContentEditable__root {
10
- tab-size: 1;
11
- min-height: calc(100% - 16px);
12
- @apply relative block border-0 px-6 py-2 !pt-0 text-sm outline-0;
13
- }
@@ -1,15 +0,0 @@
1
- /**
2
- * Copyright (c) Meta Platforms, Inc. and affiliates.
3
- *
4
- * This source code is licensed under the MIT license found in the
5
- * LICENSE file in the root directory of this source tree.
6
- *
7
- */
8
-
9
- import './ContentEditable.css';
10
- import * as React from 'react';
11
- import { ContentEditable } from '@lexical/react/LexicalContentEditable';
12
-
13
- export default function LexicalContentEditable({ className }: { className?: string }): JSX.Element {
14
- return <ContentEditable className={className || 'ContentEditable__root'} />;
15
- }