@tightknitai/block-kitchen 0.10.17 → 0.10.19

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.
package/dist/index.d.cts CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
- import { B as BlockKitchenProps, S as SupportedBlock, C as ChannelOption, a as SendAsUserStatus, b as SendPayload, c as SendExtrasContext, T as Template, P as PreviewSurface, d as PreviewTheme } from './palette-Cxh1yNzW.cjs';
4
- export { A as AlertBlock, e as AlertLevel, f as BlockKitchenBaseProps, g as BlockKitchenComposeOnlyProps, h as BlockKitchenSendProps, i as BrandPreset, j as BrandTheme, k as BrandTokens, l as BuilderBlock, m as CardBlock, n as CarouselBlock, o as CartesianChart, p as Chart, q as ChartAxisConfig, r as ChartDataPoint, s as ChartSeries, t as ChartType, u as ContainerBlock, v as ContainerChildBlock, w as ContextActionsBlock, x as ContextActionsElement, y as CustomEmoji, D as DataVisualizationBlock, E as EditableVia, F as FeedbackButtonSubobject, z as FeedbackButtonsElement, H as HeaderLevel, I as IconButtonElement, G as IconButtonIcon, J as InputBlock, L as LoadMessageInput, K as LoadResult, M as LoadedMessage, N as LoadingConfig, O as MarkdownBlock, Q as PaletteMode, R as PaletteSection, U as PaletteVariant, V as PieChart, W as PieChartSegment, X as PlanBlock, Y as PreviewHooks, Z as PrimaryActionConfig, _ as PrimaryActionContext, $ as RecentMessage, a0 as SendResult, a1 as SupportedBlockType, a2 as SupportedHeaderBlock, a3 as TableBlock, a4 as TableCell, a5 as TableColumnSetting, a6 as TaskCardBlock, a7 as TaskCardStatus, a8 as UpdatePayload, a9 as UpdateResult, aa as UrlSourceElement, ab as ValidationSummary, ac as VideoBlock, ad as defaultPalette, ae as extraAlertVariant, af as legacyInputVariants } from './palette-Cxh1yNzW.cjs';
3
+ import { B as BlockKitchenProps, S as SupportedBlock, C as ChannelOption, a as SendAsUserStatus, b as SendPayload, c as SendExtrasContext, T as Template, P as PreviewSurface, d as PreviewTheme } from './palette-DNUiqoQK.cjs';
4
+ export { A as AlertBlock, e as AlertLevel, f as BlockKitchenBaseProps, g as BlockKitchenComposeOnlyProps, h as BlockKitchenSendProps, i as BrandPreset, j as BrandTheme, k as BrandTokens, l as BuilderBlock, m as CardBlock, n as CarouselBlock, o as CartesianChart, p as Chart, q as ChartAxisConfig, r as ChartDataPoint, s as ChartSeries, t as ChartType, u as ContainerBlock, v as ContainerChildBlock, w as ContextActionsBlock, x as ContextActionsElement, y as CustomEmoji, D as DataVisualizationBlock, E as EditableVia, F as FeedbackButtonSubobject, z as FeedbackButtonsElement, H as HeaderLevel, I as IconButtonElement, G as IconButtonIcon, J as InputBlock, L as LoadMessageInput, K as LoadResult, M as LoadedMessage, N as LoadingConfig, O as MarkdownBlock, Q as MentionUser, R as MentionsConfig, U as PaletteMode, V as PaletteSection, W as PaletteVariant, X as PieChart, Y as PieChartSegment, Z as PlanBlock, _ as PreviewHooks, $ as PrimaryActionConfig, a0 as PrimaryActionContext, a1 as RecentMessage, a2 as SendResult, a3 as SupportedBlockType, a4 as SupportedHeaderBlock, a5 as TableBlock, a6 as TableCell, a7 as TableColumnSetting, a8 as TaskCardBlock, a9 as TaskCardStatus, aa as UpdatePayload, ab as UpdateResult, ac as UrlSourceElement, ad as ValidationSummary, ae as VideoBlock, af as defaultPalette, ag as extraAlertVariant, ah as legacyInputVariants } from './palette-DNUiqoQK.cjs';
5
5
  export { decodeBlocksFromString, encodeBlocksToString, toSlackBlocks } from './helpers.cjs';
6
6
  import 'slack-web-api-client';
