@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 +13 -4
- package/dist/helpers.d.cts +2 -2
- package/dist/helpers.d.ts +2 -2
- package/dist/index.cjs +268 -81
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +269 -82
- package/dist/index.js.map +1 -1
- package/dist/{palette-4BJw_nd7.d.cts → palette-CXXqlwIH.d.cts} +29 -9
- package/dist/{palette-4BJw_nd7.d.ts → palette-CXXqlwIH.d.ts} +29 -9
- package/dist/palette.d.cts +1 -1
- package/dist/palette.d.ts +1 -1
- package/dist/styles.css +1 -1
- package/package.json +2 -1
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
|
|
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) =>
|
|
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
|
-
|
|
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
|
|
package/dist/helpers.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { S as SupportedBlock } from './palette-
|
|
2
|
-
export {
|
|
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-
|
|
2
|
-
export {
|
|
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
|
|
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 (!
|
|
5343
|
-
out.push({ label:
|
|
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 =
|
|
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
|
|
5701
|
+
function mentionStyle(marks) {
|
|
5670
5702
|
const style = {};
|
|
5671
5703
|
for (const [key, value] of Object.entries(marksToStyle(marks))) {
|
|
5672
|
-
if (
|
|
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
|
|
5702
|
-
|
|
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
|
-
|
|
5720
|
-
|
|
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
|
-
|
|
5728
|
-
|
|
5729
|
-
|
|
5730
|
-
|
|
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-
|
|
5765
|
+
return [{ tag: "span[data-channel-mention]" }];
|
|
5735
5766
|
},
|
|
5736
|
-
renderHTML({ HTMLAttributes }) {
|
|
5737
|
-
|
|
5738
|
-
|
|
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(
|
|
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 ?
|
|
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
|
|
6143
|
-
if (!
|
|
6286
|
+
const item = items[activeIndex];
|
|
6287
|
+
if (!item) {
|
|
6144
6288
|
return false;
|
|
6145
6289
|
}
|
|
6146
|
-
command(
|
|
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(
|
|
6194
|
-
|
|
6195
|
-
|
|
6196
|
-
|
|
6197
|
-
|
|
6198
|
-
|
|
6199
|
-
|
|
6200
|
-
|
|
6201
|
-
|
|
6202
|
-
|
|
6203
|
-
|
|
6204
|
-
|
|
6205
|
-
|
|
6206
|
-
|
|
6207
|
-
|
|
6208
|
-
|
|
6209
|
-
|
|
6210
|
-
|
|
6211
|
-
|
|
6212
|
-
|
|
6213
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
6887
|
-
|
|
6888
|
-
|
|
6889
|
-
|
|
6890
|
-
|
|
6891
|
-
|
|
6892
|
-
|
|
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:
|
|
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({
|
|
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,
|