@thangph2146/lexical-editor 0.0.4 → 0.0.5

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 (99) hide show
  1. package/dist/editor-x/editor.cjs +721 -432
  2. package/dist/editor-x/editor.cjs.map +1 -1
  3. package/dist/editor-x/editor.css +1418 -1120
  4. package/dist/editor-x/editor.css.map +1 -1
  5. package/dist/editor-x/editor.js +725 -436
  6. package/dist/editor-x/editor.js.map +1 -1
  7. package/dist/index.cjs +757 -469
  8. package/dist/index.cjs.map +1 -1
  9. package/dist/index.css +1418 -1120
  10. package/dist/index.css.map +1 -1
  11. package/dist/index.js +760 -472
  12. package/dist/index.js.map +1 -1
  13. package/package.json +3 -3
  14. package/src/components/lexical-editor.tsx +138 -123
  15. package/src/editor-x/editor.tsx +16 -3
  16. package/src/editor-x/plugins.tsx +385 -380
  17. package/src/nodes/list-with-color-node.tsx +160 -160
  18. package/src/plugins/autocomplete-plugin.tsx +2574 -2574
  19. package/src/plugins/context-menu-plugin.tsx +239 -9
  20. package/src/plugins/floating-text-format-plugin.tsx +84 -92
  21. package/src/plugins/images-plugin.tsx +4 -4
  22. package/src/plugins/list-color-plugin.tsx +178 -178
  23. package/src/plugins/tab-focus-plugin.tsx +66 -66
  24. package/src/plugins/table-column-resizer-plugin.tsx +329 -190
  25. package/src/plugins/toolbar/block-format/block-format-data.tsx +4 -0
  26. package/src/plugins/toolbar/block-format/format-bulleted-list.tsx +40 -40
  27. package/src/plugins/toolbar/block-format/format-list-with-marker.tsx +74 -74
  28. package/src/plugins/toolbar/block-format/format-numbered-list.tsx +40 -40
  29. package/src/plugins/toolbar/block-format-toolbar-plugin.tsx +118 -117
  30. package/src/plugins/toolbar/element-format-toolbar-plugin.tsx +37 -53
  31. package/src/plugins/toolbar/font-format-toolbar-plugin.tsx +8 -15
  32. package/src/plugins/toolbar/font-size-toolbar-plugin.tsx +2 -3
  33. package/src/plugins/toolbar/history-toolbar-plugin.tsx +2 -5
  34. package/src/plugins/toolbar/subsuper-toolbar-plugin.tsx +15 -23
  35. package/src/themes/_mixins.scss +158 -10
  36. package/src/themes/_variables.scss +168 -0
  37. package/src/themes/core/_code.scss +59 -0
  38. package/src/themes/core/_images.scss +80 -0
  39. package/src/themes/core/_lists.scss +214 -0
  40. package/src/themes/core/_misc.scss +46 -0
  41. package/src/themes/core/_reset.scss +119 -0
  42. package/src/themes/core/_tables.scss +145 -0
  43. package/src/themes/core/_text.scss +35 -0
  44. package/src/themes/core/_typography.scss +73 -0
  45. package/src/themes/editor-theme copy.scss +763 -0
  46. package/src/themes/editor-theme.scss +9 -623
  47. package/src/themes/editor-theme.ts +118 -118
  48. package/src/themes/plugins/_auto-embed.scss +11 -0
  49. package/src/themes/plugins/_color-picker.scss +103 -0
  50. package/src/themes/plugins/_draggable-block.scss +32 -0
  51. package/src/themes/plugins/_floating-link-editor.scss +47 -0
  52. package/src/themes/plugins/_floating-toolbars.scss +61 -0
  53. package/src/themes/plugins/_image-resizer.scss +38 -0
  54. package/src/themes/plugins/_image.scss +57 -0
  55. package/src/themes/plugins/_layout.scss +39 -0
  56. package/src/themes/plugins/_list-color.scss +23 -0
  57. package/src/themes/plugins/_mentions.scss +21 -0
  58. package/src/themes/plugins/_menus-and-pickers.scss +153 -0
  59. package/src/themes/plugins/_table.scss +20 -0
  60. package/src/themes/plugins/_toolbar.scss +36 -0
  61. package/src/themes/plugins/_tree-view.scss +11 -0
  62. package/src/themes/plugins copy.scss +656 -0
  63. package/src/themes/plugins.scss +20 -1165
  64. package/src/themes/ui-components/_animations.scss +31 -0
  65. package/src/themes/ui-components/_backgrounds.scss +27 -0
  66. package/src/themes/ui-components/_borders.scss +20 -0
  67. package/src/themes/ui-components/_button.scss +176 -0
  68. package/src/themes/ui-components/_checkbox.scss +14 -0
  69. package/src/themes/ui-components/_cursors.scss +31 -0
  70. package/src/themes/ui-components/_dialog.scss +86 -0
  71. package/src/themes/ui-components/_display-sizing.scss +100 -0
  72. package/src/themes/ui-components/_flex.scss +124 -0
  73. package/src/themes/ui-components/_form-layout.scss +15 -0
  74. package/src/themes/ui-components/_icons.scss +23 -0
  75. package/src/themes/ui-components/_input.scss +86 -0
  76. package/src/themes/ui-components/_label.scss +19 -0
  77. package/src/themes/ui-components/_loader.scss +9 -0
  78. package/src/themes/ui-components/_margins-paddings.scss +45 -0
  79. package/src/themes/ui-components/_popover.scss +16 -0
  80. package/src/themes/ui-components/_positioning.scss +73 -0
  81. package/src/themes/ui-components/_rounded.scss +19 -0
  82. package/src/themes/ui-components/_scroll-area.scss +11 -0
  83. package/src/themes/ui-components/_select.scss +110 -0
  84. package/src/themes/ui-components/_separator.scss +19 -0
  85. package/src/themes/ui-components/_shadow.scss +15 -0
  86. package/src/themes/ui-components/_tabs.scss +46 -0
  87. package/src/themes/ui-components/_text-utilities.scss +48 -0
  88. package/src/themes/ui-components/_toggle-toolbar.scss +128 -0
  89. package/src/themes/ui-components/_toggle.scss +80 -0
  90. package/src/themes/ui-components/_typography.scss +22 -0
  91. package/src/themes/ui-components copy.scss +1335 -0
  92. package/src/themes/ui-components.scss +27 -937
  93. package/src/transformers/markdown-list-transformer.ts +51 -51
  94. package/src/ui/button.tsx +11 -2
  95. package/src/ui/collapsible.tsx +1 -1
  96. package/src/ui/dialog.tsx +2 -2
  97. package/src/ui/flex.tsx +4 -4
  98. package/src/ui/popover.tsx +1 -1
  99. package/src/ui/tooltip.tsx +2 -2