7
7
 
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
- import { B as BlockKitchenProps, S as SupportedBlock, C as ChannelOption, a as SendAsUserStatus, b as SendPayload, c as SendExtrasContext, T as Template, P as PreviewSurface, d as PreviewTheme } from './palette-Cxh1yNzW.js';
4
- export { A as AlertBlock, e as AlertLevel, f as BlockKitchenBaseProps, g as BlockKitchenComposeOnlyProps, h as BlockKitchenSendProps, i as BrandPreset, j as BrandTheme, k as BrandTokens, l as BuilderBlock, m as CardBlock, n as CarouselBlock, o as CartesianChart, p as Chart, q as ChartAxisConfig, r as ChartDataPoint, s as ChartSeries, t as ChartType, u as ContainerBlock, v as ContainerChildBlock, w as ContextActionsBlock, x as ContextActionsElement, y as CustomEmoji, D as DataVisualizationBlock, E as EditableVia, F as FeedbackButtonSubobject, z as FeedbackButtonsElement, H as HeaderLevel, I as IconButtonElement, G as IconButtonIcon, J as InputBlock, L as LoadMessageInput, K as LoadResult, M as LoadedMessage, N as LoadingConfig, O as MarkdownBlock, Q as PaletteMode, R as PaletteSection, U as PaletteVariant, V as PieChart, W as PieChartSegment, X as PlanBlock, Y as PreviewHooks, Z as PrimaryActionConfig, _ as PrimaryActionContext, $ as RecentMessage, a0 as SendResult, a1 as SupportedBlockType, a2 as SupportedHeaderBlock, a3 as TableBlock, a4 as TableCell, a5 as TableColumnSetting, a6 as TaskCardBlock, a7 as TaskCardStatus, a8 as UpdatePayload, a9 as UpdateResult, aa as UrlSourceElement, ab as ValidationSummary, ac as VideoBlock, ad as defaultPalette, ae as extraAlertVariant, af as legacyInputVariants } from './palette-Cxh1yNzW.js';
3
+ import { B as BlockKitchenProps, S as SupportedBlock, C as ChannelOption, a as SendAsUserStatus, b as SendPayload, c as SendExtrasContext, T as Template, P as PreviewSurface, d as PreviewTheme } from './palette-DNUiqoQK.js';
4
+ export { A as AlertBlock, e as AlertLevel, f as BlockKitchenBaseProps, g as BlockKitchenComposeOnlyProps, h as BlockKitchenSendProps, i as BrandPreset, j as BrandTheme, k as BrandTokens, l as BuilderBlock, m as CardBlock, n as CarouselBlock, o as CartesianChart, p as Chart, q as ChartAxisConfig, r as ChartDataPoint, s as ChartSeries, t as ChartType, u as ContainerBlock, v as ContainerChildBlock, w as ContextActionsBlock, x as ContextActionsElement, y as CustomEmoji, D as DataVisualizationBlock, E as EditableVia, F as FeedbackButtonSubobject, z as FeedbackButtonsElement, H as HeaderLevel, I as IconButtonElement, G as IconButtonIcon, J as InputBlock, L as LoadMessageInput, K as LoadResult, M as LoadedMessage, N as LoadingConfig, O as MarkdownBlock, Q as MentionUser, R as MentionsConfig, U as PaletteMode, V as PaletteSection, W as PaletteVariant, X as PieChart, Y as PieChartSegment, Z as PlanBlock, _ as PreviewHooks, $ as PrimaryActionConfig, a0 as PrimaryActionContext, a1 as RecentMessage, a2 as SendResult, a3 as SupportedBlockType, a4 as SupportedHeaderBlock, a5 as TableBlock, a6 as TableCell, a7 as TableColumnSetting, a8 as TaskCardBlock, a9 as TaskCardStatus, aa as UpdatePayload, ab as UpdateResult, ac as UrlSourceElement, ad as ValidationSummary, ae as VideoBlock, af as defaultPalette, ag as extraAlertVariant, ah as legacyInputVariants } from './palette-DNUiqoQK.js';
5
5
  export { decodeBlocksFromString, encodeBlocksToString, toSlackBlocks } from './helpers.js';
6
6
  import 'slack-web-api-client';
7
7
 
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { useSensors, useSensor, PointerSensor, TouchSensor, KeyboardSensor, pointerWithin, closestCenter, DndContext, DragOverlay, useDroppable, useDraggable } from '@dnd-kit/core';
2
2
  import { sortableKeyboardCoordinates, SortableContext, verticalListSortingStrategy, useSortable } from '@dnd-kit/sortable';
