@groupher/rich-editor 0.0.8 → 0.0.9

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 (36) hide show
  1. package/dist/rich-editor.es.js +31268 -20037
  2. package/dist/rich-editor.umd.js +77 -96
  3. package/package.json +17 -2
  4. package/src/RichEditor.tsx +204 -92
  5. package/src/components/editor/editor-kit.tsx +27 -0
  6. package/src/components/editor/plugins/autoformat-kit.tsx +99 -0
  7. package/src/components/editor/plugins/callout-kit.tsx +7 -0
  8. package/src/components/editor/plugins/emoji-kit.tsx +14 -0
  9. package/src/components/editor/plugins/indent-kit.tsx +12 -0
  10. package/src/components/editor/plugins/link-kit.tsx +13 -0
  11. package/src/components/editor/plugins/list-kit.tsx +17 -0
  12. package/src/components/editor/plugins/mention-kit.tsx +17 -0
  13. package/src/components/editor/plugins/slash-kit.tsx +15 -0
  14. package/src/components/editor/plugins/toggle-kit.tsx +7 -0
  15. package/src/components/ui/action-bar.tsx +208 -0
  16. package/src/components/ui/block-list.tsx +94 -0
  17. package/src/components/ui/button.tsx +49 -50
  18. package/src/components/ui/callout-node.tsx +65 -0
  19. package/src/components/ui/editor-static.tsx +44 -44
  20. package/src/components/ui/editor.tsx +107 -107
  21. package/src/components/ui/emoji-node.tsx +71 -0
  22. package/src/components/ui/emoji-toolbar-button.tsx +618 -0
  23. package/src/components/ui/floating-toolbar.tsx +86 -0
  24. package/src/components/ui/inline-combobox.tsx +414 -0
  25. package/src/components/ui/link-node.tsx +31 -0
  26. package/src/components/ui/link-toolbar-button.tsx +33 -0
  27. package/src/components/ui/mention-node.tsx +126 -0
  28. package/src/components/ui/slash-node.tsx +191 -0
  29. package/src/components/ui/toggle-node.tsx +36 -0
  30. package/src/components/ui/toolbar.tsx +10 -10
  31. package/src/hooks/use-debounce.ts +15 -0
  32. package/src/hooks/use-mounted.ts +11 -0
  33. package/src/i18n.tsx +155 -0
  34. package/src/main.tsx +35 -14
  35. package/src/mention-context.tsx +32 -0
  36. package/src/vite-env.d.ts +7 -0
