@tightknitai/block-kitchen 0.10.20 → 0.10.21

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/README.md CHANGED
@@ -139,7 +139,7 @@ Given no height to work with — a plain `<div>` in ordinary document flow — i
139
139
  | `confirmUpdateLabel` | `string` | no | Label for the update dialog's final confirm button. Defaults to `'Update message'` (shows `'Updating…'` while in flight). |
140
140
  | `previewHooks` | `PreviewHooks` | no | Hooks forwarded to `slack-blocks-to-jsx`'s `<Message>` for resolving user / channel / emoji directives. |
141
141
  | `customEmojis` | `CustomEmoji[]` | no | Workspace custom emoji (`{ name, url, alias }`) the preview resolves. Entries with a `url` render `:name:` as the workspace image; alias entries (`url: null`) fall back to their target emoji. Render-only — never serialized into the emitted Block Kit JSON. A caller-supplied `previewHooks.emoji` takes precedence. |
142
- | `mentions` | `{ searchUsers, getUserName? }` | no | Opt-in `@` mentions in the rich-text editor: typing `@` or the toolbar's Mention button opens a picker backed by `searchUsers(query, { signal })`, and a pick inserts a rich text `user` element. `getUserName(userId)` labels mentions in the editor. See [Mentions](#mentions-opt-in). |
142
+ | `mentions` | `{ searchUsers, getUserName?, searchChannels?, getChannelName? }` | no | Opt-in rich-text mentions. `@` and the Mention button search users; when `searchChannels` is provided, `#` and the Channel button search channels. Picks serialize as Slack `user` / `channel` elements. See [Mentions](#mentions-opt-in). |
143
143
  | `palette` | `PaletteSection[]` | no | The left-hand palette of draggable variants. Defaults to `defaultPalette`. Spread it to filter, reorder, or add your own pre-configured variants — see [Customizing the palette](#customizing-the-palette). |
144
144
  | `disabledBlockTypes` | `SupportedBlockType[]` | no | Block types to hide from the palette without rebuilding it. Filters at the variant level — a section keeps any variants whose block types aren't disabled; sections that end up empty are dropped. Convenient when you want the default palette minus a few types (e.g. `['image', 'table']` for a text-only builder). |
145
145
  | `paletteMode` | `'advanced' \| 'simple'` | no | How much of the palette the user meets first. `'advanced'` (default) renders the full sectioned palette with its search input. `'simple'` opens on a flat list of the variants flagged `basic` — in the built-in palette: **Header**, **Divider**, **Rich Text Section** and **Image** — with no search and an "Advanced" link at the top that swaps in the full palette. Nothing is removed; it's a smaller first screen. See [Simple vs advanced palette](#simple-vs-advanced-palette). |
@@ -608,8 +608,9 @@ import type {
608
608
  BlockKitchenSendProps, // the send trio + `onUpdate` + `renderSendExtras`
609
609
  LoadingConfig, // load an existing message (works in both modes)
610
610
  LoadedMessage, // the loaded target: channel + ts + verdict
611
- MentionsConfig, // opt-in `@` mentions: host search + name lookup
611
+ MentionsConfig, // opt-in `@` / `#` mentions: host search + name lookup
612
612
  MentionUser, // one person the `@` picker offers
613
+ MentionChannel, // one channel the `#` picker offers
613
614
  BlockKitchenComposeOnlyProps, // the trio explicitly absent + `primaryAction`
614
615
  PaletteSection,
615
616
  PaletteVariant,
@@ -642,6 +643,8 @@ Pass [`customEmojis`](#props) to add your workspace's custom emoji: image entrie
642
643
 
643
644
  Pass `mentions` to let people `@`-mention someone in the rich-text WYSIWYG editor. Typing `@` (after a space or at the start of a line, so `name@domain` is left alone) or clicking the toolbar's **Mention** button opens a picker backed by your search. Picking someone inserts a rich text `user` element, `{ type: "user", user_id }`. The picker's name and avatar are never serialized.
644
645
 
646
+ Add `searchChannels` to enable the matching `#` trigger and **Channel** toolbar button. Picking a channel inserts `{ type: "channel", channel_id }`; its display name is never serialized.
647
+
645
648
  ```tsx
646
649
  <BlockKitchen
647
650
  mentions={{
@@ -654,13 +657,19 @@ Pass `mentions` to let people `@`-mention someone in the rich-text WYSIWYG edito
654
657
  },
655
658
  // Optional: labels mentions in the editor by id, including ones that came
656
659
  // with the draft or a loaded message. Return undefined when unknown.
657
- getUserName: (userId) => names.get(userId)
660
+ getUserName: (userId) => userNames.get(userId),
661
+ searchChannels: async (query, { signal }) => {
662
+ const res = await fetch(`/api/channels?q=${encodeURIComponent(query)}`, { signal });
663
+ const channels = await res.json();
664
+ return channels.map((channel) => ({ id: channel.id, name: channel.name }));
665
+ },
666
+ getChannelName: (channelId) => channelNames.get(channelId)
658
667
  }}
659
668
  {...rest}
660
669
  />
661
670
  ```
662
671
 
663
- Mentions already in a draft round-trip through the WYSIWYG with or without `mentions`; without `getUserName` they show the picked name or the raw id. The builder never looks people up itself. To show names in the Slack preview too, resolve the ids in [`previewHooks.user`](#props).
672
+ User and channel mentions already in a draft round-trip through the WYSIWYG with or without picker configuration. Labels resolve through `getUserName` / `getChannelName`, then the name captured when picked, then the raw id. The builder never looks users or channels up itself. To show names in the Slack preview too, resolve the ids in [`previewHooks`](#props).
664
673
 
665
674
  ## Validation
666
675
 
@@ -1,5 +1,5 @@
1
- import { S as SupportedBlock } from './palette-4BJw_nd7.cjs';
2
- export { a7 as SupportedBlockType } from './palette-4BJw_nd7.cjs';
1
+ import { S as SupportedBlock } from './palette-CXXqlwIH.cjs';
2
+ export { a8 as SupportedBlockType } from './palette-CXXqlwIH.cjs';
3
3
  import 'react';
4
4
  import 'slack-web-api-client';
5
5
 
package/dist/helpers.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { S as SupportedBlock } from './palette-4BJw_nd7.js';
2
- export { a7 as SupportedBlockType } from './palette-4BJw_nd7.js';
1
+ import { S as SupportedBlock } from './palette-CXXqlwIH.js';
2
+ export { a8 as SupportedBlockType } from './palette-CXXqlwIH.js';
3
3
  import 'react';
4
4
  import 'slack-web-api-client';
5
5
 
package/dist/index.cjs CHANGED
@@ -22,6 +22,7 @@ var RadioGroupPrimitive = require('@radix-ui/react-radio-group');
22
22
  var Link = require('@tiptap/extension-link');
23
23
  var react = require('@tiptap/react');
24
24
  var StarterKit = require('@tiptap/starter-kit');
25
+ var state = require('@tiptap/pm/state');
25
26
  var suggestion = require('@tiptap/suggestion');
26
27
 
27
28
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
@@ -5274,15 +5275,16 @@ function makeEmojiImportResolver(customByName, index = getEmojiIndex()) {
5274
5275
 
5275
5276
  // src/lib/rich-text-tiptap.ts
5276
5277
  var USER_MENTION_NODE = "userMention";
5278
+ var CHANNEL_MENTION_NODE = "channelMention";
5277
5279
  var SUPPORTED_BLOCK_KINDS = /* @__PURE__ */ new Set([
5278
5280
  "rich_text_section",
5279
5281
  "rich_text_list",
5280
5282
  "rich_text_quote",
5281
5283
  "rich_text_preformatted"
5282
5284
  ]);
5283
- var SUPPORTED_INLINE_KINDS = /* @__PURE__ */ new Set(["text", "link", "emoji", "user"]);
5285
+ var SUPPORTED_INLINE_KINDS = /* @__PURE__ */ new Set(["text", "link", "emoji", "user", "channel"]);
5284
5286
  var SUPPORTED_TEXT_STYLE_KEYS = /* @__PURE__ */ new Set(["bold", "italic", "strike", "underline", "code"]);
5285
- var SUPPORTED_USER_STYLE_KEYS = /* @__PURE__ */ new Set(["bold", "italic", "strike", "underline"]);
5287
+ var SUPPORTED_MENTION_STYLE_KEYS = /* @__PURE__ */ new Set(["bold", "italic", "strike", "underline"]);
5286
5288
  var MAX_LIST_INDENT = 8;
5287
5289
  function detectLossy(block) {
5288
5290
  const out = [];
@@ -5337,10 +5339,13 @@ function scanInlines(inlines, where, out) {
5337
5339
  }
5338
5340
  }
5339
5341
  }
5340
- if (inline.type === "user") {
5342
+ if (inline.type === "user" || inline.type === "channel") {
5343
+ if (inline.type === "channel" && !inline.channel_id) {
5344
+ out.push({ label: "Channel without id", where: sub });
5345
+ }
5341
5346
  for (const k of Object.keys(inline.style ?? {})) {
5342
- if (!SUPPORTED_USER_STYLE_KEYS.has(k)) {
5343
- out.push({ label: `Mention style: ${k}`, where: sub });
5347
+ if (!SUPPORTED_MENTION_STYLE_KEYS.has(k)) {
5348
+ out.push({ label: `${inline.type === "channel" ? "Channel" : "Mention"} style: ${k}`, where: sub });
5344
5349
  }
5345
5350
  }
5346
5351
  }
@@ -5466,6 +5471,20 @@ function inlinesToPM(inlines, resolveEmoji) {
5466
5471
  node.marks = marks;
5467
5472
  }
5468
5473
  out.push(node);
5474
+ } else if (inline.type === "channel") {
5475
+ const channel = inline;
5476
+ if (!channel.channel_id) {
5477
+ continue;
5478
+ }
5479
+ const node = {
5480
+ type: CHANNEL_MENTION_NODE,
5481
+ attrs: { channelId: channel.channel_id, name: null }
5482
+ };
5483
+ const marks = styleToMarks(channel.style);
5484
+ if (marks.length > 0) {
5485
+ node.marks = marks;
5486
+ }
5487
+ out.push(node);
5469
5488
  } else if (inline.type === "link") {
5470
5489
  const link = inline;
5471
5490
  const text = link.text || link.url || "";
@@ -5605,13 +5624,26 @@ function proseMirrorInlinesToRichTextElements(nodes) {
5605
5624
  continue;
5606
5625
  }
5607
5626
  const user = { type: "user", user_id: userId };
5608
- const style2 = userStyle(node.marks ?? []);
5627
+ const style2 = mentionStyle(node.marks ?? []);
5609
5628
  if (Object.keys(style2).length > 0) {
5610
5629
  user.style = style2;
5611
5630
  }
5612
5631
  out.push(user);
5613
5632
  continue;
5614
5633
  }
5634
+ if (node.type === CHANNEL_MENTION_NODE) {
5635
+ const channelId = String(node.attrs?.channelId ?? "");
5636
+ if (!channelId) {
5637
+ continue;
5638
+ }
5639
+ const channel = { type: "channel", channel_id: channelId };
5640
+ const style2 = mentionStyle(node.marks ?? []);
5641
+ if (Object.keys(style2).length > 0) {
5642
+ channel.style = style2;
5643
+ }
5644
+ out.push(channel);
5645
+ continue;
5646
+ }
5615
5647
  if (node.type === "hardBreak") {
5616
5648
  out.push({ type: "text", text: "\n" });
5617
5649
  continue;
@@ -5666,10 +5698,10 @@ function marksToStyle(marks) {
5666
5698
  }
5667
5699
  return style;
5668
5700
  }
5669
- function userStyle(marks) {
5701
+ function mentionStyle(marks) {
5670
5702
  const style = {};
5671
5703
  for (const [key, value] of Object.entries(marksToStyle(marks))) {
5672
- if (SUPPORTED_USER_STYLE_KEYS.has(key)) {
5704
+ if (SUPPORTED_MENTION_STYLE_KEYS.has(key)) {
5673
5705
  style[key] = value;
5674
5706
  }
5675
5707
  }
@@ -5698,50 +5730,52 @@ function sameStyle(a, b) {
5698
5730
  }
5699
5731
  return ak.every((k, i) => k === bk[i]);
5700
5732
  }
5701
- function EmojiNodeView({ node }) {
5702
- const { name, src, unicode } = node.attrs;
5703
- const label = `:${name}:`;
5704
- return /* @__PURE__ */ jsxRuntime.jsx(react.NodeViewWrapper, { as: "span", className: "bk-emoji-node", "data-emoji": name, title: label, children: src ? /* @__PURE__ */ jsxRuntime.jsx(
5705
- "img",
5706
- {
5707
- src,
5708
- alt: label,
5709
- style: {
5710
- display: "inline-block",
5711
- width: "1.25em",
5712
- height: "1.25em",
5713
- verticalAlign: "-0.25em",
5714
- objectFit: "contain"
5715
- }
5716
- }
5717
- ) : /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-label": label, role: "img", children: unicode ? codepointsToGlyph(unicode) : label }) });
5733
+ function channelMentionLabel(attrs, getChannelName) {
5734
+ return getChannelName?.(attrs.channelId) ?? attrs.name ?? attrs.channelId;
5718
5735
  }
5719
- var EmojiNode = react.Node.create({
5720
- name: "emoji",
5736
+ function ChannelMentionNodeView({ node }) {
5737
+ const mentions = useMentions();
5738
+ const attrs = node.attrs;
5739
+ return /* @__PURE__ */ jsxRuntime.jsxs(react.NodeViewWrapper, { as: "span", className: "bk-channel-mention", "data-channel-mention": attrs.channelId, children: [
5740
+ "#",
5741
+ channelMentionLabel(attrs, mentions?.getChannelName)
5742
+ ] });
5743
+ }
5744
+ var ChannelMentionNode = react.Node.create({
5745
+ name: CHANNEL_MENTION_NODE,
5721
5746
  group: "inline",
5722
5747
  inline: true,
5723
5748
  atom: true,
5724
5749
  selectable: true,
5725
5750
  addAttributes() {
5726
5751
  return {
5727
- name: { default: "" },
5728
- src: { default: null },
5729
- unicode: { default: null },
5730
- skinTone: { default: null }
5752
+ channelId: {
5753
+ default: "",
5754
+ parseHTML: (element) => element.getAttribute("data-channel-mention") ?? "",
5755
+ renderHTML: (attributes) => ({ "data-channel-mention": attributes.channelId })
5756
+ },
5757
+ name: {
5758
+ default: null,
5759
+ parseHTML: (element) => element.getAttribute("data-channel-name"),
5760
+ renderHTML: (attributes) => attributes.name ? { "data-channel-name": attributes.name } : {}
5761
+ }
5731
5762
  };
5732
5763
  },
5733
5764
  parseHTML() {
5734
- return [{ tag: "span[data-emoji]" }];
5765
+ return [{ tag: "span[data-channel-mention]" }];
5735
5766
  },
5736
- renderHTML({ HTMLAttributes }) {
5737
- const name = HTMLAttributes.name ?? "";
5738
- return ["span", react.mergeAttributes({ "data-emoji": name }, HTMLAttributes), `:${name}:`];
5767
+ renderHTML({ node, HTMLAttributes }) {
5768
+ return ["span", react.mergeAttributes(HTMLAttributes), `#${channelMentionLabel(node.attrs)}`];
5769
+ },
5770
+ renderText({ node }) {
5771
+ return `#${channelMentionLabel(node.attrs)}`;
5739
5772
  },
5740
5773
  addNodeView() {
5741
- return react.ReactNodeViewRenderer(EmojiNodeView);
5774
+ return react.ReactNodeViewRenderer(ChannelMentionNodeView);
5742
5775
  }
5743
5776
  });
5744
5777
  var MENTION_SEARCH_DEBOUNCE_MS = 200;
5778
+ var USER_MENTION_SUGGESTION_PLUGIN_KEY = new state.PluginKey("userMentionSuggestion");
5745
5779
  function insertUserMention(editor, range, user) {
5746
5780
  const to = editor.state.doc.textBetween(range.to, Math.min(range.to + 1, editor.state.doc.content.size)) === " " ? range.to + 1 : range.to;
5747
5781
  editor.chain().focus().insertContentAt({ from: range.from, to }, [
@@ -5750,13 +5784,16 @@ function insertUserMention(editor, range, user) {
5750
5784
  ]).run();
5751
5785
  }
5752
5786
  function insertMentionTrigger(editor) {
5787
+ insertSuggestionTrigger(editor, "@");
5788
+ }
5789
+ function insertSuggestionTrigger(editor, char) {
5753
5790
  const { from } = editor.state.selection;
5754
5791
  const before = from > 1 ? editor.state.doc.textBetween(from - 1, from, void 0, "\uFFFC") : "";
5755
5792
  const needsSpace = before !== "" && !/\s/.test(before);
5756
- editor.chain().focus().insertContent(needsSpace ? " @" : "@").run();
5793
+ editor.chain().focus().insertContent(needsSpace ? ` ${char}` : char).run();
5757
5794
  }
5758
5795
  function dismissMentionSuggestion(editor) {
5759
- suggestion.exitSuggestion(editor.view);
5796
+ suggestion.exitSuggestion(editor.view, USER_MENTION_SUGGESTION_PLUGIN_KEY);
5760
5797
  }
5761
5798
  var MentionSuggestion = react.Extension.create({
5762
5799
  name: "userMentionSuggestion",
@@ -5772,6 +5809,7 @@ var MentionSuggestion = react.Extension.create({
5772
5809
  return [
5773
5810
  suggestion.Suggestion({
5774
5811
  editor: this.editor,
5812
+ pluginKey: USER_MENTION_SUGGESTION_PLUGIN_KEY,
5775
5813
  char: "@",
5776
5814
  debounce: MENTION_SEARCH_DEBOUNCE_MS,
5777
5815
  allow: () => isEnabled(),
@@ -5782,6 +5820,90 @@ var MentionSuggestion = react.Extension.create({
5782
5820
  ];
5783
5821
  }
5784
5822
  });
5823
+
5824
+ // src/lib/tiptap-channel-suggestion.ts
5825
+ var CHANNEL_MENTION_SUGGESTION_PLUGIN_KEY = new state.PluginKey("channelMentionSuggestion");
5826
+ function insertChannelMention(editor, range, channel) {
5827
+ const to = editor.state.doc.textBetween(range.to, Math.min(range.to + 1, editor.state.doc.content.size)) === " " ? range.to + 1 : range.to;
5828
+ editor.chain().focus().insertContentAt({ from: range.from, to }, [
5829
+ { type: CHANNEL_MENTION_NODE, attrs: { channelId: channel.id, name: channel.name } },
5830
+ { type: "text", text: " " }
5831
+ ]).run();
5832
+ }
5833
+ function insertChannelTrigger(editor) {
5834
+ insertSuggestionTrigger(editor, "#");
5835
+ }
5836
+ function dismissChannelSuggestion(editor) {
5837
+ suggestion.exitSuggestion(editor.view, CHANNEL_MENTION_SUGGESTION_PLUGIN_KEY);
5838
+ }
5839
+ var ChannelSuggestion = react.Extension.create({
5840
+ name: "channelMentionSuggestion",
5841
+ addOptions() {
5842
+ return {
5843
+ isEnabled: () => false,
5844
+ search: async () => [],
5845
+ render: () => ({})
5846
+ };
5847
+ },
5848
+ addProseMirrorPlugins() {
5849
+ const { isEnabled, search, render } = this.options;
5850
+ return [
5851
+ suggestion.Suggestion({
5852
+ editor: this.editor,
5853
+ pluginKey: CHANNEL_MENTION_SUGGESTION_PLUGIN_KEY,
5854
+ char: "#",
5855
+ debounce: MENTION_SEARCH_DEBOUNCE_MS,
5856
+ allow: () => isEnabled(),
5857
+ items: ({ query, signal }) => search(query, signal),
5858
+ command: ({ editor, range, props }) => insertChannelMention(editor, range, props),
5859
+ render
5860
+ })
5861
+ ];
5862
+ }
5863
+ });
5864
+ function EmojiNodeView({ node }) {
5865
+ const { name, src, unicode } = node.attrs;
5866
+ const label = `:${name}:`;
5867
+ return /* @__PURE__ */ jsxRuntime.jsx(react.NodeViewWrapper, { as: "span", className: "bk-emoji-node", "data-emoji": name, title: label, children: src ? /* @__PURE__ */ jsxRuntime.jsx(
5868
+ "img",
5869
+ {
5870
+ src,
5871
+ alt: label,
5872
+ style: {
5873
+ display: "inline-block",
5874
+ width: "1.25em",
5875
+ height: "1.25em",
5876
+ verticalAlign: "-0.25em",
5877
+ objectFit: "contain"
5878
+ }
5879
+ }
5880
+ ) : /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-label": label, role: "img", children: unicode ? codepointsToGlyph(unicode) : label }) });
5881
+ }
5882
+ var EmojiNode = react.Node.create({
5883
+ name: "emoji",
5884
+ group: "inline",
5885
+ inline: true,
5886
+ atom: true,
5887
+ selectable: true,
5888
+ addAttributes() {
5889
+ return {
5890
+ name: { default: "" },
5891
+ src: { default: null },
5892
+ unicode: { default: null },
5893
+ skinTone: { default: null }
5894
+ };
5895
+ },
5896
+ parseHTML() {
5897
+ return [{ tag: "span[data-emoji]" }];
5898
+ },
5899
+ renderHTML({ HTMLAttributes }) {
5900
+ const name = HTMLAttributes.name ?? "";
5901
+ return ["span", react.mergeAttributes({ "data-emoji": name }, HTMLAttributes), `:${name}:`];
5902
+ },
5903
+ addNodeView() {
5904
+ return react.ReactNodeViewRenderer(EmojiNodeView);
5905
+ }
5906
+ });
5785
5907
  function userMentionLabel(attrs, getUserName) {
5786
5908
  return getUserName?.(attrs.userId) ?? attrs.name ?? attrs.userId;
5787
5909
  }
@@ -6117,6 +6239,7 @@ function nextMentionPickerState(props, prev) {
6117
6239
  }
6118
6240
  function MentionPicker({
6119
6241
  editor,
6242
+ kind,
6120
6243
  state,
6121
6244
  keyHandlerRef,
6122
6245
  onDismiss
@@ -6129,6 +6252,27 @@ function MentionPicker({
6129
6252
  setActiveIndex(0);
6130
6253
  }
6131
6254
  const listRef = React.useRef(null);
6255
+ const listboxId = React.useId();
6256
+ const activeOptionId = items[activeIndex] ? `${listboxId}-${activeIndex}` : void 0;
6257
+ React.useEffect(() => {
6258
+ const root = editor.view.dom;
6259
+ root.setAttribute("role", "combobox");
6260
+ root.setAttribute("aria-autocomplete", "list");
6261
+ root.setAttribute("aria-expanded", "true");
6262
+ root.setAttribute("aria-controls", listboxId);
6263
+ if (activeOptionId) {
6264
+ root.setAttribute("aria-activedescendant", activeOptionId);
6265
+ } else {
6266
+ root.removeAttribute("aria-activedescendant");
6267
+ }
6268
+ return () => {
6269
+ root.removeAttribute("role");
6270
+ root.removeAttribute("aria-autocomplete");
6271
+ root.removeAttribute("aria-expanded");
6272
+ root.removeAttribute("aria-controls");
6273
+ root.removeAttribute("aria-activedescendant");
6274
+ };
6275
+ }, [activeOptionId, editor, listboxId]);
6132
6276
  React.useEffect(() => {
6133
6277
  keyHandlerRef.current = (event) => {
6134
6278
  if (event.key === "ArrowDown" || event.key === "ArrowUp") {
@@ -6139,11 +6283,11 @@ function MentionPicker({
6139
6283
  return true;
6140
6284
  }
6141
6285
  if (event.key === "Enter" || event.key === "Tab") {
6142
- const user = items[activeIndex];
6143
- if (!user) {
6286
+ const item = items[activeIndex];
6287
+ if (!item) {
6144
6288
  return false;
6145
6289
  }
6146
- command(user);
6290
+ command(item);
6147
6291
  return true;
6148
6292
  }
6149
6293
  return false;
@@ -6190,29 +6334,39 @@ function MentionPicker({
6190
6334
  e.preventDefault();
6191
6335
  }
6192
6336
  },
6193
- children: /* @__PURE__ */ jsxRuntime.jsx("div", { ref: listRef, role: "listbox", "aria-label": "People to mention", children: items.length === 0 ? /* @__PURE__ */ jsxRuntime.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) => (
6194
- // biome-ignore lint/a11y/useFocusableInteractive: focus stays in the editor, which drives this list through the suggestion plugin's key handling.
6195
- // biome-ignore lint/a11y/useKeyWithClickEvents: the keyboard path is the editor's, forwarded via keyHandlerRef.
6196
- /* @__PURE__ */ jsxRuntime.jsxs(
6197
- "div",
6198
- {
6199
- role: "option",
6200
- "aria-selected": index === activeIndex,
6201
- onPointerDown: (e) => e.preventDefault(),
6202
- onClick: () => command(user),
6203
- onMouseMove: () => setActiveIndex(index),
6204
- className: cn(
6205
- "flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm text-foreground",
6206
- index === activeIndex && "bg-accent"
6207
- ),
6208
- children: [
6209
- /* @__PURE__ */ jsxRuntime.jsx(MentionAvatar, { user }),
6210
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: user.name })
6211
- ]
6212
- },
6213
- user.id
6214
- )
6215
- )) })
6337
+ children: /* @__PURE__ */ jsxRuntime.jsx(
6338
+ "div",
6339
+ {
6340
+ ref: listRef,
6341
+ id: listboxId,
6342
+ role: "listbox",
6343
+ "aria-label": kind === "channel" ? "Channels to mention" : "People to mention",
6344
+ children: items.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "px-2 py-3 text-center text-xs text-muted-foreground", children: loading ? "Searching\u2026" : `No ${kind === "channel" ? "channels" : "people"} match that search.` }) : items.map((item, index) => (
6345
+ // biome-ignore lint/a11y/useFocusableInteractive: focus stays in the editor, which drives this list through the suggestion plugin's key handling.
6346
+ // biome-ignore lint/a11y/useKeyWithClickEvents: the keyboard path is the editor's, forwarded via keyHandlerRef.
6347
+ /* @__PURE__ */ jsxRuntime.jsxs(
6348
+ "div",
6349
+ {
6350
+ id: `${listboxId}-${index}`,
6351
+ role: "option",
6352
+ "aria-selected": index === activeIndex,
6353
+ onPointerDown: (e) => e.preventDefault(),
6354
+ onClick: () => command(item),
6355
+ onMouseMove: () => setActiveIndex(index),
6356
+ className: cn(
6357
+ "flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm text-foreground",
6358
+ index === activeIndex && "bg-accent"
6359
+ ),
6360
+ children: [
6361
+ kind === "channel" ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex h-5 w-5 shrink-0 items-center justify-center rounded bg-muted text-muted-foreground", children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Hash, { className: "h-3.5 w-3.5" }) }) : /* @__PURE__ */ jsxRuntime.jsx(MentionAvatar, { user: item }),
6362
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: kind === "channel" ? `#${item.name}` : item.name })
6363
+ ]
6364
+ },
6365
+ item.id
6366
+ )
6367
+ ))
6368
+ }
6369
+ )
6216
6370
  }
6217
6371
  )
6218
6372
  ]
@@ -6809,7 +6963,7 @@ function LossyBanner({ reasons }) {
6809
6963
  ] });
6810
6964
  }
6811
6965
  var RICH_TEXT_INPUT_RULE_ALLOWLIST = ["bold", "italic", "strike", "code"];
6812
- function buildRichTextExtensions(mentionSuggestion) {
6966
+ function buildRichTextExtensions(mentionSuggestion, channelSuggestion) {
6813
6967
  return [
6814
6968
  StarterKit__default.default.configure({
6815
6969
  heading: false,
@@ -6843,10 +6997,11 @@ function buildRichTextExtensions(mentionSuggestion) {
6843
6997
  shouldAutoLink: hasExplicitSafeScheme,
6844
6998
  HTMLAttributes: { rel: "noreferrer noopener", target: "_blank" }
6845
6999
  }),
6846
- // Always registered, so mentions already in the content round-trip with
6847
- // or without the picker.
7000
+ // Always registered, so mentions already in the content round-trip with or without pickers.
6848
7001
  UserMentionNode,
6849
- ...mentionSuggestion ? [MentionSuggestion.configure(mentionSuggestion)] : []
7002
+ ChannelMentionNode,
7003
+ ...mentionSuggestion ? [MentionSuggestion.configure(mentionSuggestion)] : [],
7004
+ ...channelSuggestion ? [ChannelSuggestion.configure(channelSuggestion)] : []
6850
7005
  ];
6851
7006
  }
6852
7007
  function RichTextWysiwygEditor({ block, onChange }) {
@@ -6874,24 +7029,39 @@ function RichTextWysiwygEditor({ block, onChange }) {
6874
7029
  [onChange]
6875
7030
  );
6876
7031
  const mentions = useMentions();
6877
- const mentionsEnabled = mentions !== null;
7032
+ const userMentionsEnabled = mentions !== null;
7033
+ const channelMentionsEnabled = mentions?.searchChannels !== void 0;
6878
7034
  const mentionsRef = React.useRef(mentions);
6879
7035
  React.useEffect(() => {
6880
7036
  mentionsRef.current = mentions;
6881
7037
  }, [mentions]);
6882
7038
  const [mentionPicker, setMentionPicker] = React.useState(null);
6883
7039
  const mentionKeyHandlerRef = React.useRef(() => false);
7040
+ const [channelPicker, setChannelPicker] = React.useState(null);
7041
+ const channelKeyHandlerRef = React.useRef(() => false);
6884
7042
  const editor = react.useEditor({
6885
- extensions: buildRichTextExtensions({
6886
- isEnabled: () => mentionsRef.current !== null,
6887
- search: async (query, signal) => await mentionsRef.current?.searchUsers(query, { signal }) ?? [],
6888
- render: () => ({
6889
- onStart: (props) => setMentionPicker((prev) => nextMentionPickerState(props, prev)),
6890
- onUpdate: (props) => setMentionPicker((prev) => nextMentionPickerState(props, prev)),
6891
- onExit: () => setMentionPicker(null),
6892
- onKeyDown: ({ event }) => mentionKeyHandlerRef.current(event)
6893
- })
6894
- }),
7043
+ extensions: buildRichTextExtensions(
7044
+ {
7045
+ isEnabled: () => mentionsRef.current !== null,
7046
+ search: async (query, signal) => await mentionsRef.current?.searchUsers(query, { signal }) ?? [],
7047
+ render: () => ({
7048
+ onStart: (props) => setMentionPicker((prev) => nextMentionPickerState(props, prev)),
7049
+ onUpdate: (props) => setMentionPicker((prev) => nextMentionPickerState(props, prev)),
7050
+ onExit: () => setMentionPicker(null),
7051
+ onKeyDown: ({ event }) => mentionKeyHandlerRef.current(event)
7052
+ })
7053
+ },
7054
+ {
7055
+ isEnabled: () => mentionsRef.current?.searchChannels !== void 0,
7056
+ search: async (query, signal) => await mentionsRef.current?.searchChannels?.(query, { signal }) ?? [],
7057
+ render: () => ({
7058
+ onStart: (props) => setChannelPicker((prev) => nextMentionPickerState(props, prev)),
7059
+ onUpdate: (props) => setChannelPicker((prev) => nextMentionPickerState(props, prev)),
7060
+ onExit: () => setChannelPicker(null),
7061
+ onKeyDown: ({ event }) => channelKeyHandlerRef.current(event)
7062
+ })
7063
+ }
7064
+ ),
6895
7065
  enableInputRules: RICH_TEXT_INPUT_RULE_ALLOWLIST,
6896
7066
  content: richTextToProseMirror(block, resolveEmoji),
6897
7067
  editorProps: {
@@ -6926,7 +7096,8 @@ function RichTextWysiwygEditor({ block, onChange }) {
6926
7096
  Toolbar,
6927
7097
  {
6928
7098
  editor,
6929
- mention: mentionsEnabled ? { active: mentionPicker !== null, insert: () => insertMentionTrigger(editor) } : null
7099
+ mention: userMentionsEnabled ? { active: mentionPicker !== null, insert: () => insertMentionTrigger(editor) } : null,
7100
+ channel: channelMentionsEnabled ? { active: channelPicker !== null, insert: () => insertChannelTrigger(editor) } : null
6930
7101
  }
6931
7102
  ),
6932
7103
  /* @__PURE__ */ jsxRuntime.jsx(react.EditorContent, { editor }),
@@ -6934,14 +7105,29 @@ function RichTextWysiwygEditor({ block, onChange }) {
6934
7105
  MentionPicker,
6935
7106
  {
6936
7107
  editor,
7108
+ kind: "user",
6937
7109
  state: mentionPicker,
6938
7110
  keyHandlerRef: mentionKeyHandlerRef,
6939
7111
  onDismiss: () => dismissMentionSuggestion(editor)
6940
7112
  }
7113
+ ),
7114
+ channelPicker && /* @__PURE__ */ jsxRuntime.jsx(
7115
+ MentionPicker,
7116
+ {
7117
+ editor,
7118
+ kind: "channel",
7119
+ state: channelPicker,
7120
+ keyHandlerRef: channelKeyHandlerRef,
7121
+ onDismiss: () => dismissChannelSuggestion(editor)
7122
+ }
6941
7123
  )
6942
7124
  ] });
6943
7125
  }
6944
- function Toolbar({ editor, mention }) {
7126
+ function Toolbar({
7127
+ editor,
7128
+ mention,
7129
+ channel
7130
+ }) {
6945
7131
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-center gap-0.5 rounded-md border bg-muted/30 p-1", children: [
6946
7132
  /* @__PURE__ */ jsxRuntime.jsx(
6947
7133
  ToolbarButton,
@@ -7001,6 +7187,7 @@ function Toolbar({ editor, mention }) {
7001
7187
  }
7002
7188
  ),
7003
7189
  mention && /* @__PURE__ */ jsxRuntime.jsx(ToolbarButton, { label: "Mention", active: mention.active, onClick: mention.insert, Icon: lucideReact.AtSign }),
7190
+ channel && /* @__PURE__ */ jsxRuntime.jsx(ToolbarButton, { label: "Channel", active: channel.active, onClick: channel.insert, Icon: lucideReact.Hash }),
7004
7191
  /* @__PURE__ */ jsxRuntime.jsx(Divider, {}),
7005
7192
  /* @__PURE__ */ jsxRuntime.jsx(
7006
7193
  ToolbarButton,