3
- import { X, Circle, Sparkles, FileText, AlignLeft, MousePointerClick, TextCursorInput, LayoutTemplate, Bell, Pilcrow, Image, Video, GalleryHorizontal, Table, ChartColumnBig, Container, Loader2, ExternalLink, AlertTriangle, ChevronRight, ChevronLeft, GripVertical, ChevronsUpDown, Check, Sun, Moon, MessageSquare, AppWindow, Home, Plus, ChevronDown, BookOpen, Undo2, Redo2, Trash2, Code2, Pencil, Send, Search, Copy, Info, CheckCircle2, LayoutGrid, ArrowUp, ArrowDown, Type, Bold, Italic, Strikethrough, Underline, Code, List, ListOrdered, Quote, Smile, Link as Link$1 } from 'lucide-react';
3
+ import { X, Circle, Sparkles, FileText, AlignLeft, MousePointerClick, TextCursorInput, LayoutTemplate, Bell, Pilcrow, Image, Video, GalleryHorizontal, Table, ChartColumnBig, Container, Loader2, ExternalLink, AlertTriangle, ChevronRight, ChevronLeft, GripVertical, ChevronsUpDown, Check, Sun, Moon, MessageSquare, AppWindow, Home, Plus, ChevronDown, BookOpen, Undo2, Redo2, Trash2, Code2, Pencil, Send, Search, Copy, Info, CheckCircle2, LayoutGrid, ArrowUp, ArrowDown, Type, Bold, Italic, Strikethrough, Underline, Code, AtSign, List, ListOrdered, Quote, Smile, Link as Link$1 } from 'lucide-react';
4
4
  import * as React from 'react';
5
- import { createContext, useState, useRef, useEffect, useCallback, useMemo, useId, useContext } from 'react';
5
+ import { createContext, useContext, useState, useRef, useEffect, useCallback, useMemo, useId } from 'react';
6
6
  import { clsx } from 'clsx';
7
7
  import { twMerge } from 'tailwind-merge';
8
8
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
@@ -19,8 +19,9 @@ import { Message } from 'slack-blocks-to-jsx';
19
19
  import { CSS } from '@dnd-kit/utilities';
20
20
  import * as RadioGroupPrimitive from '@radix-ui/react-radio-group';
21
21
  import Link from '@tiptap/extension-link';
22
- import { Node as Node$1, ReactNodeViewRenderer, mergeAttributes, NodeViewWrapper, useEditor, EditorContent } from '@tiptap/react';
22
+ import { Node as Node$1, ReactNodeViewRenderer, mergeAttributes, Extension, NodeViewWrapper, useEditor, EditorContent } from '@tiptap/react';
23
23
  import StarterKit from '@tiptap/starter-kit';
24
+ import { Suggestion, exitSuggestion } from '@tiptap/suggestion';
24
25
 
25
26
  // src/components/block-kitchen.tsx
26
27
 
@@ -1833,6 +1834,13 @@ function CustomEmojiProvider({ customEmojis, children }) {
1833
1834
  function useCustomEmojis() {
1834
1835
  return useContext(CustomEmojiContext);
1835
1836
  }
1837
+ var MentionsContext = createContext(null);
1838
+ function MentionsProvider({ mentions, children }) {
1839
+ return /* @__PURE__ */ jsx(MentionsContext.Provider, { value: mentions ?? null, children });
1840
+ }
1841
+ function useMentions() {
1842
+ return useContext(MentionsContext);
1843
+ }
1836
1844
 
1837
1845
  // src/lib/error-grouping.ts
1838
1846
  function toValidatorSurface(surface) {
@@ -3333,12 +3341,27 @@ function ChannelCombobox({
3333
3341
  ] });
3334
3342
  }
3335
3343
  var EMBED_SELECTOR = "iframe[src], embed[src], object[data], source[src], video[src], audio[src], track[src]";
3344
+ function useLatestHooks(hooks) {
3345
+ const latest = useRef(hooks);
3346
+ latest.current = hooks;
3347
+ const [live] = useState(
3348
+ () => new Proxy(
3349
+ {},
3350
+ {
3351
+ get: (_target, key) => typeof key === "string" ? latest.current?.[key] : void 0,
3352
+ has: (_target, key) => typeof key === "string" && latest.current !== void 0 && key in latest.current
3353
+ }
3354
+ )
3355
+ );
3356
+ return live;
3357
+ }
3336
3358
  function SlackBlockPreview({
3337
3359
  block,
3338
3360
  hooks,
3339
3361
  theme = "light"
3340
3362
  }) {
3341
3363
  const rootRef = useRef(null);
3364
+ const liveHooks = useLatestHooks(hooks);
3342
3365
  const safeBlock = useMemo(() => sanitizeBlock(block), [block]);
3343
3366
  useEffect(() => {
3344
3367
  const root = rootRef.current;
@@ -3393,7 +3416,7 @@ function SlackBlockPreview({
3393
3416
  withoutWrapper: true,
3394
3417
  theme,
3395
3418
  blocks: [safeBlock],
3396
- hooks
3419
+ hooks: liveHooks
3397
3420
  }
3398
3421
  ) })
3399
3422
  );