@@ -1,5 +1,5 @@
1
1
  import type { JSX } from "react"
2
- import { useMemo } from "react"
2
+ import { useCallback, useEffect, useMemo, useState } from "react"
3
3
  import { $isLinkNode, TOGGLE_LINK_COMMAND } from "@lexical/link"
4
4
  import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"
5
5
  import {
@@ -9,10 +9,11 @@ import {
9
9
  } from "@lexical/react/LexicalNodeContextMenuPlugin"
10
10
  import { $mergeCells, $isTableCellNode, $isTableSelection, $unmergeCell, $insertTableColumnAtSelection, $insertTableRowAtSelection } from "@lexical/table"
11
11
  import type { TableCellNode } from "@lexical/table"
12
- import { $isListNode } from "@lexical/list"
12
+ import { $isListNode, ListNode } from "@lexical/list"
13
13
  import { $findMatchingParent } from "@lexical/utils"
14
14
  import {
15
15
  $getSelection,
16
+ $getNearestNodeFromDOMNode,
16
17
  $isDecoratorNode,
17
18
  $isNodeSelection,
18
19
  $isRangeSelection,
@@ -22,6 +23,8 @@ import {
22
23
  type LexicalNode,
23
24
  } from "lexical"
24
25
  import { $isLayoutItemNode } from "../nodes/layout-item-node"
26
+ import { createListWithColorNodeFromRegistry } from "../editor-x/nodes"
27
+ import { $isListWithColorNode, $createListWithColorNode } from "../nodes/list-with-color-node"
25
28
  import { OPEN_UPDATE_LAYOUT_MODAL_COMMAND } from "../plugins/layout-plugin"
26
29
  import { OPEN_LIST_COLOR_PICKER_COMMAND } from "../plugins/list-color-plugin"
27
30
  import {
@@ -37,11 +40,21 @@ import {
37
40
  Rows3,
38
41
  LayoutGrid,
39
42
  Palette,
43
+ ListOrderedIcon,
44
+ Hash,
45
+ ListIcon,
40
46
  } from "lucide-react"
41
47
  import { IconSize } from "../ui/typography"
48
+ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "../ui/dialog"
49
+ import { Input } from "../ui/input"
50
+ import { Button } from "../ui/button"
51
+ import { Label } from "../ui/label"
42
52
 
43
53
  export function ContextMenuPlugin(): JSX.Element {
44
54
  const [editor] = useLexicalComposerContext()
55
+ const [contextTarget, setContextTarget] = useState<HTMLElement | null>(null)
56
+ const [showMarkerDialog, setShowMarkerDialog] = useState(false)
57
+ const [customMarker, setCustomMarker] = useState("")
45
58
 
46
59
  const isInTable = (node: LexicalNode) =>
47
60
  $findMatchingParent(node, $isTableCellNode) !== null
@@ -67,6 +80,123 @@ export function ContextMenuPlugin(): JSX.Element {
67
80
  const isInList = (node: LexicalNode) =>
68
81
  $findMatchingParent(node, $isListNode) !== null
69
82
 
83
+ const isInNumberList = (node: LexicalNode) => {
84
+ const listNode = $findMatchingParent(node, $isListNode)
85
+ return $isListNode(listNode) && listNode.getListType() === "number"
86
+ }
87
+
88
+ useEffect(() => {
89
+ const rootElement = editor.getRootElement()
90
+ if (!rootElement) return
91
+
92
+ const onContextMenu = (event: MouseEvent) => {
93
+ const target = event.target
94
+ if (!(target instanceof HTMLElement)) return
95
+ setContextTarget(target)
96
+ }
97
+
98
+ rootElement.addEventListener("contextmenu", onContextMenu)
99
+ return () => {
100
+ rootElement.removeEventListener("contextmenu", onContextMenu)
101
+ }
102
+ }, [editor])
103
+
104
+ const getContextAnchorNode = useCallback(() => {
105
+ if (contextTarget) {
106
+ const nearestNode = $getNearestNodeFromDOMNode(contextTarget)
107
+ if (nearestNode) return nearestNode
108
+ }
109
+ const selection = $getSelection()
110
+ if ($isRangeSelection(selection)) {
111
+ return selection.anchor.getNode()
112
+ }
113
+ return null
114
+ }, [contextTarget])
115
+
116
+ const updateNumberListMarkerType = useCallback((markerType: string | undefined) => {
117
+ editor.update(() => {
118
+ const anchorNode = getContextAnchorNode()
119
+ if (!anchorNode) return
120
+ const nearestListNode = $findMatchingParent(
121
+ anchorNode,
122
+ (node): node is ListNode => $isListNode(node) && node.getListType() === "number"
123
+ )
124
+ if (!nearestListNode) return
125
+ let listNode: ListNode = nearestListNode
126
+ let parent = listNode.getParent()
127
+ while (parent) {
128
+ if ($isListNode(parent) && parent.getListType() === "number") {
129
+ listNode = parent
130
+ parent = parent.getParent()
131
+ continue
132
+ }
133
+ break
134
+ }
135
+ if ($isListWithColorNode(listNode)) {
136
+ listNode.setMarkerType(markerType)
137
+ return
138
+ }
139
+ const newList = createListWithColorNodeFromRegistry(
140
+ editor,
141
+ listNode.getListType(),
142
+ listNode.getStart()
143
+ )
144
+ newList.setMarkerType(markerType)
145
+ const children = listNode.getChildren()
146
+ for (const child of children) newList.append(child)
147
+ listNode.replace(newList)
148
+ })
149
+ }, [editor, getContextAnchorNode])
150
+
151
+ const syncMarkerToSameLevelLists = useCallback((targetListNode: ListNode, markerType: string | undefined) => {
152
+ editor.update(() => {
153
+ // Lấy parent của list hiện tại
154
+ const parent = targetListNode.getParent()
155
+ if (!parent) return
156
+
157
+ // Tìm tất cả các list cùng cấp với list hiện tại
158
+ const siblings = parent.getChildren()
159
+
160
+ for (const sibling of siblings) {
161
+ if ($isListNode(sibling) && sibling.getListType() === "number" && sibling !== targetListNode) {
162
+ // Cập nhật marker cho list cùng cấp
163
+ if ($isListWithColorNode(sibling)) {
164
+ sibling.setMarkerType(markerType)
165
+ } else {
166
+ const newList = $createListWithColorNode("number", sibling.getStart())
167
+ newList.setMarkerType(markerType)
168
+ const children = sibling.getChildren()
169
+ for (const child of children) newList.append(child)
170
+ sibling.replace(newList)
171
+ }
172
+ }
173
+ }
174
+ })
175
+ }, [editor])
176
+
177
+ const handleCustomMarkerSubmit = () => {
178
+ if (customMarker.trim()) {
179
+ editor.update(() => {
180
+ const anchorNode = getContextAnchorNode()
181
+ if (!anchorNode) return
182
+ const nearestListNode = $findMatchingParent(
183
+ anchorNode,
184
+ (node): node is ListNode => $isListNode(node) && node.getListType() === "number"
185
+ )
186
+ if (!nearestListNode) return
187
+
188
+ // Cập nhật marker cho list hiện tại
189
+ updateNumberListMarkerType(customMarker.trim())
190
+
191
+ // Đồng bộ với các list cùng cấp
192
+ syncMarkerToSameLevelLists(nearestListNode, customMarker.trim())
193
+ })
194
+
195
+ setShowMarkerDialog(false)
196
+ setCustomMarker("")
197
+ }
198
+ }
199
+
70
200
  const items = useMemo(() => {
71
201
  return [
72
202
  new NodeContextMenuOption(`Gộp ô`, {
@@ -192,6 +322,41 @@ export function ContextMenuPlugin(): JSX.Element {
192
322
  new NodeContextMenuSeparator({
193
323
  $showOn: isInList,
194
324
  }),
325
+ new NodeContextMenuOption(`Đánh số 1, 2, 3`, {
326
+ $onSelect: () => {
327
+ updateNumberListMarkerType(undefined)
328
+ },
329
+ $showOn: isInNumberList,
330
+ disabled: false,
331
+ icon: <IconSize size="sm"><ListOrderedIcon /></IconSize>,
332
+ }),
333
+ new NodeContextMenuOption(`Đánh số đa cấp 1.1 / 1.1.1`, {
334
+ $onSelect: () => {
335
+ updateNumberListMarkerType("multi-level")
336
+ },
337
+ $showOn: isInNumberList,
338
+ disabled: false,
339
+ icon: <IconSize size="sm"><ListOrderedIcon /></IconSize>,
340
+ }),
341
+ new NodeContextMenuOption(`Đánh số a, b, c`, {
342
+ $onSelect: () => {
343
+ updateNumberListMarkerType("alpha")
344
+ },
345
+ $showOn: isInNumberList,
346
+ disabled: false,
347
+ icon: <IconSize size="sm"><ListOrderedIcon /></IconSize>,
348
+ }),
349
+ new NodeContextMenuOption(`Tùy chỉnh marker...`, {
350
+ $onSelect: () => {
351
+ setShowMarkerDialog(true)
352
+ },
353
+ $showOn: isInNumberList,
354
+ disabled: false,
355
+ icon: <IconSize size="sm"><Hash className="text-blue-600" /></IconSize>,
356
+ }),
357
+ new NodeContextMenuSeparator({
358
+ $showOn: isInNumberList,
359
+ }),
195
360
  new NodeContextMenuOption(`Remove Link`, {
196
361
  $onSelect: () => {
197
362
  editor.dispatchCommand(TOGGLE_LINK_COMMAND, null)
@@ -298,14 +463,79 @@ export function ContextMenuPlugin(): JSX.Element {
298
463
  icon: <IconSize size="sm"><Trash2 /></IconSize>,
299
464
  }),
300
465
  ]
301
- }, [editor])
466
+ }, [editor, updateNumberListMarkerType])
302
467
 
303
468
  return (
304
- <NodeContextMenuPlugin
305
- className="editor-context-menu"
306
- itemClassName="editor-context-menu-item"
307
- separatorClassName="editor-context-menu-separator"
308
- items={items}
309
- />
469
+ <>
470
+ <NodeContextMenuPlugin
471
+ className="editor-context-menu"
472
+ itemClassName="editor-context-menu-item"
473
+ separatorClassName="editor-context-menu-separator"
474
+ items={items}
475
+ />
476
+ <Dialog open={showMarkerDialog} onOpenChange={setShowMarkerDialog}>
477
+ <DialogContent style={{ maxWidth: '400px' }}>
478
+ <DialogHeader>
479
+ <DialogTitle>Tùy chỉnh Marker</DialogTitle>
480
+ <p className="editor-text-sm editor-text-muted-foreground">Nhập kiểu marker cho danh sách của bạn</p>
481
+ </DialogHeader>
482
+
483
+ <div className="editor-flex-col editor-py-4 editor-gap-4">
484
+ <div className="editor-flex-col editor-gap-2">
485
+ <Label htmlFor="marker-input" className="editor-font-medium">
486
+ Kiểu marker
487
+ </Label>
488
+ <div className="editor-relative editor-flex editor-items-center">
489
+ <Hash
490
+ className="editor-absolute editor-text-muted-foreground"
491
+ style={{ left: '0.75rem' }}
492
+ size={16}
493
+ />
494
+ <Input
495
+ id="marker-input"
496
+ value={customMarker}
497
+ onChange={(e) => setCustomMarker(e.target.value)}
498
+ placeholder="Ví dụ: multi-level, alpha, 4.1, ..."
499
+ style={{ paddingLeft: '2.25rem' }}
500
+ onKeyDown={(e) => {
501
+ if (e.key === "Enter") {
502
+ handleCustomMarkerSubmit()
503
+ }
504
+ }}
505
+ />
506
+ </div>
507
+ <div className="editor-flex-col editor-gap-1 editor-text-sm editor-text-muted-foreground editor-mt-2">
508
+ <p className="editor-flex editor-items-center editor-gap-2" style={{ margin: 0 }}>
509
+ <span className="editor-font-medium">multi-level</span> - Hiển thị dạng 4.1, 4.1.1
510
+ </p>
511
+ <p className="editor-flex editor-items-center editor-gap-2" style={{ margin: 0 }}>
512
+ <span className="editor-font-medium">alpha</span> - Hiển thị dạng a, b, c
513
+ </p>
514
+ <p className="editor-flex editor-items-center editor-gap-2" style={{ margin: 0 }}>
515
+ <span className="editor-font-medium">Để trống</span> - Về mặc định 1, 2, 3
516
+ </p>
517
+ </div>
518
+ </div>
519
+ </div>
520
+
521
+ <DialogFooter>
522
+ <Button
523
+ variant="outline"
524
+ onClick={() => setShowMarkerDialog(false)}
525
+ >
526
+ Hủy
527
+ </Button>
528
+ <Button
529
+ onClick={handleCustomMarkerSubmit}
530
+ disabled={!customMarker.trim()}
531
+ className="editor-gap-2"
532
+ >
533
+ <ListIcon size={16} />
534
+ <span>Áp dụng</span>
535
+ </Button>
536
+ </DialogFooter>
537
+ </DialogContent>
538
+ </Dialog>
539
+ </>
310
540
  )
311
541
  }
@@ -59,10 +59,9 @@ import { DialogFooter } from "../ui/dialog"
59
59
  import { Flex } from "../ui/flex"
60
60
  import { Separator } from "../ui/separator"
61
61
  import {
62
- ToggleGroup,
63
- ToggleGroupItem,
64
- } from "../ui/toggle-group"
65
- import { IconSize } from "../ui/typography"
62
+ IconSize,
63
+ } from "../ui/typography"
64
+ import { ToggleGroup, ToggleGroupItem } from "../ui/toggle-group"
66
65
 
67
66
  function FontColorModalContent({
68
67
  initialColor,
@@ -390,95 +389,88 @@ function FloatingTextFormat({
390
389
  {editor.isEditable() && (
391
390
  <Flex align="center" gap={1} className="editor-flex-nowrap">
392
391
  <div className="editor-floating-group editor-flex editor-items-center">
393
- <ToggleGroup
394
- type="multiple"
395
- className="editor-flex editor-items-center"
396
- value={[
397
- isBold ? "bold" : "",
398
- isItalic ? "italic" : "",
399
- isUnderline ? "underline" : "",
400
- isStrikethrough ? "strikethrough" : "",
401
- isCode ? "code" : "",
402
- isLink ? "link" : "",
403
- ].filter(Boolean)}
392
+ <Button
393
+ variant="ghost"
394
+ size="sm"
395
+ className="editor-toolbar-item"
396
+ data-state={isBold ? "on" : "off"}
397
+ onClick={() => {
398
+ editor.dispatchCommand(FORMAT_TEXT_COMMAND, "bold")
399
+ }}
400
+ aria-label="Toggle bold"
404
401
  >
405
- <ToggleGroupItem
406
- value="bold"
407
- aria-label="Toggle bold"
408
- className="editor-toolbar-item"
409
- onClick={() => {
410
- editor.dispatchCommand(FORMAT_TEXT_COMMAND, "bold")
411
- }}
412
- size="sm"
413
- >
414
- <IconSize size="sm">
415
- <BoldIcon />
416
- </IconSize>
417
- </ToggleGroupItem>
418
- <ToggleGroupItem
419
- value="italic"
420
- aria-label="Toggle italic"
421
- className="editor-toolbar-item"
422
- onClick={() => {
423
- editor.dispatchCommand(FORMAT_TEXT_COMMAND, "italic")
424
- }}
425
- size="sm"
426
- >
427
- <IconSize size="sm">
428
- <ItalicIcon />
429
- </IconSize>
430
- </ToggleGroupItem>
431
- <ToggleGroupItem
432
- value="underline"
433
- aria-label="Toggle underline"
434
- className="editor-toolbar-item"
435
- onClick={() => {
436
- editor.dispatchCommand(FORMAT_TEXT_COMMAND, "underline")
437
- }}
438
- size="sm"
439
- >
440
- <IconSize size="sm">
441
- <UnderlineIcon />
442
- </IconSize>
443
- </ToggleGroupItem>
444
- <ToggleGroupItem
445
- value="strikethrough"
446
- aria-label="Toggle strikethrough"
447
- className="editor-toolbar-item"
448
- onClick={() => {
449
- editor.dispatchCommand(FORMAT_TEXT_COMMAND, "strikethrough")
450
- }}
451
- size="sm"
452
- >
453
- <IconSize size="sm">
454
- <StrikethroughIcon />
455
- </IconSize>
456
- </ToggleGroupItem>
457
- <ToggleGroupItem
458
- value="code"
459
- aria-label="Toggle code"
460
- className="editor-toolbar-item"
461
- onClick={() => {
462
- editor.dispatchCommand(FORMAT_TEXT_COMMAND, "code")
463
- }}
464
- size="sm"
465
- >
466
- <IconSize size="sm">
467
- <CodeIcon />
468
- </IconSize>
469
- </ToggleGroupItem>
470
- <ToggleGroupItem
471
- value="link"
472
- aria-label="Toggle link"
473
- className="editor-toolbar-item"
474
- onClick={insertLink}
475
- size="sm"
476
- >
477
- <IconSize size="sm">
478
- <LinkIcon />
479
- </IconSize>
480
- </ToggleGroupItem>
481
- </ToggleGroup>
402
+ <IconSize size="sm">
403
+ <BoldIcon />
404
+ </IconSize>
405
+ </Button>
406
+ <Button
407
+ variant="ghost"
408
+ size="sm"
409
+ className="editor-toolbar-item"
410
+ data-state={isItalic ? "on" : "off"}
411
+ onClick={() => {
412
+ editor.dispatchCommand(FORMAT_TEXT_COMMAND, "italic")
413
+ }}
414
+ aria-label="Toggle italic"
415
+ >
416
+ <IconSize size="sm">
417
+ <ItalicIcon />
418
+ </IconSize>
419
+ </Button>
420
+ <Button
421
+ variant="ghost"
422
+ size="sm"
423
+ className="editor-toolbar-item"
424
+ data-state={isUnderline ? "on" : "off"}
425
+ onClick={() => {
426
+ editor.dispatchCommand(FORMAT_TEXT_COMMAND, "underline")
427
+ }}
428
+ aria-label="Toggle underline"
429
+ >
430
+ <IconSize size="sm">
431
+ <UnderlineIcon />
432
+ </IconSize>
433
+ </Button>
434
+ <Button
435
+ variant="ghost"
436
+ size="sm"
437
+ className="editor-toolbar-item"
438
+ data-state={isStrikethrough ? "on" : "off"}
439
+ onClick={() => {
440
+ editor.dispatchCommand(FORMAT_TEXT_COMMAND, "strikethrough")
441
+ }}
442
+ aria-label="Toggle strikethrough"
443
+ >
444
+ <IconSize size="sm">
445
+ <StrikethroughIcon />
446
+ </IconSize>
447
+ </Button>
448
+ <Button
449
+ variant="ghost"
450
+ size="sm"
451
+ className="editor-toolbar-item"
452
+ data-state={isCode ? "on" : "off"}
453
+ onClick={() => {
454
+ editor.dispatchCommand(FORMAT_TEXT_COMMAND, "code")
455
+ }}
456
+ aria-label="Toggle code"
457
+ >
458
+ <IconSize size="sm">
459
+ <CodeIcon />
460
+ </IconSize>
461
+ </Button>
462
+ <Button
463
+ variant="ghost"
464
+ size="sm"
465
+ className="editor-toolbar-item"
466
+ data-state={isLink ? "on" : "off"}
467
+ onClick={insertLink}
468
+ aria-label="Toggle link"
469
+ >
470
+ <IconSize size="sm">
471
+ <LinkIcon />
472
+ </IconSize>
473
+ </Button>
482
474
  </div>
483
475
 
484
476
  <Separator orientation="vertical" className="editor-separator--vertical" />
@@ -220,10 +220,10 @@ function ImagePickerFolderTree({
220
220
  {`${folder.images.length} hình${folder.subfolders.length > 0 ? `, ${folder.subfolders.length} thư mục` : ""}`}
221
221
  </TypographySpanSmallMuted>
222
222
  </CollapsibleTrigger>
223
- <CollapsibleContent className="editor-ml-4 editor-mt-1">
223
+ <CollapsibleContent className="editor-ml-4 editor-mt-1 editor-flex editor-flex-col editor-gap-1">
224
224
  {/* Render images in this folder */}
225
225
  {folder.images.length > 0 && (
226
- <div className="editor-image-grid">
226
+ <div className="editor-image-grid editor-gap-2">
227
227
  {folder.images.map((image) => (
228
228
  <button
229
229
  key={image.fileName}
@@ -345,12 +345,12 @@ export function InsertImageUploadsDialogBody({
345
345
  <TypographySpanSmallMuted>Chưa có hình ảnh nào được upload</TypographySpanSmallMuted>
346
346
  </div>
347
347
  ) : (
348
- <div className="editor-scroll-area">
348
+ <div className="editor-scroll-area editor-flex editor-flex-col editor-gap-1">
349
349
  {/* Render root level images if any */}
350
350
  {folderTree.images.length > 0 && (
351
351
  <div className="editor-mb-3">
352
352
  <TypographySpanSmallMuted className="editor-mb-2 editor-px-1">Root</TypographySpanSmallMuted>
353
- <div className="editor-image-grid">
353
+ <div className="editor-image-grid editor-gap-2">
354
354
  {folderTree.images.map((image) => (
355
355
  <button
356
356
  key={image.fileName}