@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/README.md +27 -0
- package/dist/helpers.d.cts +2 -2
- package/dist/helpers.d.ts +2 -2
- package/dist/index.cjs +342 -10
- 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 +345 -13
- package/dist/index.js.map +1 -1
- package/dist/{palette-Cxh1yNzW.d.cts → palette-DNUiqoQK.d.cts} +46 -1
- package/dist/{palette-Cxh1yNzW.d.ts → palette-DNUiqoQK.d.ts} +46 -1
- package/dist/palette.d.cts +1 -1
- package/dist/palette.d.ts +1 -1
- package/dist/styles.css +1 -1
- package/package.json +14 -3
package/README.md
CHANGED
|
@@ -139,6 +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
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). |
|
|
143
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). |
|
|
144
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). |
|
|
@@ -607,6 +608,8 @@ import type {
|
|
|
607
608
|
BlockKitchenSendProps, // the send trio + `onUpdate` + `renderSendExtras`
|
|
608
609
|
LoadingConfig, // load an existing message (works in both modes)
|
|
609
610
|
LoadedMessage, // the loaded target: channel + ts + verdict
|
|
611
|
+
MentionsConfig, // opt-in `@` mentions: host search + name lookup
|
|
612
|
+
MentionUser, // one person the `@` picker offers
|
|
610
613
|
BlockKitchenComposeOnlyProps, // the trio explicitly absent + `primaryAction`
|
|
611
614
|
PaletteSection,
|
|
612
615
|
PaletteVariant,
|
|
@@ -635,6 +638,30 @@ A searchable emoji picker (search, categories, skin tone, recents) is built into
|
|
|
635
638
|
|
|
636
639
|
Pass [`customEmojis`](#props) to add your workspace's custom emoji: image entries appear as the picker's **Custom** category and render in the preview; alias entries fall back to their target. Custom emoji never block Send — Slack accepts unknown emoji names, so they're excluded from validation. The emitted Block Kit JSON is unchanged whether or not `customEmojis` is passed.
|
|
637
640
|
|
|
641
|
+
## Mentions (opt-in)
|
|
642
|
+
|
|
643
|
+
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
|
+
```tsx
|
|
646
|
+
<BlockKitchen
|
|
647
|
+
mentions={{
|
|
648
|
+
// Called with the text after `@` (empty right after it); debounced, and
|
|
649
|
+
// `signal` aborts once a newer query supersedes this one.
|
|
650
|
+
searchUsers: async (query, { signal }) => {
|
|
651
|
+
const res = await fetch(`/api/members?q=${encodeURIComponent(query)}`, { signal });
|
|
652
|
+
const members = await res.json();
|
|
653
|
+
return members.map((m) => ({ id: m.slackUserId, name: m.name, avatarUrl: m.avatar }));
|
|
654
|
+
},
|
|
655
|
+
// Optional: labels mentions in the editor by id, including ones that came
|
|
656
|
+
// with the draft or a loaded message. Return undefined when unknown.
|
|
657
|
+
getUserName: (userId) => names.get(userId)
|
|
658
|
+
}}
|
|
659
|
+
{...rest}
|
|
660
|
+
/>
|
|
661
|
+
```
|
|
662
|
+
|
|
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).
|
|
664
|
+
|
|
638
665
|
## Validation
|
|
639
666
|
|
|
640
667
|
Defense-in-depth: blocks are validated against [slack-block-kit-validator](https://github.com/TightknitAI/slack-block-kit-validator) before send. Issues are surfaced in the issues sheet with line numbers — users can fix them inline before posting.
|
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-DNUiqoQK.cjs';
|
|
2
|
+
export { a3 as SupportedBlockType } from './palette-DNUiqoQK.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-DNUiqoQK.js';
|
|
2
|
+
export { a3 as SupportedBlockType } from './palette-DNUiqoQK.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 suggestion = require('@tiptap/suggestion');
|
|
25
26
|
|
|
26
27
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
27
28
|
|
|
@@ -1863,6 +1864,13 @@ function CustomEmojiProvider({ customEmojis, children }) {
|
|
|
1863
1864
|
function useCustomEmojis() {
|
|
1864
1865
|
return React.useContext(CustomEmojiContext);
|
|
1865
1866
|
}
|
|
1867
|
+
var MentionsContext = React.createContext(null);
|
|
1868
|
+
function MentionsProvider({ mentions, children }) {
|
|
1869
|
+
return /* @__PURE__ */ jsxRuntime.jsx(MentionsContext.Provider, { value: mentions ?? null, children });
|
|
1870
|
+
}
|
|
1871
|
+
function useMentions() {
|
|
1872
|
+
return React.useContext(MentionsContext);
|
|
1873
|
+
}
|
|
1866
1874
|
|
|
1867
1875
|
// src/lib/error-grouping.ts
|
|
1868
1876
|
function toValidatorSurface(surface) {
|
|
@@ -3363,12 +3371,27 @@ function ChannelCombobox({
|
|
|
3363
3371
|
] });
|
|
3364
3372
|
}
|
|
3365
3373
|
var EMBED_SELECTOR = "iframe[src], embed[src], object[data], source[src], video[src], audio[src], track[src]";
|
|
3374
|
+
function useLatestHooks(hooks) {
|
|
3375
|
+
const latest = React.useRef(hooks);
|
|
3376
|
+
latest.current = hooks;
|
|
3377
|
+
const [live] = React.useState(
|
|
3378
|
+
() => new Proxy(
|
|
3379
|
+
{},
|
|
3380
|
+
{
|
|
3381
|
+
get: (_target, key) => typeof key === "string" ? latest.current?.[key] : void 0,
|
|
3382
|
+
has: (_target, key) => typeof key === "string" && latest.current !== void 0 && key in latest.current
|
|
3383
|
+
}
|
|
3384
|
+
)
|
|
3385
|
+
);
|
|
3386
|
+
return live;
|
|
3387
|
+
}
|
|
3366
3388
|
function SlackBlockPreview({
|
|
3367
3389
|
block,
|
|
3368
3390
|
hooks,
|
|
3369
3391
|
theme = "light"
|
|
3370
3392
|
}) {
|
|
3371
3393
|
const rootRef = React.useRef(null);
|
|
3394
|
+
const liveHooks = useLatestHooks(hooks);
|
|
3372
3395
|
const safeBlock = React.useMemo(() => sanitizeBlock(block), [block]);
|
|
3373
3396
|
React.useEffect(() => {
|
|
3374
3397
|
const root = rootRef.current;
|
|
@@ -3423,7 +3446,7 @@ function SlackBlockPreview({
|
|
|
3423
3446
|
withoutWrapper: true,
|
|
3424
3447
|
theme,
|
|
3425
3448
|
blocks: [safeBlock],
|
|
3426
|
-
hooks
|
|
3449
|
+
hooks: liveHooks
|
|
3427
3450
|
}
|
|
3428
3451
|
) })
|
|
3429
3452
|
);
|
|
@@ -5196,14 +5219,16 @@ function makeEmojiImportResolver(customByName, index = getEmojiIndex()) {
|
|
|
5196
5219
|
}
|
|
5197
5220
|
|
|
5198
5221
|
// src/lib/rich-text-tiptap.ts
|
|
5222
|
+
var USER_MENTION_NODE = "userMention";
|
|
5199
5223
|
var SUPPORTED_BLOCK_KINDS = /* @__PURE__ */ new Set([
|
|
5200
5224
|
"rich_text_section",
|
|
5201
5225
|
"rich_text_list",
|
|
5202
5226
|
"rich_text_quote",
|
|
5203
5227
|
"rich_text_preformatted"
|
|
5204
5228
|
]);
|
|
5205
|
-
var SUPPORTED_INLINE_KINDS = /* @__PURE__ */ new Set(["text", "link", "emoji"]);
|
|
5229
|
+
var SUPPORTED_INLINE_KINDS = /* @__PURE__ */ new Set(["text", "link", "emoji", "user"]);
|
|
5206
5230
|
var SUPPORTED_TEXT_STYLE_KEYS = /* @__PURE__ */ new Set(["bold", "italic", "strike", "underline", "code"]);
|
|
5231
|
+
var SUPPORTED_USER_STYLE_KEYS = /* @__PURE__ */ new Set(["bold", "italic", "strike", "underline"]);
|
|
5207
5232
|
var MAX_LIST_INDENT = 8;
|
|
5208
5233
|
function detectLossy(block) {
|
|
5209
5234
|
const out = [];
|
|
@@ -5226,6 +5251,17 @@ function detectLossy(block) {
|
|
|
5226
5251
|
if ("border" in el && (el.border ?? 0) > 0) {
|
|
5227
5252
|
out.push({ label: "Quote/preformatted border", where });
|
|
5228
5253
|
}
|
|
5254
|
+
if (el.type === "rich_text_preformatted") {
|
|
5255
|
+
el.elements?.forEach((inline, i) => {
|
|
5256
|
+
const sub = `${where}.elements[${i}]`;
|
|
5257
|
+
if (inline.type !== "text") {
|
|
5258
|
+
out.push({ label: `In code block: ${inline.type}`, where: sub });
|
|
5259
|
+
} else if (Object.values(inline.style ?? {}).some(Boolean)) {
|
|
5260
|
+
out.push({ label: "Styled text in code block", where: sub });
|
|
5261
|
+
}
|
|
5262
|
+
});
|
|
5263
|
+
return;
|
|
5264
|
+
}
|
|
5229
5265
|
scanInlines(el.elements ?? [], where, out);
|
|
5230
5266
|
});
|
|
5231
5267
|
return out;
|
|
@@ -5247,6 +5283,13 @@ function scanInlines(inlines, where, out) {
|
|
|
5247
5283
|
}
|
|
5248
5284
|
}
|
|
5249
5285
|
}
|
|
5286
|
+
if (inline.type === "user") {
|
|
5287
|
+
for (const k of Object.keys(inline.style ?? {})) {
|
|
5288
|
+
if (!SUPPORTED_USER_STYLE_KEYS.has(k)) {
|
|
5289
|
+
out.push({ label: `Mention style: ${k}`, where: sub });
|
|
5290
|
+
}
|
|
5291
|
+
}
|
|
5292
|
+
}
|
|
5250
5293
|
});
|
|
5251
5294
|
}
|
|
5252
5295
|
var defaultEmojiResolver = (el) => ({
|
|
@@ -5358,6 +5401,17 @@ function inlinesToPM(inlines, resolveEmoji) {
|
|
|
5358
5401
|
continue;
|
|
5359
5402
|
}
|
|
5360
5403
|
out.push({ type: "emoji", attrs });
|
|
5404
|
+
} else if (inline.type === "user") {
|
|
5405
|
+
const user = inline;
|
|
5406
|
+
if (!user.user_id) {
|
|
5407
|
+
continue;
|
|
5408
|
+
}
|
|
5409
|
+
const node = { type: USER_MENTION_NODE, attrs: { userId: user.user_id, name: null } };
|
|
5410
|
+
const marks = styleToMarks(user.style);
|
|
5411
|
+
if (marks.length > 0) {
|
|
5412
|
+
node.marks = marks;
|
|
5413
|
+
}
|
|
5414
|
+
out.push(node);
|
|
5361
5415
|
} else if (inline.type === "link") {
|
|
5362
5416
|
const link = inline;
|
|
5363
5417
|
const text = link.text || link.url || "";
|
|
@@ -5491,6 +5545,19 @@ function proseMirrorInlinesToRichTextElements(nodes) {
|
|
|
5491
5545
|
out.push(emoji);
|
|
5492
5546
|
continue;
|
|
5493
5547
|
}
|
|
5548
|
+
if (node.type === USER_MENTION_NODE) {
|
|
5549
|
+
const userId = String(node.attrs?.userId ?? "");
|
|
5550
|
+
if (!userId) {
|
|
5551
|
+
continue;
|
|
5552
|
+
}
|
|
5553
|
+
const user = { type: "user", user_id: userId };
|
|
5554
|
+
const style2 = userStyle(node.marks ?? []);
|
|
5555
|
+
if (Object.keys(style2).length > 0) {
|
|
5556
|
+
user.style = style2;
|
|
5557
|
+
}
|
|
5558
|
+
out.push(user);
|
|
5559
|
+
continue;
|
|
5560
|
+
}
|
|
5494
5561
|
if (node.type === "hardBreak") {
|
|
5495
5562
|
out.push({ type: "text", text: "\n" });
|
|
5496
5563
|
continue;
|
|
@@ -5545,6 +5612,15 @@ function marksToStyle(marks) {
|
|
|
5545
5612
|
}
|
|
5546
5613
|
return style;
|
|
5547
5614
|
}
|
|
5615
|
+
function userStyle(marks) {
|
|
5616
|
+
const style = {};
|
|
5617
|
+
for (const [key, value] of Object.entries(marksToStyle(marks))) {
|
|
5618
|
+
if (SUPPORTED_USER_STYLE_KEYS.has(key)) {
|
|
5619
|
+
style[key] = value;
|
|
5620
|
+
}
|
|
5621
|
+
}
|
|
5622
|
+
return style;
|
|
5623
|
+
}
|
|
5548
5624
|
function mergeAdjacentTextRuns(inlines) {
|
|
5549
5625
|
const out = [];
|
|
5550
5626
|
for (const inline of inlines) {
|
|
@@ -5611,6 +5687,91 @@ var EmojiNode = react.Node.create({
|
|
|
5611
5687
|
return react.ReactNodeViewRenderer(EmojiNodeView);
|
|
5612
5688
|
}
|
|
5613
5689
|
});
|
|
5690
|
+
var MENTION_SEARCH_DEBOUNCE_MS = 200;
|
|
5691
|
+
function insertUserMention(editor, range, user) {
|
|
5692
|
+
const to = editor.state.doc.textBetween(range.to, Math.min(range.to + 1, editor.state.doc.content.size)) === " " ? range.to + 1 : range.to;
|
|
5693
|
+
editor.chain().focus().insertContentAt({ from: range.from, to }, [
|
|
5694
|
+
{ type: USER_MENTION_NODE, attrs: { userId: user.id, name: user.name } },
|
|
5695
|
+
{ type: "text", text: " " }
|
|
5696
|
+
]).run();
|
|
5697
|
+
}
|
|
5698
|
+
function insertMentionTrigger(editor) {
|
|
5699
|
+
const { from } = editor.state.selection;
|
|
5700
|
+
const before = from > 1 ? editor.state.doc.textBetween(from - 1, from, void 0, "\uFFFC") : "";
|
|
5701
|
+
const needsSpace = before !== "" && !/\s/.test(before);
|
|
5702
|
+
editor.chain().focus().insertContent(needsSpace ? " @" : "@").run();
|
|
5703
|
+
}
|
|
5704
|
+
function dismissMentionSuggestion(editor) {
|
|
5705
|
+
suggestion.exitSuggestion(editor.view);
|
|
5706
|
+
}
|
|
5707
|
+
var MentionSuggestion = react.Extension.create({
|
|
5708
|
+
name: "userMentionSuggestion",
|
|
5709
|
+
addOptions() {
|
|
5710
|
+
return {
|
|
5711
|
+
isEnabled: () => false,
|
|
5712
|
+
search: async () => [],
|
|
5713
|
+
render: () => ({})
|
|
5714
|
+
};
|
|
5715
|
+
},
|
|
5716
|
+
addProseMirrorPlugins() {
|
|
5717
|
+
const { isEnabled, search, render } = this.options;
|
|
5718
|
+
return [
|
|
5719
|
+
suggestion.Suggestion({
|
|
5720
|
+
editor: this.editor,
|
|
5721
|
+
char: "@",
|
|
5722
|
+
debounce: MENTION_SEARCH_DEBOUNCE_MS,
|
|
5723
|
+
allow: () => isEnabled(),
|
|
5724
|
+
items: ({ query, signal }) => search(query, signal),
|
|
5725
|
+
command: ({ editor, range, props }) => insertUserMention(editor, range, props),
|
|
5726
|
+
render
|
|
5727
|
+
})
|
|
5728
|
+
];
|
|
5729
|
+
}
|
|
5730
|
+
});
|
|
5731
|
+
function userMentionLabel(attrs, getUserName) {
|
|
5732
|
+
return getUserName?.(attrs.userId) ?? attrs.name ?? attrs.userId;
|
|
5733
|
+
}
|
|
5734
|
+
function UserMentionNodeView({ node }) {
|
|
5735
|
+
const mentions = useMentions();
|
|
5736
|
+
const attrs = node.attrs;
|
|
5737
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(react.NodeViewWrapper, { as: "span", className: "bk-user-mention", "data-user-mention": attrs.userId, children: [
|
|
5738
|
+
"@",
|
|
5739
|
+
userMentionLabel(attrs, mentions?.getUserName)
|
|
5740
|
+
] });
|
|
5741
|
+
}
|
|
5742
|
+
var UserMentionNode = react.Node.create({
|
|
5743
|
+
name: USER_MENTION_NODE,
|
|
5744
|
+
group: "inline",
|
|
5745
|
+
inline: true,
|
|
5746
|
+
atom: true,
|
|
5747
|
+
selectable: true,
|
|
5748
|
+
addAttributes() {
|
|
5749
|
+
return {
|
|
5750
|
+
userId: {
|
|
5751
|
+
default: "",
|
|
5752
|
+
parseHTML: (element) => element.getAttribute("data-user-mention") ?? "",
|
|
5753
|
+
renderHTML: (attributes) => ({ "data-user-mention": attributes.userId })
|
|
5754
|
+
},
|
|
5755
|
+
name: {
|
|
5756
|
+
default: null,
|
|
5757
|
+
parseHTML: (element) => element.getAttribute("data-user-name"),
|
|
5758
|
+
renderHTML: (attributes) => attributes.name ? { "data-user-name": attributes.name } : {}
|
|
5759
|
+
}
|
|
5760
|
+
};
|
|
5761
|
+
},
|
|
5762
|
+
parseHTML() {
|
|
5763
|
+
return [{ tag: "span[data-user-mention]" }];
|
|
5764
|
+
},
|
|
5765
|
+
renderHTML({ node, HTMLAttributes }) {
|
|
5766
|
+
return ["span", react.mergeAttributes(HTMLAttributes), `@${userMentionLabel(node.attrs)}`];
|
|
5767
|
+
},
|
|
5768
|
+
renderText({ node }) {
|
|
5769
|
+
return `@${userMentionLabel(node.attrs)}`;
|
|
5770
|
+
},
|
|
5771
|
+
addNodeView() {
|
|
5772
|
+
return react.ReactNodeViewRenderer(UserMentionNodeView);
|
|
5773
|
+
}
|
|
5774
|
+
});
|
|
5614
5775
|
|
|
5615
5776
|
// src/lib/emoji-recents.ts
|
|
5616
5777
|
var STORAGE_KEY = "bk:emoji-recents";
|
|
@@ -5892,6 +6053,133 @@ function EmojiPickerButton({
|
|
|
5892
6053
|
) })
|
|
5893
6054
|
] });
|
|
5894
6055
|
}
|
|
6056
|
+
function nextMentionPickerState(props, prev) {
|
|
6057
|
+
return {
|
|
6058
|
+
items: props.loading && prev ? prev.items : props.items,
|
|
6059
|
+
loading: props.loading,
|
|
6060
|
+
clientRect: props.clientRect ?? null,
|
|
6061
|
+
command: props.command
|
|
6062
|
+
};
|
|
6063
|
+
}
|
|
6064
|
+
function MentionPicker({
|
|
6065
|
+
editor,
|
|
6066
|
+
state,
|
|
6067
|
+
keyHandlerRef,
|
|
6068
|
+
onDismiss
|
|
6069
|
+
}) {
|
|
6070
|
+
const { items, loading, clientRect, command } = state;
|
|
6071
|
+
const [activeIndex, setActiveIndex] = React.useState(0);
|
|
6072
|
+
const [shownItems, setShownItems] = React.useState(items);
|
|
6073
|
+
if (shownItems !== items) {
|
|
6074
|
+
setShownItems(items);
|
|
6075
|
+
setActiveIndex(0);
|
|
6076
|
+
}
|
|
6077
|
+
const listRef = React.useRef(null);
|
|
6078
|
+
React.useEffect(() => {
|
|
6079
|
+
keyHandlerRef.current = (event) => {
|
|
6080
|
+
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
|
|
6081
|
+
if (items.length > 0) {
|
|
6082
|
+
const step = event.key === "ArrowDown" ? 1 : -1;
|
|
6083
|
+
setActiveIndex((index) => (index + step + items.length) % items.length);
|
|
6084
|
+
}
|
|
6085
|
+
return true;
|
|
6086
|
+
}
|
|
6087
|
+
if (event.key === "Enter" || event.key === "Tab") {
|
|
6088
|
+
const user = items[activeIndex];
|
|
6089
|
+
if (!user) {
|
|
6090
|
+
return false;
|
|
6091
|
+
}
|
|
6092
|
+
command(user);
|
|
6093
|
+
return true;
|
|
6094
|
+
}
|
|
6095
|
+
return false;
|
|
6096
|
+
};
|
|
6097
|
+
return () => {
|
|
6098
|
+
keyHandlerRef.current = () => false;
|
|
6099
|
+
};
|
|
6100
|
+
}, [keyHandlerRef, items, activeIndex, command]);
|
|
6101
|
+
React.useEffect(() => {
|
|
6102
|
+
const row = listRef.current?.querySelectorAll('[role="option"]')[activeIndex];
|
|
6103
|
+
row?.scrollIntoView?.({ block: "nearest" });
|
|
6104
|
+
}, [activeIndex]);
|
|
6105
|
+
const anchorRef = React.useMemo(
|
|
6106
|
+
() => ({
|
|
6107
|
+
current: {
|
|
6108
|
+
getBoundingClientRect: () => clientRect?.() ?? editor.view.dom.getBoundingClientRect()
|
|
6109
|
+
}
|
|
6110
|
+
}),
|
|
6111
|
+
[clientRect, editor]
|
|
6112
|
+
);
|
|
6113
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
6114
|
+
Popover,
|
|
6115
|
+
{
|
|
6116
|
+
open: true,
|
|
6117
|
+
onOpenChange: (open) => {
|
|
6118
|
+
if (!open) {
|
|
6119
|
+
onDismiss();
|
|
6120
|
+
}
|
|
6121
|
+
},
|
|
6122
|
+
children: [
|
|
6123
|
+
/* @__PURE__ */ jsxRuntime.jsx(PopoverAnchor, { virtualRef: anchorRef }),
|
|
6124
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
6125
|
+
PopoverContent,
|
|
6126
|
+
{
|
|
6127
|
+
side: "bottom",
|
|
6128
|
+
align: "start",
|
|
6129
|
+
sideOffset: 4,
|
|
6130
|
+
className: "max-h-[min(15rem,var(--radix-popover-content-available-height))] w-64 p-1",
|
|
6131
|
+
onOpenAutoFocus: (e) => e.preventDefault(),
|
|
6132
|
+
onCloseAutoFocus: (e) => e.preventDefault(),
|
|
6133
|
+
onEscapeKeyDown: (e) => e.stopPropagation(),
|
|
6134
|
+
onInteractOutside: (e) => {
|
|
6135
|
+
if (e.target instanceof Node && editor.view.dom.contains(e.target)) {
|
|
6136
|
+
e.preventDefault();
|
|
6137
|
+
}
|
|
6138
|
+
},
|
|
6139
|
+
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) => (
|
|
6140
|
+
// biome-ignore lint/a11y/useFocusableInteractive: focus stays in the editor, which drives this list through the suggestion plugin's key handling.
|
|
6141
|
+
// biome-ignore lint/a11y/useKeyWithClickEvents: the keyboard path is the editor's, forwarded via keyHandlerRef.
|
|
6142
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
6143
|
+
"div",
|
|
6144
|
+
{
|
|
6145
|
+
role: "option",
|
|
6146
|
+
"aria-selected": index === activeIndex,
|
|
6147
|
+
onPointerDown: (e) => e.preventDefault(),
|
|
6148
|
+
onClick: () => command(user),
|
|
6149
|
+
onMouseMove: () => setActiveIndex(index),
|
|
6150
|
+
className: cn(
|
|
6151
|
+
"flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm text-foreground",
|
|
6152
|
+
index === activeIndex && "bg-accent"
|
|
6153
|
+
),
|
|
6154
|
+
children: [
|
|
6155
|
+
/* @__PURE__ */ jsxRuntime.jsx(MentionAvatar, { user }),
|
|
6156
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: user.name })
|
|
6157
|
+
]
|
|
6158
|
+
},
|
|
6159
|
+
user.id
|
|
6160
|
+
)
|
|
6161
|
+
)) })
|
|
6162
|
+
}
|
|
6163
|
+
)
|
|
6164
|
+
]
|
|
6165
|
+
}
|
|
6166
|
+
);
|
|
6167
|
+
}
|
|
6168
|
+
function MentionAvatar({ user }) {
|
|
6169
|
+
const [failed, setFailed] = React.useState(false);
|
|
6170
|
+
const src = user.avatarUrl && isSafeImageSrc(user.avatarUrl) ? user.avatarUrl : null;
|
|
6171
|
+
if (src && !failed) {
|
|
6172
|
+
return /* @__PURE__ */ jsxRuntime.jsx("img", { src, alt: "", className: "h-5 w-5 shrink-0 rounded object-cover", onError: () => setFailed(true) });
|
|
6173
|
+
}
|
|
6174
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
6175
|
+
"span",
|
|
6176
|
+
{
|
|
6177
|
+
"aria-hidden": "true",
|
|
6178
|
+
className: "flex h-5 w-5 shrink-0 items-center justify-center rounded bg-muted text-[10px] font-medium text-muted-foreground",
|
|
6179
|
+
children: Array.from(user.name.trim())[0]?.toUpperCase() ?? "@"
|
|
6180
|
+
}
|
|
6181
|
+
);
|
|
6182
|
+
}
|
|
5895
6183
|
var BLOCK_ELEMENT_LABEL = {
|
|
5896
6184
|
rich_text_section: "Section",
|
|
5897
6185
|
rich_text_list: "List",
|
|
@@ -6467,7 +6755,7 @@ function LossyBanner({ reasons }) {
|
|
|
6467
6755
|
] });
|
|
6468
6756
|
}
|
|
6469
6757
|
var RICH_TEXT_INPUT_RULE_ALLOWLIST = ["bold", "italic", "strike", "code"];
|
|
6470
|
-
function buildRichTextExtensions() {
|
|
6758
|
+
function buildRichTextExtensions(mentionSuggestion) {
|
|
6471
6759
|
return [
|
|
6472
6760
|
StarterKit__default.default.configure({
|
|
6473
6761
|
heading: false,
|
|
@@ -6500,7 +6788,11 @@ function buildRichTextExtensions() {
|
|
|
6500
6788
|
// via the link button.
|
|
6501
6789
|
shouldAutoLink: hasExplicitSafeScheme,
|
|
6502
6790
|
HTMLAttributes: { rel: "noreferrer noopener", target: "_blank" }
|
|
6503
|
-
})
|
|
6791
|
+
}),
|
|
6792
|
+
// Always registered, so mentions already in the content round-trip with
|
|
6793
|
+
// or without the picker.
|
|
6794
|
+
UserMentionNode,
|
|
6795
|
+
...mentionSuggestion ? [MentionSuggestion.configure(mentionSuggestion)] : []
|
|
6504
6796
|
];
|
|
6505
6797
|
}
|
|
6506
6798
|
function RichTextWysiwygEditor({ block, onChange }) {
|
|
@@ -6527,8 +6819,25 @@ function RichTextWysiwygEditor({ block, onChange }) {
|
|
|
6527
6819
|
},
|
|
6528
6820
|
[onChange]
|
|
6529
6821
|
);
|
|
6822
|
+
const mentions = useMentions();
|
|
6823
|
+
const mentionsEnabled = mentions !== null;
|
|
6824
|
+
const mentionsRef = React.useRef(mentions);
|
|
6825
|
+
React.useEffect(() => {
|
|
6826
|
+
mentionsRef.current = mentions;
|
|
6827
|
+
}, [mentions]);
|
|
6828
|
+
const [mentionPicker, setMentionPicker] = React.useState(null);
|
|
6829
|
+
const mentionKeyHandlerRef = React.useRef(() => false);
|
|
6530
6830
|
const editor = react.useEditor({
|
|
6531
|
-
extensions: buildRichTextExtensions(
|
|
6831
|
+
extensions: buildRichTextExtensions({
|
|
6832
|
+
isEnabled: () => mentionsRef.current !== null,
|
|
6833
|
+
search: async (query, signal) => await mentionsRef.current?.searchUsers(query, { signal }) ?? [],
|
|
6834
|
+
render: () => ({
|
|
6835
|
+
onStart: (props) => setMentionPicker((prev) => nextMentionPickerState(props, prev)),
|
|
6836
|
+
onUpdate: (props) => setMentionPicker((prev) => nextMentionPickerState(props, prev)),
|
|
6837
|
+
onExit: () => setMentionPicker(null),
|
|
6838
|
+
onKeyDown: ({ event }) => mentionKeyHandlerRef.current(event)
|
|
6839
|
+
})
|
|
6840
|
+
}),
|
|
6532
6841
|
enableInputRules: RICH_TEXT_INPUT_RULE_ALLOWLIST,
|
|
6533
6842
|
content: richTextToProseMirror(block, resolveEmoji),
|
|
6534
6843
|
editorProps: {
|
|
@@ -6538,10 +6847,16 @@ function RichTextWysiwygEditor({ block, onChange }) {
|
|
|
6538
6847
|
},
|
|
6539
6848
|
onUpdate: ({ editor: ed }) => handleUpdate(ed)
|
|
6540
6849
|
});
|
|
6850
|
+
const seenBlockRef = React.useRef(block);
|
|
6541
6851
|
React.useEffect(() => {
|
|
6542
6852
|
if (!editor) {
|
|
6543
6853
|
return;
|
|
6544
6854
|
}
|
|
6855
|
+
const blockChanged = seenBlockRef.current !== block;
|
|
6856
|
+
seenBlockRef.current = block;
|
|
6857
|
+
if (!blockChanged && lastEmittedRef.current) {
|
|
6858
|
+
return;
|
|
6859
|
+
}
|
|
6545
6860
|
if (lastEmittedRef.current && JSON.stringify(lastEmittedRef.current) === JSON.stringify(block)) {
|
|
6546
6861
|
return;
|
|
6547
6862
|
}
|
|
@@ -6553,11 +6868,26 @@ function RichTextWysiwygEditor({ block, onChange }) {
|
|
|
6553
6868
|
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "min-h-[120px] rounded-md border border-input bg-background" });
|
|
6554
6869
|
}
|
|
6555
6870
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-2", children: [
|
|
6556
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
6557
|
-
|
|
6871
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
6872
|
+
Toolbar,
|
|
6873
|
+
{
|
|
6874
|
+
editor,
|
|
6875
|
+
mention: mentionsEnabled ? { active: mentionPicker !== null, insert: () => insertMentionTrigger(editor) } : null
|
|
6876
|
+
}
|
|
6877
|
+
),
|
|
6878
|
+
/* @__PURE__ */ jsxRuntime.jsx(react.EditorContent, { editor }),
|
|
6879
|
+
mentionPicker && /* @__PURE__ */ jsxRuntime.jsx(
|
|
6880
|
+
MentionPicker,
|
|
6881
|
+
{
|
|
6882
|
+
editor,
|
|
6883
|
+
state: mentionPicker,
|
|
6884
|
+
keyHandlerRef: mentionKeyHandlerRef,
|
|
6885
|
+
onDismiss: () => dismissMentionSuggestion(editor)
|
|
6886
|
+
}
|
|
6887
|
+
)
|
|
6558
6888
|
] });
|
|
6559
6889
|
}
|
|
6560
|
-
function Toolbar({ editor }) {
|
|
6890
|
+
function Toolbar({ editor, mention }) {
|
|
6561
6891
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-center gap-0.5 rounded-md border bg-muted/30 p-1", children: [
|
|
6562
6892
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
6563
6893
|
ToolbarButton,
|
|
@@ -6616,6 +6946,7 @@ function Toolbar({ editor }) {
|
|
|
6616
6946
|
}).run()
|
|
6617
6947
|
}
|
|
6618
6948
|
),
|
|
6949
|
+
mention && /* @__PURE__ */ jsxRuntime.jsx(ToolbarButton, { label: "Mention", active: mention.active, onClick: mention.insert, Icon: lucideReact.AtSign }),
|
|
6619
6950
|
/* @__PURE__ */ jsxRuntime.jsx(Divider, {}),
|
|
6620
6951
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
6621
6952
|
ToolbarButton,
|
|
@@ -10952,6 +11283,7 @@ function BlockKitchen(props) {
|
|
|
10952
11283
|
onValidationChange,
|
|
10953
11284
|
previewHooks,
|
|
10954
11285
|
customEmojis,
|
|
11286
|
+
mentions,
|
|
10955
11287
|
loadChannels,
|
|
10956
11288
|
loadSendAsUserStatus,
|
|
10957
11289
|
onSend,
|
|
@@ -11312,7 +11644,7 @@ function BlockKitchen(props) {
|
|
|
11312
11644
|
}
|
|
11313
11645
|
)
|
|
11314
11646
|
] }),
|
|
11315
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
11647
|
+
/* @__PURE__ */ jsxRuntime.jsx(MentionsProvider, { mentions, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
11316
11648
|
Surface,
|
|
11317
11649
|
{
|
|
11318
11650
|
blocks,
|
|
@@ -11332,7 +11664,7 @@ function BlockKitchen(props) {
|
|
|
11332
11664
|
isPaletteDrag: activePaletteVariant !== null,
|
|
11333
11665
|
onOpenPalette: () => setPaletteOpen(true)
|
|
11334
11666
|
}
|
|
11335
|
-
)
|
|
11667
|
+
) })
|
|
11336
11668
|
] })
|
|
11337
11669
|
]
|
|
11338
11670
|
}
|