@@ -5166,14 +5189,16 @@ function makeEmojiImportResolver(customByName, index = getEmojiIndex()) {
5166
5189
  }
5167
5190
 
5168
5191
  // src/lib/rich-text-tiptap.ts
5192
+ var USER_MENTION_NODE = "userMention";
5169
5193
  var SUPPORTED_BLOCK_KINDS = /* @__PURE__ */ new Set([
5170
5194
  "rich_text_section",
5171
5195
  "rich_text_list",
5172
5196
  "rich_text_quote",
5173
5197
  "rich_text_preformatted"
5174
5198
  ]);
5175
- var SUPPORTED_INLINE_KINDS = /* @__PURE__ */ new Set(["text", "link", "emoji"]);
5199
+ var SUPPORTED_INLINE_KINDS = /* @__PURE__ */ new Set(["text", "link", "emoji", "user"]);
5176
5200
  var SUPPORTED_TEXT_STYLE_KEYS = /* @__PURE__ */ new Set(["bold", "italic", "strike", "underline", "code"]);
5201
+ var SUPPORTED_USER_STYLE_KEYS = /* @__PURE__ */ new Set(["bold", "italic", "strike", "underline"]);
5177
5202
  var MAX_LIST_INDENT = 8;
5178
5203
  function detectLossy(block) {
5179
5204
  const out = [];
@@ -5196,6 +5221,17 @@ function detectLossy(block) {
5196
5221
  if ("border" in el && (el.border ?? 0) > 0) {
5197
5222
  out.push({ label: "Quote/preformatted border", where });
5198
5223
  }
5224
+ if (el.type === "rich_text_preformatted") {
5225
+ el.elements?.forEach((inline, i) => {
5226
+ const sub = `${where}.elements[${i}]`;
5227
+ if (inline.type !== "text") {
5228
+ out.push({ label: `In code block: ${inline.type}`, where: sub });
5229
+ } else if (Object.values(inline.style ?? {}).some(Boolean)) {
5230
+ out.push({ label: "Styled text in code block", where: sub });
5231
+ }
5232
+ });
5233
+ return;
5234
+ }
5199
5235
  scanInlines(el.elements ?? [], where, out);
5200
5236
  });
5201
5237
  return out;
@@ -5217,6 +5253,13 @@ function scanInlines(inlines, where, out) {
5217
5253
  }
5218
5254
  }
5219
5255
  }
5256
+ if (inline.type === "user") {
5257
+ for (const k of Object.keys(inline.style ?? {})) {
5258
+ if (!SUPPORTED_USER_STYLE_KEYS.has(k)) {
5259
+ out.push({ label: `Mention style: ${k}`, where: sub });
5260
+ }
5261
+ }
5262
+ }
5220
5263
  });
5221
5264
  }