@@ -0,0 +1,618 @@
1
+ 'use client';
2
+ /* eslint-disable react-hooks/refs */
3
+
4
+ import * as React from 'react';
5
+
6
+ import type { Emoji } from '@emoji-mart/data';
7
+
8
+ import {
9
+ type EmojiCategoryList,
10
+ type EmojiIconList,
11
+ type GridRow,
12
+ EmojiSettings,
13
+ } from '@platejs/emoji';
14
+ import {
15
+ type EmojiDropdownMenuOptions,
16
+ type UseEmojiPickerType,
17
+ useEmojiDropdownMenuState,
18
+ } from '@platejs/emoji/react';
19
+ import * as Popover from '@radix-ui/react-popover';
20
+ import {
21
+ AppleIcon,
22
+ ClockIcon,
23
+ CompassIcon,
24
+ FlagIcon,
25
+ LeafIcon,
26
+ LightbulbIcon,
27
+ MusicIcon,
28
+ SearchIcon,
29
+ SmileIcon,
30
+ StarIcon,
31
+ XIcon,
32
+ } from 'lucide-react';
33
+
34
+ import { Button } from '@/components/ui/button';
35
+ import {
36
+ Tooltip,
37
+ TooltipContent,
38
+ TooltipProvider,
39
+ TooltipTrigger,
40
+ } from '@/components/ui/tooltip';
41
+ import { cn } from '@/lib/utils';
42
+
43
+ import { ToolbarButton } from './toolbar';
44
+
45
+ export function EmojiToolbarButton({
46
+ options,
47
+ ...props
48
+ }: {
49
+ options?: EmojiDropdownMenuOptions;
50
+ } & React.ComponentPropsWithoutRef<typeof ToolbarButton>) {
51
+ const { emojiPickerState, isOpen, setIsOpen } =
52
+ useEmojiDropdownMenuState(options);
53
+
54
+ return (
55
+ <EmojiPopover
56
+ control={
57
+ <ToolbarButton pressed={isOpen} tooltip="Emoji" isDropdown {...props}>
58
+ <SmileIcon />
59
+ </ToolbarButton>
60
+ }
61
+ isOpen={isOpen}
62
+ setIsOpen={setIsOpen}
63
+ >
64
+ <EmojiPicker
65
+ {...emojiPickerState}
66
+ isOpen={isOpen}
67
+ setIsOpen={setIsOpen}
68
+ settings={options?.settings}
69
+ />
70
+ </EmojiPopover>
71
+ );
72
+ }
73
+
74
+ export function EmojiPopover({
75
+ children,
76
+ control,
77
+ isOpen,
78
+ setIsOpen,
79
+ }: {
80
+ children: React.ReactNode;
81
+ control: React.ReactNode;
82
+ isOpen: boolean;
83
+ setIsOpen: (open: boolean) => void;
84
+ }) {
85
+ return (
86
+ <Popover.Root open={isOpen} onOpenChange={setIsOpen}>
87
+ <Popover.Trigger asChild>{control}</Popover.Trigger>
88
+
89
+ <Popover.Portal>
90
+ <Popover.Content className="z-100">{children}</Popover.Content>
91
+ </Popover.Portal>
92
+ </Popover.Root>
93
+ );
94
+ }
95
+
96
+ export function EmojiPicker({
97
+ clearSearch,
98
+ emoji,
99
+ emojiLibrary,
100
+ focusedCategory,
101
+ hasFound,
102
+ i18n,
103
+ icons = {
104
+ categories: emojiCategoryIcons,
105
+ search: emojiSearchIcons,
106
+ },
107
+ isSearching,
108
+ refs,
109
+ searchResult,
110
+ searchValue,
111
+ setSearch,
112
+ settings = EmojiSettings,
113
+ visibleCategories,
114
+ handleCategoryClick,
115
+ onMouseOver,
116
+ onSelectEmoji,
117
+ }: Omit<UseEmojiPickerType, 'icons'> & {
118
+ icons?: EmojiIconList<React.ReactElement>;
119
+ }) {
120
+ return (
121
+ <div
122
+ className={cn(
123
+ 'flex h-[23rem] w-80 flex-col rounded-xl bg-popover text-popover-foreground',
124
+ 'border shadow-md'
125
+ )}
126
+ >
127
+ <EmojiPickerNavigation
128
+ onClick={handleCategoryClick}
129
+ emojiLibrary={emojiLibrary}
130
+ focusedCategory={focusedCategory}
131
+ i18n={i18n}
132
+ icons={icons}
133
+ />
134
+ <EmojiPickerSearchBar
135
+ i18n={i18n}
136
+ searchValue={searchValue}
137
+ setSearch={setSearch}
138
+ >
139
+ <EmojiPickerSearchAndClear
140
+ clearSearch={clearSearch}
141
+ i18n={i18n}
142
+ searchValue={searchValue}
143
+ />
144
+ </EmojiPickerSearchBar>
145
+ <EmojiPickerContent
146
+ onMouseOver={onMouseOver}
147
+ onSelectEmoji={onSelectEmoji}
148
+ emojiLibrary={emojiLibrary}
149
+ i18n={i18n}
150
+ isSearching={isSearching}
151
+ refs={refs}
152
+ searchResult={searchResult}
153
+ settings={settings}
154
+ visibleCategories={visibleCategories}
155
+ />
156
+ <EmojiPickerPreview
157
+ emoji={emoji}
158
+ hasFound={hasFound}
159
+ i18n={i18n}
160
+ isSearching={isSearching}
161
+ />
162
+ </div>
163
+ );
164
+ }
165
+
166
+ const EmojiButton = React.memo(function EmojiButton({
167
+ emoji,
168
+ index,
169
+ onMouseOver,
170
+ onSelect,
171
+ }: {
172
+ emoji: Emoji;
173
+ index: number;
174
+ onMouseOver: (emoji?: Emoji) => void;
175
+ onSelect: (emoji: Emoji) => void;
176
+ }) {
177
+ return (
178
+ <button
179
+ className="group relative flex size-9 cursor-pointer items-center justify-center border-none bg-transparent text-2xl leading-none"
180
+ onClick={() => onSelect(emoji)}
181
+ onMouseEnter={() => onMouseOver(emoji)}
182
+ onMouseLeave={() => onMouseOver()}
183
+ aria-label={emoji.skins[0].native}
184
+ data-index={index}
185
+ tabIndex={-1}
186
+ type="button"
187
+ >
188
+ <div
189
+ className="absolute inset-0 rounded-full opacity-0 group-hover:opacity-100"
190
+ aria-hidden="true"
191
+ />
192
+ <span
193
+ className="relative"
194
+ style={{
195
+ fontFamily:
196
+ '"Apple Color Emoji", "Segoe UI Emoji", NotoColorEmoji, "Noto Color Emoji", "Segoe UI Symbol", "Android Emoji", EmojiSymbols',
197
+ }}
198
+ data-emoji-set="native"
199
+ >
200
+ {emoji.skins[0].native}
201
+ </span>
202
+ </button>
203
+ );
204
+ });
205
+
206
+ const RowOfButtons = React.memo(function RowOfButtons({
207
+ emojiLibrary,
208
+ row,
209
+ onMouseOver,
210
+ onSelectEmoji,
211
+ }: {
212
+ row: GridRow;
213
+ } & Pick<
214
+ UseEmojiPickerType,
215
+ 'emojiLibrary' | 'onMouseOver' | 'onSelectEmoji'
216
+ >) {
217
+ return (
218
+ <div key={row.id} className="flex" data-index={row.id}>
219
+ {row.elements.map((emojiId: string, index: number) => (
220
+ <EmojiButton
221
+ key={emojiId}
222
+ onMouseOver={onMouseOver}
223
+ onSelect={onSelectEmoji}
224
+ emoji={emojiLibrary.getEmoji(emojiId)}
225
+ index={index}
226
+ />
227
+ ))}
228
+ </div>
229
+ );
230
+ });
231
+
232
+ function EmojiPickerContent({
233
+ emojiLibrary,
234
+ i18n,
235
+ isSearching = false,
236
+ refs,
237
+ searchResult,
238
+ settings = EmojiSettings,
239
+ visibleCategories,
240
+ onMouseOver,
241
+ onSelectEmoji,
242
+ }: Pick<
243
+ UseEmojiPickerType,
244
+ | 'emojiLibrary'
245
+ | 'i18n'
246
+ | 'isSearching'
247
+ | 'onMouseOver'
248
+ | 'onSelectEmoji'
249
+ | 'refs'
250
+ | 'searchResult'
251
+ | 'settings'
252
+ | 'visibleCategories'
253
+ >) {
254
+ const getRowWidth = settings.perLine.value * settings.buttonSize.value;
255
+
256
+ const isCategoryVisible = React.useCallback(
257
+ (categoryId: EmojiCategoryList) =>
258
+ visibleCategories.has(categoryId)
259
+ ? visibleCategories.get(categoryId)
260
+ : false,
261
+ [visibleCategories]
262
+ );
263
+
264
+ const EmojiList = React.useCallback(
265
+ () =>
266
+ emojiLibrary
267
+ .getGrid()
268
+ .sections()
269
+ .map(({ id: categoryId }: { id: EmojiCategoryList }) => {
270
+ const section = emojiLibrary.getGrid().section(categoryId);
271
+ const { buttonSize } = settings;
272
+
273
+ return (
274
+ <div
275
+ key={categoryId}
276
+ ref={section.root}
277
+ style={{ width: getRowWidth }}
278
+ data-id={categoryId}
279
+ >
280
+ <div className="sticky -top-px z-1 bg-popover/90 p-1 py-2 text-sm font-semibold backdrop-blur-xs">
281
+ {i18n.categories[categoryId]}
282
+ </div>
283
+ <div
284
+ className="relative flex flex-wrap"
285
+ style={{ height: section.getRows().length * buttonSize.value }}
286
+ >
287
+ {isCategoryVisible(categoryId) &&
288
+ section
289
+ .getRows()
290
+ .map((row: GridRow) => (
291
+ <RowOfButtons
292
+ key={row.id}
293
+ onMouseOver={onMouseOver}
294
+ onSelectEmoji={onSelectEmoji}
295
+ emojiLibrary={emojiLibrary}
296
+ row={row}
297
+ />
298
+ ))}
299
+ </div>
300
+ </div>
301
+ );
302
+ }),
303
+ [
304
+ emojiLibrary,
305
+ getRowWidth,
306
+ i18n.categories,
307
+ isCategoryVisible,
308
+ onSelectEmoji,
309
+ onMouseOver,
310
+ settings,
311
+ ]
312
+ );
313
+
314
+ const SearchList = React.useCallback(
315
+ () => (
316
+ <div style={{ width: getRowWidth }} data-id="search">
317
+ <div className="sticky -top-px z-1 bg-popover/90 p-1 py-2 text-sm font-semibold text-card-foreground backdrop-blur-xs">
318
+ {i18n.searchResult}
319
+ </div>
320
+ <div className="relative flex flex-wrap">
321
+ {searchResult.map((emoji: Emoji, index: number) => (
322
+ <EmojiButton
323
+ key={emoji.id}
324
+ onMouseOver={onMouseOver}
325
+ onSelect={onSelectEmoji}
326
+ emoji={emojiLibrary.getEmoji(emoji.id)}
327
+ index={index}
328
+ />
329
+ ))}
330
+ </div>
331
+ </div>
332
+ ),
333
+ [
334
+ emojiLibrary,
335
+ getRowWidth,
336
+ i18n.searchResult,
337
+ searchResult,
338
+ onSelectEmoji,
339
+ onMouseOver,
340
+ ]
341
+ );
342
+
343
+ return (
344
+ <div
345
+ ref={refs.current.contentRoot}
346
+ className={cn(
347
+ 'h-full min-h-[50%] overflow-y-auto overflow-x-hidden px-2',
348
+ '[&::-webkit-scrollbar]:w-4',
349
+ '[&::-webkit-scrollbar-button]:hidden [&::-webkit-scrollbar-button]:size-0',
350
+ '[&::-webkit-scrollbar-thumb]:min-h-11 [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-muted [&::-webkit-scrollbar-thumb]:hover:bg-muted-foreground/25',
351
+ '[&::-webkit-scrollbar-thumb]:border-4 [&::-webkit-scrollbar-thumb]:border-popover [&::-webkit-scrollbar-thumb]:border-solid [&::-webkit-scrollbar-thumb]:bg-clip-padding'
352
+ )}
353
+ data-id="scroll"
354
+ >
355
+ <div ref={refs.current.content} className="h-full">
356
+ {isSearching ? SearchList() : EmojiList()}
357
+ </div>
358
+ </div>
359
+ );
360
+ }
361
+
362
+ function EmojiPickerSearchBar({
363
+ children,
364
+ i18n,
365
+ searchValue,
366
+ setSearch,
367
+ }: {
368
+ children: React.ReactNode;
369
+ } & Pick<UseEmojiPickerType, 'i18n' | 'searchValue' | 'setSearch'>) {
370
+ return (
371
+ <div className="flex items-center px-2">
372
+ <div className="relative flex grow items-center">
373
+ <input
374
+ className="block w-full appearance-none rounded-full border-0 bg-muted px-10 py-2 text-sm outline-none placeholder:text-muted-foreground focus-visible:outline-none"
375
+ value={searchValue}
376
+ onChange={(event) => setSearch(event.target.value)}
377
+ placeholder={i18n.search}
378
+ aria-label="Search"
379
+ autoComplete="off"
380
+ type="text"
381
+ />
382
+ {children}
383
+ </div>
384
+ </div>
385
+ );
386
+ }
387
+
388
+ function EmojiPickerSearchAndClear({
389
+ clearSearch,
390
+ i18n,
391
+ searchValue,
392
+ }: Pick<UseEmojiPickerType, 'clearSearch' | 'i18n' | 'searchValue'>) {
393
+ return (
394
+ <div className="flex items-center text-foreground">
395
+ <div
396
+ className={cn(
397
+ 'absolute left-2.5 top-1/2 z-10 flex size-5 -translate-y-1/2 items-center justify-center text-foreground'
398
+ )}
399
+ >
400
+ {emojiSearchIcons.loupe}
401
+ </div>
402
+ {searchValue && (
403
+ <Button
404
+ size="icon"
405
+ variant="ghost"
406
+ className={cn(
407
+ 'absolute right-0.5 top-1/2 flex size-8 -translate-y-1/2 cursor-pointer items-center justify-center rounded-full border-none bg-transparent text-popover-foreground hover:bg-transparent'
408
+ )}
409
+ onClick={clearSearch}
410
+ title={i18n.clear}
411
+ aria-label="Clear"
412
+ type="button"
413
+ >
414
+ {emojiSearchIcons.delete}
415
+ </Button>
416
+ )}
417
+ </div>
418
+ );
419
+ }
420
+
421
+ function EmojiPreview({ emoji }: Pick<UseEmojiPickerType, 'emoji'>) {
422
+ return (
423
+ <div className="flex h-14 min-h-14 max-h-14 items-center border-t border-muted p-2">
424
+ <div className="flex items-center justify-center text-2xl">
425
+ {emoji?.skins[0].native}
426
+ </div>
427
+ <div className="overflow-hidden pl-2">
428
+ <div className="truncate text-sm font-semibold">{emoji?.name}</div>
429
+ <div className="truncate text-sm">{`:${emoji?.id}:`}</div>
430
+ </div>
431
+ </div>
432
+ );
433
+ }
434
+
435
+ function NoEmoji({ i18n }: Pick<UseEmojiPickerType, 'i18n'>) {
436
+ return (
437
+ <div className="flex h-14 min-h-14 max-h-14 items-center border-t border-muted p-2">
438
+ <div className="flex items-center justify-center text-2xl">😢</div>
439
+ <div className="overflow-hidden pl-2">
440
+ <div className="truncate text-sm font-bold">
441
+ {i18n.searchNoResultsTitle}
442
+ </div>
443
+ <div className="truncate text-sm">{i18n.searchNoResultsSubtitle}</div>
444
+ </div>
445
+ </div>
446
+ );
447
+ }
448
+
449
+ function PickAnEmoji({ i18n }: Pick<UseEmojiPickerType, 'i18n'>) {
450
+ return (
451
+ <div className="flex h-14 min-h-14 max-h-14 items-center border-t border-muted p-2">
452
+ <div className="flex items-center justify-center text-2xl">☝️</div>
453
+ <div className="overflow-hidden pl-2">
454
+ <div className="truncate text-sm font-semibold">{i18n.pick}</div>
455
+ </div>
456
+ </div>
457
+ );
458
+ }
459
+
460
+ function EmojiPickerPreview({
461
+ emoji,
462
+ hasFound = true,
463
+ i18n,
464
+ isSearching = false,
465
+ ...props
466
+ }: Pick<UseEmojiPickerType, 'emoji' | 'hasFound' | 'i18n' | 'isSearching'>) {
467
+ const showPickEmoji = !emoji && (!isSearching || hasFound);
468
+ const showNoEmoji = isSearching && !hasFound;
469
+ const showPreview = emoji && !showNoEmoji && !showNoEmoji;
470
+
471
+ return (
472
+ <>
473
+ {showPreview && <EmojiPreview emoji={emoji} {...props} />}
474
+ {showPickEmoji && <PickAnEmoji i18n={i18n} {...props} />}
475
+ {showNoEmoji && <NoEmoji i18n={i18n} {...props} />}
476
+ </>
477
+ );
478
+ }
479
+
480
+ function EmojiPickerNavigation({
481
+ emojiLibrary,
482
+ focusedCategory,
483
+ i18n,
484
+ icons,
485
+ onClick,
486
+ }: {
487
+ onClick: (id: EmojiCategoryList) => void;
488
+ } & Pick<
489
+ UseEmojiPickerType,
490
+ 'emojiLibrary' | 'focusedCategory' | 'i18n' | 'icons'
491
+ >) {
492
+ return (
493
+ <TooltipProvider delayDuration={500}>
494
+ <nav className="mb-2.5 border-b border-b-border border-solid p-1.5">
495
+ <div className="relative flex items-center justify-evenly">
496
+ {emojiLibrary
497
+ .getGrid()
498
+ .sections()
499
+ .map(({ id }: { id: EmojiCategoryList }) => (
500
+ <Tooltip key={id}>
501
+ <TooltipTrigger asChild>
502
+ <Button
503
+ size="sm"
504
+ variant="ghost"
505
+ className={cn(
506
+ 'h-fit rounded-full fill-current p-1.5 text-muted-foreground hover:bg-muted hover:text-muted-foreground',
507
+ id === focusedCategory &&
508
+ 'pointer-events-none bg-accent fill-current text-accent-foreground'
509
+ )}
510
+ onClick={() => {
511
+ onClick(id);
512
+ }}
513
+ aria-label={i18n.categories[id]}
514
+ type="button"
515
+ >
516
+ <span className="inline-flex size-5 items-center justify-center">
517
+ {icons.categories[id].outline}
518
+ </span>
519
+ </Button>
520
+ </TooltipTrigger>
521
+ <TooltipContent side="bottom">
522
+ {i18n.categories[id]}
523
+ </TooltipContent>
524
+ </Tooltip>
525
+ ))}
526
+ </div>
527
+ </nav>
528
+ </TooltipProvider>
529
+ );
530
+ }
531
+
532
+ const emojiCategoryIcons: Record<
533
+ EmojiCategoryList,
534
+ {
535
+ outline: React.ReactElement;
536
+ solid: React.ReactElement;
537
+ }
538
+ > = {
539
+ activity: {
540
+ outline: (
541
+ <svg
542
+ className="size-full"
543
+ fill="none"
544
+ stroke="currentColor"
545
+ strokeLinecap="round"
546
+ strokeLinejoin="round"
547
+ strokeWidth="2"
548
+ viewBox="0 0 24 24"
549
+ xmlns="http://www.w3.org/2000/svg"
550
+ >
551
+ <title>Activity</title>
552
+ <circle cx="12" cy="12" r="10" />
553
+ <path d="M2.1 13.4A10.1 10.1 0 0 0 13.4 2.1" />
554
+ <path d="m5 4.9 14 14.2" />
555
+ <path d="M21.9 10.6a10.1 10.1 0 0 0-11.3 11.3" />
556
+ </svg>
557
+ ),
558
+ solid: (
559
+ <svg
560
+ className="size-full"
561
+ fill="none"
562
+ stroke="currentColor"
563
+ strokeLinecap="round"
564
+ strokeLinejoin="round"
565
+ strokeWidth="2"
566
+ viewBox="0 0 24 24"
567
+ xmlns="http://www.w3.org/2000/svg"
568
+ >
569
+ <title>Activity</title>
570
+ <circle cx="12" cy="12" r="10" />
571
+ <path d="M2.1 13.4A10.1 10.1 0 0 0 13.4 2.1" />
572
+ <path d="m5 4.9 14 14.2" />
573
+ <path d="M21.9 10.6a10.1 10.1 0 0 0-11.3 11.3" />
574
+ </svg>
575
+ ),
576
+ },
577
+ custom: {
578
+ outline: <StarIcon className="size-full" />,
579
+ solid: <StarIcon className="size-full" />,
580
+ },
581
+ flags: {
582
+ outline: <FlagIcon className="size-full" />,
583
+ solid: <FlagIcon className="size-full" />,
584
+ },
585
+ foods: {
586
+ outline: <AppleIcon className="size-full" />,
587
+ solid: <AppleIcon className="size-full" />,
588
+ },
589
+ frequent: {
590
+ outline: <ClockIcon className="size-full" />,
591
+ solid: <ClockIcon className="size-full" />,
592
+ },
593
+ nature: {
594
+ outline: <LeafIcon className="size-full" />,
595
+ solid: <LeafIcon className="size-full" />,
596
+ },
597
+ objects: {
598
+ outline: <LightbulbIcon className="size-full" />,
599
+ solid: <LightbulbIcon className="size-full" />,
600
+ },
601
+ people: {
602
+ outline: <SmileIcon className="size-full" />,
603
+ solid: <SmileIcon className="size-full" />,
604
+ },
605
+ places: {
606
+ outline: <CompassIcon className="size-full" />,
607
+ solid: <CompassIcon className="size-full" />,
608
+ },
609
+ symbols: {
610
+ outline: <MusicIcon className="size-full" />,
611
+ solid: <MusicIcon className="size-full" />,
612
+ },
613
+ };
614
+
615
+ const emojiSearchIcons = {
616
+ delete: <XIcon className="size-4 text-current" />,
617
+ loupe: <SearchIcon className="size-4 text-current" />,
618
+ };
@@ -0,0 +1,86 @@
1
+ 'use client';
2
+
3
+ import type * as React from 'react';
4
+
5
+ import {
6
+ type FloatingToolbarState,
7
+ flip,
8
+ offset,
9
+ useFloatingToolbar,
10
+ useFloatingToolbarState,
11
+ } from '@platejs/floating';
12
+ import { useComposedRef } from '@udecode/cn';
13
+ import { KEYS } from 'platejs';
14
+ import {
15
+ useEditorId,
16
+ useEventEditorValue,
17
+ usePluginOption,
18
+ } from 'platejs/react';
19
+
20
+ import { cn } from '@/lib/utils';
21
+
22
+ import { Toolbar } from './toolbar';
23
+
24
+ export function FloatingToolbar({
25
+ children,
26
+ className,
27
+ state,
28
+ ...props
29
+ }: React.ComponentProps<typeof Toolbar> & {
30
+ state?: FloatingToolbarState;
31
+ }) {
32
+ const editorId = useEditorId();
33
+ const focusedEditorId = useEventEditorValue('focus');
34
+ const isFloatingLinkOpen = !!usePluginOption({ key: KEYS.link }, 'mode');
35
+
36
+ const floatingToolbarState = useFloatingToolbarState({
37
+ editorId,
38
+ focusedEditorId,
39
+ hideToolbar: isFloatingLinkOpen,
40
+ ...state,
41
+ floatingOptions: {
42
+ middleware: [
43
+ offset(12),
44
+ flip({
45
+ fallbackPlacements: [
46
+ 'top-start',
47
+ 'top-end',
48
+ 'bottom-start',
49
+ 'bottom-end',
50
+ ],
51
+ padding: 12,
52
+ }),
53
+ ],
54
+ placement: 'top',
55
+ ...state?.floatingOptions,
56
+ },
57
+ });
58
+
59
+ const {
60
+ clickOutsideRef,
61
+ hidden,
62
+ props: rootProps,
63
+ ref: floatingRef,
64
+ } = useFloatingToolbar(floatingToolbarState);
65
+
66
+ const ref = useComposedRef<HTMLDivElement>(props.ref, floatingRef);
67
+
68
+ if (hidden) return null;
69
+
70
+ return (
71
+ <div ref={clickOutsideRef}>
72
+ <Toolbar
73
+ {...props}
74
+ {...rootProps}
75
+ ref={ref}
76
+ className={cn(
77
+ 'scrollbar-hide absolute z-50 overflow-x-auto whitespace-nowrap rounded-md border bg-popover p-1 opacity-100 shadow-md print:hidden',
78
+ 'max-w-[80vw]',
79
+ className
80
+ )}
81
+ >
82
+ {children}
83
+ </Toolbar>
84
+ </div>
85
+ );
86
+ }