5222
5265
  var defaultEmojiResolver = (el) => ({
@@ -5328,6 +5371,17 @@ function inlinesToPM(inlines, resolveEmoji) {
5328
5371
  continue;
5329
5372
  }
5330
5373
  out.push({ type: "emoji", attrs });
5374
+ } else if (inline.type === "user") {
5375
+ const user = inline;
5376
+ if (!user.user_id) {
5377
+ continue;
5378
+ }
5379
+ const node = { type: USER_MENTION_NODE, attrs: { userId: user.user_id, name: null } };
5380
+ const marks = styleToMarks(user.style);
5381
+ if (marks.length > 0) {
5382
+ node.marks = marks;
5383
+ }
5384
+ out.push(node);
5331
5385
  } else if (inline.type === "link") {
5332
5386
  const link = inline;
5333
5387
  const text = link.text || link.url || "";
@@ -5461,6 +5515,19 @@ function proseMirrorInlinesToRichTextElements(nodes) {
5461
5515
  out.push(emoji);
5462
5516
  continue;
5463
5517
  }
5518
+ if (node.type === USER_MENTION_NODE) {
5519
+ const userId = String(node.attrs?.userId ?? "");
5520
+ if (!userId) {
5521
+ continue;
5522
+ }
5523
+ const user = { type: "user", user_id: userId };
5524
+ const style2 = userStyle(node.marks ?? []);
5525
+ if (Object.keys(style2).length > 0) {
5526
+ user.style = style2;
5527
+ }
5528
+ out.push(user);
5529
+ continue;
5530
+ }
5464
5531
  if (node.type === "hardBreak") {
5465
5532
  out.push({ type: "text", text: "\n" });
5466
5533
  continue;
@@ -5515,6 +5582,15 @@ function marksToStyle(marks) {
5515
5582
  }
5516
5583
  return style;
5517
5584
  }
5585
+ function userStyle(marks) {
5586
+ const style = {};
5587
+ for (const [key, value] of Object.entries(marksToStyle(marks))) {
5588
+ if (SUPPORTED_USER_STYLE_KEYS.has(key)) {
5589
+ style[key] = value;
5590
+ }
5591
+ }
5592
+ return style;
5593
+ }
5518
5594
  function mergeAdjacentTextRuns(inlines) {
5519
5595
  const out = [];
5520
5596
  for (const inline of inlines) {
@@ -5581,6 +5657,91 @@ var EmojiNode = Node$1.create({
5581
5657
  return ReactNodeViewRenderer(EmojiNodeView);
5582
5658
  }
5583
5659
  });
5660
+ var MENTION_SEARCH_DEBOUNCE_MS = 200;
5661
+ function insertUserMention(editor, range, user) {
5662
+ const to = editor.state.doc.textBetween(range.to, Math.min(range.to + 1, editor.state.doc.content.size)) === " " ? range.to + 1 : range.to;
5663
+ editor.chain().focus().insertContentAt({ from: range.from, to }, [
5664
+ { type: USER_MENTION_NODE, attrs: { userId: user.id, name: user.name } },
5665
+ { type: "text", text: " " }
5666
+ ]).run();
5667
+ }
5668
+ function insertMentionTrigger(editor) {
5669
+ const { from } = editor.state.selection;
5670
+ const before = from > 1 ? editor.state.doc.textBetween(from - 1, from, void 0, "\uFFFC") : "";
5671
+ const needsSpace = before !== "" && !/\s/.test(before);
5672
+ editor.chain().focus().insertContent(needsSpace ? " @" : "@").run();
5673
+ }
5674
+ function dismissMentionSuggestion(editor) {
5675
+ exitSuggestion(editor.view);
5676
+ }
5677
+ var MentionSuggestion = Extension.create({
5678
+ name: "userMentionSuggestion",
5679
+ addOptions() {
5680
+ return {
5681
+ isEnabled: () => false,
5682
+ search: async () => [],
5683
+ render: () => ({})
5684
+ };
5685
+ },
5686
+ addProseMirrorPlugins() {
5687
+ const { isEnabled, search, render } = this.options;
5688
+ return [
5689
+ Suggestion({
5690
+ editor: this.editor,
5691
+ char: "@",
5692
+ debounce: MENTION_SEARCH_DEBOUNCE_MS,
5693
+ allow: () => isEnabled(),
5694
+ items: ({ query, signal }) => search(query, signal),
5695
+ command: ({ editor, range, props }) => insertUserMention(editor, range, props),
5696
+ render
5697
+ })
5698
+ ];
5699
+ }
5700
+ });
5701
+ function userMentionLabel(attrs, getUserName) {
5702
+ return getUserName?.(attrs.userId) ?? attrs.name ?? attrs.userId;
5703
+ }
5704
+ function UserMentionNodeView({ node }) {
5705
+ const mentions = useMentions();
5706
+ const attrs = node.attrs;
5707
+ return /* @__PURE__ */ jsxs(NodeViewWrapper, { as: "span", className: "bk-user-mention", "data-user-mention": attrs.userId, children: [
5708
+ "@",
5709
+ userMentionLabel(attrs, mentions?.getUserName)
5710
+ ] });
5711
+ }
5712
+ var UserMentionNode = Node$1.create({
5713
+ name: USER_MENTION_NODE,
5714
+ group: "inline",
5715
+ inline: true,
5716
+ atom: true,
5717
+ selectable: true,
5718
+ addAttributes() {
5719
+ return {
5720
+ userId: {
5721
+ default: "",
5722
+ parseHTML: (element) => element.getAttribute("data-user-mention") ?? "",
5723
+ renderHTML: (attributes) => ({ "data-user-mention": attributes.userId })
5724
+ },
5725
+ name: {
5726
+ default: null,
5727
+ parseHTML: (element) => element.getAttribute("data-user-name"),
5728
+ renderHTML: (attributes) => attributes.name ? { "data-user-name": attributes.name } : {}
5729
+ }
5730
+ };
5731
+ },
5732
+ parseHTML() {
5733
+ return [{ tag: "span[data-user-mention]" }];
5734
+ },
5735
+ renderHTML({ node, HTMLAttributes }) {
5736
+ return ["span", mergeAttributes(HTMLAttributes), `@${userMentionLabel(node.attrs)}`];
5737
+ },
5738
+ renderText({ node }) {
5739
+ return `@${userMentionLabel(node.attrs)}`;
5740
+ },
5741
+ addNodeView() {
5742
+ return ReactNodeViewRenderer(UserMentionNodeView);
5743
+ }
5744
+ });
5584
5745
 
5585
5746
  // src/lib/emoji-recents.ts
5586
5747
  var STORAGE_KEY = "bk:emoji-recents";
@@ -5862,6 +6023,133 @@ function EmojiPickerButton({
5862
6023
  ) })
5863
6024
  ] });
5864
6025
  }
6026
+ function nextMentionPickerState(props, prev) {
6027
+ return {
6028
+ items: props.loading && prev ? prev.items : props.items,
6029
+ loading: props.loading,
6030
+ clientRect: props.clientRect ?? null,
6031
+ command: props.command
6032
+ };
6033
+ }
6034
+ function MentionPicker({
6035
+ editor,
6036
+ state,
6037
+ keyHandlerRef,
6038
+ onDismiss
6039
+ }) {
6040
+ const { items, loading, clientRect, command } = state;
6041
+ const [activeIndex, setActiveIndex] = useState(0);
6042
+ const [shownItems, setShownItems] = useState(items);
6043
+ if (shownItems !== items) {
6044
+ setShownItems(items);
6045
+ setActiveIndex(0);
6046
+ }
6047
+ const listRef = useRef(null);
6048
+ useEffect(() => {
6049
+ keyHandlerRef.current = (event) => {
6050
+ if (event.key === "ArrowDown" || event.key === "ArrowUp") {
6051
+ if (items.length > 0) {
6052
+ const step = event.key === "ArrowDown" ? 1 : -1;
6053
+ setActiveIndex((index) => (index + step + items.length) % items.length);
6054
+ }
6055
+ return true;
6056
+ }
6057
+ if (event.key === "Enter" || event.key === "Tab") {
6058
+ const user = items[activeIndex];
6059
+ if (!user) {
6060
+ return false;
6061
+ }
6062
+ command(user);
6063
+ return true;
6064
+ }
6065
+ return false;
6066
+ };
6067
+ return () => {
6068
+ keyHandlerRef.current = () => false;
6069
+ };
6070
+ }, [keyHandlerRef, items, activeIndex, command]);
6071
+ useEffect(() => {
6072
+ const row = listRef.current?.querySelectorAll('[role="option"]')[activeIndex];
6073
+ row?.scrollIntoView?.({ block: "nearest" });
6074
+ }, [activeIndex]);
6075
+ const anchorRef = useMemo(
6076
+ () => ({
6077
+ current: {
6078
+ getBoundingClientRect: () => clientRect?.() ?? editor.view.dom.getBoundingClientRect()
6079
+ }
6080
+ }),
6081
+ [clientRect, editor]
6082
+ );
6083
+ return /* @__PURE__ */ jsxs(
6084
+ Popover,
6085
+ {
6086
+ open: true,
6087
+ onOpenChange: (open) => {
6088
+ if (!open) {
6089
+ onDismiss();
6090
+ }
6091
+ },
6092
+ children: [
6093
+ /* @__PURE__ */ jsx(PopoverAnchor, { virtualRef: anchorRef }),
6094
+ /* @__PURE__ */ jsx(
6095
+ PopoverContent,
6096
+ {
6097
+ side: "bottom",
6098
+ align: "start",
6099
+ sideOffset: 4,
6100
+ className: "max-h-[min(15rem,var(--radix-popover-content-available-height))] w-64 p-1",
6101
+ onOpenAutoFocus: (e) => e.preventDefault(),
6102
+ onCloseAutoFocus: (e) => e.preventDefault(),
6103
+ onEscapeKeyDown: (e) => e.stopPropagation(),
6104
+ onInteractOutside: (e) => {
6105
+ if (e.target instanceof Node && editor.view.dom.contains(e.target)) {
6106
+ e.preventDefault();
6107
+ }
6108
+ },
6109
+ children: /* @__PURE__ */ jsx("div", { ref: listRef, role: "listbox", "aria-label": "People to mention", children: items.length === 0 ? /* @__PURE__ */ jsx("p", { className: "px-2 py-3 text-center text-xs text-muted-foreground", children: loading ? "Searching\u2026" : "No people match that search." }) : items.map((user, index) => (
6110
+ // biome-ignore lint/a11y/useFocusableInteractive: focus stays in the editor, which drives this list through the suggestion plugin's key handling.
6111
+ // biome-ignore lint/a11y/useKeyWithClickEvents: the keyboard path is the editor's, forwarded via keyHandlerRef.
6112
+ /* @__PURE__ */ jsxs(
6113
+ "div",
6114
+ {
6115
+ role: "option",
6116
+ "aria-selected": index === activeIndex,
6117
+ onPointerDown: (e) => e.preventDefault(),
6118
+ onClick: () => command(user),
6119
+ onMouseMove: () => setActiveIndex(index),
6120
+ className: cn(
6121
+ "flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm text-foreground",
6122
+ index === activeIndex && "bg-accent"
6123
+ ),
6124
+ children: [
6125
+ /* @__PURE__ */ jsx(MentionAvatar, { user }),
6126
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: user.name })
6127
+ ]
6128
+ },
6129
+ user.id
6130
+ )
6131
+ )) })
6132
+ }
6133
+ )
6134
+ ]
6135
+ }
6136
+ );
6137
+ }
6138
+ function MentionAvatar({ user }) {
6139
+ const [failed, setFailed] = useState(false);
6140
+ const src = user.avatarUrl && isSafeImageSrc(user.avatarUrl) ? user.avatarUrl : null;
6141
+ if (src && !failed) {
6142
+ return /* @__PURE__ */ jsx("img", { src, alt: "", className: "h-5 w-5 shrink-0 rounded object-cover", onError: () => setFailed(true) });
6143
+ }
6144
+ return /* @__PURE__ */ jsx(
6145
+ "span",
6146
+ {
6147
+ "aria-hidden": "true",
6148
+ className: "flex h-5 w-5 shrink-0 items-center justify-center rounded bg-muted text-[10px] font-medium text-muted-foreground",
6149
+ children: Array.from(user.name.trim())[0]?.toUpperCase() ?? "@"
6150
+ }
6151
+ );
6152
+ }
5865
6153
  var BLOCK_ELEMENT_LABEL = {
5866
6154
  rich_text_section: "Section",
5867
6155
  rich_text_list: "List",
@@ -6437,7 +6725,7 @@ function LossyBanner({ reasons }) {
6437
6725
  ] });
6438
6726
  }
6439
6727
  var RICH_TEXT_INPUT_RULE_ALLOWLIST = ["bold", "italic", "strike", "code"];
6440
- function buildRichTextExtensions() {
6728
+ function buildRichTextExtensions(mentionSuggestion) {
6441
6729
  return [
6442
6730
  StarterKit.configure({
6443
6731
  heading: false,
@@ -6470,7 +6758,11 @@ function buildRichTextExtensions() {
6470
6758
  // via the link button.
6471
6759
  shouldAutoLink: hasExplicitSafeScheme,
6472
6760
  HTMLAttributes: { rel: "noreferrer noopener", target: "_blank" }
6473
- })
6761
+ }),
6762
+ // Always registered, so mentions already in the content round-trip with
6763
+ // or without the picker.
6764
+ UserMentionNode,
6765
+ ...mentionSuggestion ? [MentionSuggestion.configure(mentionSuggestion)] : []
6474
6766
  ];
6475
6767
  }
6476
6768
  function RichTextWysiwygEditor({ block, onChange }) {
@@ -6497,8 +6789,25 @@ function RichTextWysiwygEditor({ block, onChange }) {
6497
6789
  },
6498
6790
  [onChange]
6499
6791
  );
6792
+ const mentions = useMentions();
6793
+ const mentionsEnabled = mentions !== null;
6794
+ const mentionsRef = useRef(mentions);
6795
+ useEffect(() => {
6796
+ mentionsRef.current = mentions;
6797
+ }, [mentions]);
6798
+ const [mentionPicker, setMentionPicker] = useState(null);
6799
+ const mentionKeyHandlerRef = useRef(() => false);
6500
6800
  const editor = useEditor({
6501
- extensions: buildRichTextExtensions(),
6801
+ extensions: buildRichTextExtensions({
6802
+ isEnabled: () => mentionsRef.current !== null,
6803
+ search: async (query, signal) => await mentionsRef.current?.searchUsers(query, { signal }) ?? [],
6804
+ render: () => ({
6805
+ onStart: (props) => setMentionPicker((prev) => nextMentionPickerState(props, prev)),
6806
+ onUpdate: (props) => setMentionPicker((prev) => nextMentionPickerState(props, prev)),
6807
+ onExit: () => setMentionPicker(null),
6808
+ onKeyDown: ({ event }) => mentionKeyHandlerRef.current(event)
6809
+ })
6810
+ }),
6502
6811
  enableInputRules: RICH_TEXT_INPUT_RULE_ALLOWLIST,
6503
6812
  content: richTextToProseMirror(block, resolveEmoji),
6504
6813
  editorProps: {
@@ -6508,10 +6817,16 @@ function RichTextWysiwygEditor({ block, onChange }) {
6508
6817
  },
6509
6818
  onUpdate: ({ editor: ed }) => handleUpdate(ed)
6510
6819
  });
6820
+ const seenBlockRef = useRef(block);
6511
6821
  useEffect(() => {
6512
6822
  if (!editor) {
6513
6823
  return;
6514
6824
  }
6825
+ const blockChanged = seenBlockRef.current !== block;
6826
+ seenBlockRef.current = block;
6827
+ if (!blockChanged && lastEmittedRef.current) {
6828
+ return;
6829
+ }
6515
6830
  if (lastEmittedRef.current && JSON.stringify(lastEmittedRef.current) === JSON.stringify(block)) {
6516
6831
  return;
6517
6832
  }
@@ -6523,11 +6838,26 @@ function RichTextWysiwygEditor({ block, onChange }) {
6523
6838
  return /* @__PURE__ */ jsx("div", { className: "min-h-[120px] rounded-md border border-input bg-background" });
6524
6839
  }
6525
6840
  return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", children: [
6526
- /* @__PURE__ */ jsx(Toolbar, { editor }),
6527
- /* @__PURE__ */ jsx(EditorContent, { editor })
6841
+ /* @__PURE__ */ jsx(
6842
+ Toolbar,
6843
+ {
6844
+ editor,
6845
+ mention: mentionsEnabled ? { active: mentionPicker !== null, insert: () => insertMentionTrigger(editor) } : null
6846
+ }
6847
+ ),
6848
+ /* @__PURE__ */ jsx(EditorContent, { editor }),
6849
+ mentionPicker && /* @__PURE__ */ jsx(
6850
+ MentionPicker,
6851
+ {
6852
+ editor,
6853
+ state: mentionPicker,
6854
+ keyHandlerRef: mentionKeyHandlerRef,
6855
+ onDismiss: () => dismissMentionSuggestion(editor)
6856
+ }
6857
+ )
6528
6858
  ] });
6529
6859
  }
6530
- function Toolbar({ editor }) {
6860
+ function Toolbar({ editor, mention }) {
6531
6861
  return /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-0.5 rounded-md border bg-muted/30 p-1", children: [
6532
6862
  /* @__PURE__ */ jsx(
6533
6863
  ToolbarButton,
@@ -6586,6 +6916,7 @@ function Toolbar({ editor }) {
6586
6916
  }).run()
6587
6917
  }
6588
6918
  ),
6919
+ mention && /* @__PURE__ */ jsx(ToolbarButton, { label: "Mention", active: mention.active, onClick: mention.insert, Icon: AtSign }),
6589
6920
  /* @__PURE__ */ jsx(Divider, {}),
6590
6921
  /* @__PURE__ */ jsx(
6591
6922
  ToolbarButton,
@@ -10922,6 +11253,7 @@ function BlockKitchen(props) {
10922
11253
  onValidationChange,
10923
11254
  previewHooks,
10924
11255
  customEmojis,
11256
+ mentions,
10925
11257
  loadChannels,
10926
11258
  loadSendAsUserStatus,
10927
11259
  onSend,
@@ -11282,7 +11614,7 @@ function BlockKitchen(props) {
11282
11614
  }
11283
11615
  )
11284
11616
  ] }),
11285
- /* @__PURE__ */ jsx(
11617
+ /* @__PURE__ */ jsx(MentionsProvider, { mentions, children: /* @__PURE__ */ jsx(
11286
11618
  Surface,
11287
11619
  {
11288
11620
  blocks,
@@ -11302,7 +11634,7 @@ function BlockKitchen(props) {
11302
11634
  isPaletteDrag: activePaletteVariant !== null,
11303
11635
  onOpenPalette: () => setPaletteOpen(true)
11304
11636
  }
11305
- )
11637
+ ) })
11306
11638
  ] })
11307
11639
  ]
11308
11640
  }