@tightknitai/block-kitchen 0.10.18 → 0.10.20

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.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, Hash, 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
 
@@ -1065,6 +1066,58 @@ var defaultPalette = [
1065
1066
  ]
1066
1067
  ]
1067
1068
  })
1069
+ },
1070
+ {
1071
+ // Slack's documented example.
1072
+ id: "data_table_basic",
1073
+ label: "Data table",
1074
+ factory: () => ({
1075
+ type: "data_table",
1076
+ caption: "A Fabulous Table",
1077
+ rows: [
1078
+ [
1079
+ { type: "raw_text", text: "Name" },
1080
+ { type: "raw_text", text: "Department" },
1081
+ { type: "raw_text", text: "Badge" }
1082
+ ],
1083
+ [
1084
+ { type: "raw_text", text: "Data Refinement Department" },
1085
+ { type: "raw_text", text: "MDR" },
1086
+ {
1087
+ type: "rich_text",
1088
+ elements: [
1089
+ { type: "rich_text_section", elements: [{ type: "text", text: "Blue", style: { bold: true } }] }
1090
+ ]
1091
+ }
1092
+ ],
1093
+ [
1094
+ { type: "raw_text", text: "Art Sourcing Department" },
1095
+ { type: "raw_text", text: "O&D" },
1096
+ {
1097
+ type: "rich_text",
1098
+ elements: [
1099
+ {
1100
+ type: "rich_text_section",
1101
+ elements: [
1102
+ { type: "text", text: "Green " },
1103
+ { type: "text", text: "review", style: { italic: true } }
1104
+ ]
1105
+ }
1106
+ ]
1107
+ }
1108
+ ],
1109
+ [
1110
+ { type: "raw_text", text: "Wellness Department" },
1111
+ { type: "raw_text", text: "Wellness Center" },
1112
+ {
1113
+ type: "rich_text",
1114
+ elements: [
1115
+ { type: "rich_text_section", elements: [{ type: "text", text: "Limited", style: { bold: true } }] }
1116
+ ]
1117
+ }
1118
+ ]
1119
+ ]
1120
+ })
1068
1121
  }
1069
1122
  ]
1070
1123
  },
@@ -1729,6 +1782,8 @@ function labelForBlockType(type) {
1729
1782
  return "Data Visualization";
1730
1783
  case "container":
1731
1784
  return "Container";
1785
+ case "data_table":
1786
+ return "Data Table";
1732
1787
  }
1733
1788
  }
1734
1789
  var Sheet = DialogPrimitive.Root;
@@ -1833,6 +1888,13 @@ function CustomEmojiProvider({ customEmojis, children }) {
1833
1888
  function useCustomEmojis() {
1834
1889
  return useContext(CustomEmojiContext);
1835
1890
  }
1891
+ var MentionsContext = createContext(null);
1892
+ function MentionsProvider({ mentions, children }) {
1893
+ return /* @__PURE__ */ jsx(MentionsContext.Provider, { value: mentions ?? null, children });
1894
+ }
1895
+ function useMentions() {
1896
+ return useContext(MentionsContext);
1897
+ }
1836
1898
 
1837
1899
  // src/lib/error-grouping.ts
1838
1900
  function toValidatorSurface(surface) {
@@ -3333,12 +3395,27 @@ function ChannelCombobox({
3333
3395
  ] });
3334
3396
  }
3335
3397
  var EMBED_SELECTOR = "iframe[src], embed[src], object[data], source[src], video[src], audio[src], track[src]";
3398
+ function useLatestHooks(hooks) {
3399
+ const latest = useRef(hooks);
3400
+ latest.current = hooks;
3401
+ const [live] = useState(
3402
+ () => new Proxy(
3403
+ {},
3404
+ {
3405
+ get: (_target, key) => typeof key === "string" ? latest.current?.[key] : void 0,
3406
+ has: (_target, key) => typeof key === "string" && latest.current !== void 0 && key in latest.current
3407
+ }
3408
+ )
3409
+ );
3410
+ return live;
3411
+ }
3336
3412
  function SlackBlockPreview({
3337
3413
  block,
3338
3414
  hooks,
3339
3415
  theme = "light"
3340
3416
  }) {
3341
3417
  const rootRef = useRef(null);
3418
+ const liveHooks = useLatestHooks(hooks);
3342
3419
  const safeBlock = useMemo(() => sanitizeBlock(block), [block]);
3343
3420
  useEffect(() => {
3344
3421
  const root = rootRef.current;
@@ -3393,7 +3470,7 @@ function SlackBlockPreview({
3393
3470
  withoutWrapper: true,
3394
3471
  theme,
3395
3472
  blocks: [safeBlock],
3396
- hooks
3473
+ hooks: liveHooks
3397
3474
  }
3398
3475
  ) })
3399
3476
  );
@@ -5166,14 +5243,16 @@ function makeEmojiImportResolver(customByName, index = getEmojiIndex()) {
5166
5243
  }
5167
5244
 
5168
5245
  // src/lib/rich-text-tiptap.ts
5246
+ var USER_MENTION_NODE = "userMention";
5169
5247
  var SUPPORTED_BLOCK_KINDS = /* @__PURE__ */ new Set([
5170
5248
  "rich_text_section",
5171
5249
  "rich_text_list",
5172
5250
  "rich_text_quote",
5173
5251
  "rich_text_preformatted"
5174
5252
  ]);
5175
- var SUPPORTED_INLINE_KINDS = /* @__PURE__ */ new Set(["text", "link", "emoji"]);
5253
+ var SUPPORTED_INLINE_KINDS = /* @__PURE__ */ new Set(["text", "link", "emoji", "user"]);
5176
5254
  var SUPPORTED_TEXT_STYLE_KEYS = /* @__PURE__ */ new Set(["bold", "italic", "strike", "underline", "code"]);
5255
+ var SUPPORTED_USER_STYLE_KEYS = /* @__PURE__ */ new Set(["bold", "italic", "strike", "underline"]);
5177
5256
  var MAX_LIST_INDENT = 8;
5178
5257
  function detectLossy(block) {
5179
5258
  const out = [];
@@ -5196,6 +5275,17 @@ function detectLossy(block) {
5196
5275
  if ("border" in el && (el.border ?? 0) > 0) {
5197
5276
  out.push({ label: "Quote/preformatted border", where });
5198
5277
  }
5278
+ if (el.type === "rich_text_preformatted") {
5279
+ el.elements?.forEach((inline, i) => {
5280
+ const sub = `${where}.elements[${i}]`;
5281
+ if (inline.type !== "text") {
5282
+ out.push({ label: `In code block: ${inline.type}`, where: sub });
5283
+ } else if (Object.values(inline.style ?? {}).some(Boolean)) {
5284
+ out.push({ label: "Styled text in code block", where: sub });
5285
+ }
5286
+ });
5287
+ return;
5288
+ }
5199
5289
  scanInlines(el.elements ?? [], where, out);
5200
5290
  });
5201
5291
  return out;
@@ -5217,6 +5307,13 @@ function scanInlines(inlines, where, out) {
5217
5307
  }
5218
5308
  }
5219
5309
  }
5310
+ if (inline.type === "user") {
5311
+ for (const k of Object.keys(inline.style ?? {})) {
5312
+ if (!SUPPORTED_USER_STYLE_KEYS.has(k)) {
5313
+ out.push({ label: `Mention style: ${k}`, where: sub });
5314
+ }
5315
+ }
5316
+ }
5220
5317
  });
5221
5318
  }
5222
5319
  var defaultEmojiResolver = (el) => ({
@@ -5328,6 +5425,17 @@ function inlinesToPM(inlines, resolveEmoji) {
5328
5425
  continue;
5329
5426
  }
5330
5427
  out.push({ type: "emoji", attrs });
5428
+ } else if (inline.type === "user") {
5429
+ const user = inline;
5430
+ if (!user.user_id) {
5431
+ continue;
5432
+ }
5433
+ const node = { type: USER_MENTION_NODE, attrs: { userId: user.user_id, name: null } };
5434
+ const marks = styleToMarks(user.style);
5435
+ if (marks.length > 0) {
5436
+ node.marks = marks;
5437
+ }
5438
+ out.push(node);
5331
5439
  } else if (inline.type === "link") {
5332
5440
  const link = inline;
5333
5441
  const text = link.text || link.url || "";
@@ -5461,6 +5569,19 @@ function proseMirrorInlinesToRichTextElements(nodes) {
5461
5569
  out.push(emoji);
5462
5570
  continue;
5463
5571
  }
5572
+ if (node.type === USER_MENTION_NODE) {
5573
+ const userId = String(node.attrs?.userId ?? "");
5574
+ if (!userId) {
5575
+ continue;
5576
+ }
5577
+ const user = { type: "user", user_id: userId };
5578
+ const style2 = userStyle(node.marks ?? []);
5579
+ if (Object.keys(style2).length > 0) {
5580
+ user.style = style2;
5581
+ }
5582
+ out.push(user);
5583
+ continue;
5584
+ }
5464
5585
  if (node.type === "hardBreak") {
5465
5586
  out.push({ type: "text", text: "\n" });
5466
5587
  continue;
@@ -5515,6 +5636,15 @@ function marksToStyle(marks) {
5515
5636
  }
5516
5637
  return style;
5517
5638
  }
5639
+ function userStyle(marks) {
5640
+ const style = {};
5641
+ for (const [key, value] of Object.entries(marksToStyle(marks))) {
5642
+ if (SUPPORTED_USER_STYLE_KEYS.has(key)) {
5643
+ style[key] = value;
5644
+ }
5645
+ }
5646
+ return style;
5647
+ }
5518
5648
  function mergeAdjacentTextRuns(inlines) {
5519
5649
  const out = [];
5520
5650
  for (const inline of inlines) {
@@ -5581,6 +5711,91 @@ var EmojiNode = Node$1.create({
5581
5711
  return ReactNodeViewRenderer(EmojiNodeView);
5582
5712
  }
5583
5713
  });
5714
+ var MENTION_SEARCH_DEBOUNCE_MS = 200;
5715
+ function insertUserMention(editor, range, user) {
5716
+ const to = editor.state.doc.textBetween(range.to, Math.min(range.to + 1, editor.state.doc.content.size)) === " " ? range.to + 1 : range.to;
5717
+ editor.chain().focus().insertContentAt({ from: range.from, to }, [
5718
+ { type: USER_MENTION_NODE, attrs: { userId: user.id, name: user.name } },
5719
+ { type: "text", text: " " }
5720
+ ]).run();
5721
+ }
5722
+ function insertMentionTrigger(editor) {
5723
+ const { from } = editor.state.selection;
5724
+ const before = from > 1 ? editor.state.doc.textBetween(from - 1, from, void 0, "\uFFFC") : "";
5725
+ const needsSpace = before !== "" && !/\s/.test(before);
5726
+ editor.chain().focus().insertContent(needsSpace ? " @" : "@").run();
5727
+ }
5728
+ function dismissMentionSuggestion(editor) {
5729
+ exitSuggestion(editor.view);
5730
+ }
5731
+ var MentionSuggestion = Extension.create({
5732
+ name: "userMentionSuggestion",
5733
+ addOptions() {
5734
+ return {
5735
+ isEnabled: () => false,
5736
+ search: async () => [],
5737
+ render: () => ({})
5738
+ };
5739
+ },
5740
+ addProseMirrorPlugins() {
5741
+ const { isEnabled, search, render } = this.options;
5742
+ return [
5743
+ Suggestion({
5744
+ editor: this.editor,
5745
+ char: "@",
5746
+ debounce: MENTION_SEARCH_DEBOUNCE_MS,
5747
+ allow: () => isEnabled(),
5748
+ items: ({ query, signal }) => search(query, signal),
5749
+ command: ({ editor, range, props }) => insertUserMention(editor, range, props),
5750
+ render
5751
+ })
5752
+ ];
5753
+ }
5754
+ });
5755
+ function userMentionLabel(attrs, getUserName) {
5756
+ return getUserName?.(attrs.userId) ?? attrs.name ?? attrs.userId;
5757
+ }
5758
+ function UserMentionNodeView({ node }) {
5759
+ const mentions = useMentions();
5760
+ const attrs = node.attrs;
5761
+ return /* @__PURE__ */ jsxs(NodeViewWrapper, { as: "span", className: "bk-user-mention", "data-user-mention": attrs.userId, children: [
5762
+ "@",
5763
+ userMentionLabel(attrs, mentions?.getUserName)
5764
+ ] });
5765
+ }
5766
+ var UserMentionNode = Node$1.create({
5767
+ name: USER_MENTION_NODE,
5768
+ group: "inline",
5769
+ inline: true,
5770
+ atom: true,
5771
+ selectable: true,
5772
+ addAttributes() {
5773
+ return {
5774
+ userId: {
5775
+ default: "",
5776
+ parseHTML: (element) => element.getAttribute("data-user-mention") ?? "",
5777
+ renderHTML: (attributes) => ({ "data-user-mention": attributes.userId })
5778
+ },
5779
+ name: {
5780
+ default: null,
5781
+ parseHTML: (element) => element.getAttribute("data-user-name"),
5782
+ renderHTML: (attributes) => attributes.name ? { "data-user-name": attributes.name } : {}
5783
+ }
5784
+ };
5785
+ },
5786
+ parseHTML() {
5787
+ return [{ tag: "span[data-user-mention]" }];
5788
+ },
5789
+ renderHTML({ node, HTMLAttributes }) {
5790
+ return ["span", mergeAttributes(HTMLAttributes), `@${userMentionLabel(node.attrs)}`];
5791
+ },
5792
+ renderText({ node }) {
5793
+ return `@${userMentionLabel(node.attrs)}`;
5794
+ },
5795
+ addNodeView() {
5796
+ return ReactNodeViewRenderer(UserMentionNodeView);
5797
+ }
5798
+ });
5584
5799
 
5585
5800
  // src/lib/emoji-recents.ts
5586
5801
  var STORAGE_KEY = "bk:emoji-recents";
@@ -5862,6 +6077,133 @@ function EmojiPickerButton({
5862
6077
  ) })
5863
6078
  ] });
5864
6079
  }
6080
+ function nextMentionPickerState(props, prev) {
6081
+ return {
6082
+ items: props.loading && prev ? prev.items : props.items,
6083
+ loading: props.loading,
6084
+ clientRect: props.clientRect ?? null,
6085
+ command: props.command
6086
+ };
6087
+ }
6088
+ function MentionPicker({
6089
+ editor,
6090
+ state,
6091
+ keyHandlerRef,
6092
+ onDismiss
6093
+ }) {
6094
+ const { items, loading, clientRect, command } = state;
6095
+ const [activeIndex, setActiveIndex] = useState(0);
6096
+ const [shownItems, setShownItems] = useState(items);
6097
+ if (shownItems !== items) {
6098
+ setShownItems(items);
6099
+ setActiveIndex(0);
6100
+ }
6101
+ const listRef = useRef(null);
6102
+ useEffect(() => {
6103
+ keyHandlerRef.current = (event) => {
6104
+ if (event.key === "ArrowDown" || event.key === "ArrowUp") {
6105
+ if (items.length > 0) {
6106
+ const step = event.key === "ArrowDown" ? 1 : -1;
6107
+ setActiveIndex((index) => (index + step + items.length) % items.length);
6108
+ }
6109
+ return true;
6110
+ }
6111
+ if (event.key === "Enter" || event.key === "Tab") {
6112
+ const user = items[activeIndex];
6113
+ if (!user) {
6114
+ return false;
6115
+ }
6116
+ command(user);
6117
+ return true;
6118
+ }
6119
+ return false;
6120
+ };
6121
+ return () => {
6122
+ keyHandlerRef.current = () => false;
6123
+ };
6124
+ }, [keyHandlerRef, items, activeIndex, command]);
6125
+ useEffect(() => {
6126
+ const row = listRef.current?.querySelectorAll('[role="option"]')[activeIndex];
6127
+ row?.scrollIntoView?.({ block: "nearest" });
6128
+ }, [activeIndex]);
6129
+ const anchorRef = useMemo(
6130
+ () => ({
6131
+ current: {
6132
+ getBoundingClientRect: () => clientRect?.() ?? editor.view.dom.getBoundingClientRect()
6133
+ }
6134
+ }),
6135
+ [clientRect, editor]
6136
+ );
6137
+ return /* @__PURE__ */ jsxs(
6138
+ Popover,
6139
+ {
6140
+ open: true,
6141
+ onOpenChange: (open) => {
6142
+ if (!open) {
6143
+ onDismiss();
6144
+ }
6145
+ },
6146
+ children: [
6147
+ /* @__PURE__ */ jsx(PopoverAnchor, { virtualRef: anchorRef }),
6148
+ /* @__PURE__ */ jsx(
6149
+ PopoverContent,
6150
+ {
6151
+ side: "bottom",
6152
+ align: "start",
6153
+ sideOffset: 4,
6154
+ className: "max-h-[min(15rem,var(--radix-popover-content-available-height))] w-64 p-1",
6155
+ onOpenAutoFocus: (e) => e.preventDefault(),
6156
+ onCloseAutoFocus: (e) => e.preventDefault(),
6157
+ onEscapeKeyDown: (e) => e.stopPropagation(),
6158
+ onInteractOutside: (e) => {
6159
+ if (e.target instanceof Node && editor.view.dom.contains(e.target)) {
6160
+ e.preventDefault();
6161
+ }
6162
+ },
6163
+ 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) => (
6164
+ // biome-ignore lint/a11y/useFocusableInteractive: focus stays in the editor, which drives this list through the suggestion plugin's key handling.
6165
+ // biome-ignore lint/a11y/useKeyWithClickEvents: the keyboard path is the editor's, forwarded via keyHandlerRef.
6166
+ /* @__PURE__ */ jsxs(
6167
+ "div",
6168
+ {
6169
+ role: "option",
6170
+ "aria-selected": index === activeIndex,
6171
+ onPointerDown: (e) => e.preventDefault(),
6172
+ onClick: () => command(user),
6173
+ onMouseMove: () => setActiveIndex(index),
6174
+ className: cn(
6175
+ "flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm text-foreground",
6176
+ index === activeIndex && "bg-accent"
6177
+ ),
6178
+ children: [
6179
+ /* @__PURE__ */ jsx(MentionAvatar, { user }),
6180
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: user.name })
6181
+ ]
6182
+ },
6183
+ user.id
6184
+ )
6185
+ )) })
6186
+ }
6187
+ )
6188
+ ]
6189
+ }
6190
+ );
6191
+ }
6192
+ function MentionAvatar({ user }) {
6193
+ const [failed, setFailed] = useState(false);
6194
+ const src = user.avatarUrl && isSafeImageSrc(user.avatarUrl) ? user.avatarUrl : null;
6195
+ if (src && !failed) {
6196
+ return /* @__PURE__ */ jsx("img", { src, alt: "", className: "h-5 w-5 shrink-0 rounded object-cover", onError: () => setFailed(true) });
6197
+ }
6198
+ return /* @__PURE__ */ jsx(
6199
+ "span",
6200
+ {
6201
+ "aria-hidden": "true",
6202
+ className: "flex h-5 w-5 shrink-0 items-center justify-center rounded bg-muted text-[10px] font-medium text-muted-foreground",
6203
+ children: Array.from(user.name.trim())[0]?.toUpperCase() ?? "@"
6204
+ }
6205
+ );
6206
+ }
5865
6207
  var BLOCK_ELEMENT_LABEL = {
5866
6208
  rich_text_section: "Section",
5867
6209
  rich_text_list: "List",
@@ -6437,7 +6779,7 @@ function LossyBanner({ reasons }) {
6437
6779
  ] });
6438
6780
  }
6439
6781
  var RICH_TEXT_INPUT_RULE_ALLOWLIST = ["bold", "italic", "strike", "code"];
6440
- function buildRichTextExtensions() {
6782
+ function buildRichTextExtensions(mentionSuggestion) {
6441
6783
  return [
6442
6784
  StarterKit.configure({
6443
6785
  heading: false,
@@ -6470,7 +6812,11 @@ function buildRichTextExtensions() {
6470
6812
  // via the link button.
6471
6813
  shouldAutoLink: hasExplicitSafeScheme,
6472
6814
  HTMLAttributes: { rel: "noreferrer noopener", target: "_blank" }
6473
- })
6815
+ }),
6816
+ // Always registered, so mentions already in the content round-trip with
6817
+ // or without the picker.
6818
+ UserMentionNode,
6819
+ ...mentionSuggestion ? [MentionSuggestion.configure(mentionSuggestion)] : []
6474
6820
  ];
6475
6821
  }
6476
6822
  function RichTextWysiwygEditor({ block, onChange }) {
@@ -6497,8 +6843,25 @@ function RichTextWysiwygEditor({ block, onChange }) {
6497
6843
  },
6498
6844
  [onChange]
6499
6845
  );
6846
+ const mentions = useMentions();
6847
+ const mentionsEnabled = mentions !== null;
6848
+ const mentionsRef = useRef(mentions);
6849
+ useEffect(() => {
6850
+ mentionsRef.current = mentions;
6851
+ }, [mentions]);
6852
+ const [mentionPicker, setMentionPicker] = useState(null);
6853
+ const mentionKeyHandlerRef = useRef(() => false);
6500
6854
  const editor = useEditor({
6501
- extensions: buildRichTextExtensions(),
6855
+ extensions: buildRichTextExtensions({
6856
+ isEnabled: () => mentionsRef.current !== null,
6857
+ search: async (query, signal) => await mentionsRef.current?.searchUsers(query, { signal }) ?? [],
6858
+ render: () => ({
6859
+ onStart: (props) => setMentionPicker((prev) => nextMentionPickerState(props, prev)),
6860
+ onUpdate: (props) => setMentionPicker((prev) => nextMentionPickerState(props, prev)),
6861
+ onExit: () => setMentionPicker(null),
6862
+ onKeyDown: ({ event }) => mentionKeyHandlerRef.current(event)
6863
+ })
6864
+ }),
6502
6865
  enableInputRules: RICH_TEXT_INPUT_RULE_ALLOWLIST,
6503
6866
  content: richTextToProseMirror(block, resolveEmoji),
6504
6867
  editorProps: {
@@ -6508,10 +6871,16 @@ function RichTextWysiwygEditor({ block, onChange }) {
6508
6871
  },
6509
6872
  onUpdate: ({ editor: ed }) => handleUpdate(ed)
6510
6873
  });
6874
+ const seenBlockRef = useRef(block);
6511
6875
  useEffect(() => {
6512
6876
  if (!editor) {
6513
6877
  return;
6514
6878
  }
6879
+ const blockChanged = seenBlockRef.current !== block;
6880
+ seenBlockRef.current = block;
6881
+ if (!blockChanged && lastEmittedRef.current) {
6882
+ return;
6883
+ }
6515
6884
  if (lastEmittedRef.current && JSON.stringify(lastEmittedRef.current) === JSON.stringify(block)) {
6516
6885
  return;
6517
6886
  }
@@ -6523,11 +6892,26 @@ function RichTextWysiwygEditor({ block, onChange }) {
6523
6892
  return /* @__PURE__ */ jsx("div", { className: "min-h-[120px] rounded-md border border-input bg-background" });
6524
6893
  }
6525
6894
  return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", children: [
6526
- /* @__PURE__ */ jsx(Toolbar, { editor }),
6527
- /* @__PURE__ */ jsx(EditorContent, { editor })
6895
+ /* @__PURE__ */ jsx(
6896
+ Toolbar,
6897
+ {
6898
+ editor,
6899
+ mention: mentionsEnabled ? { active: mentionPicker !== null, insert: () => insertMentionTrigger(editor) } : null
6900
+ }
6901
+ ),
6902
+ /* @__PURE__ */ jsx(EditorContent, { editor }),
6903
+ mentionPicker && /* @__PURE__ */ jsx(
6904
+ MentionPicker,
6905
+ {
6906
+ editor,
6907
+ state: mentionPicker,
6908
+ keyHandlerRef: mentionKeyHandlerRef,
6909
+ onDismiss: () => dismissMentionSuggestion(editor)
6910
+ }
6911
+ )
6528
6912
  ] });
6529
6913
  }
6530
- function Toolbar({ editor }) {
6914
+ function Toolbar({ editor, mention }) {
6531
6915
  return /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-0.5 rounded-md border bg-muted/30 p-1", children: [
6532
6916
  /* @__PURE__ */ jsx(
6533
6917
  ToolbarButton,
@@ -6586,6 +6970,7 @@ function Toolbar({ editor }) {
6586
6970
  }).run()
6587
6971
  }
6588
6972
  ),
6973
+ mention && /* @__PURE__ */ jsx(ToolbarButton, { label: "Mention", active: mention.active, onClick: mention.insert, Icon: AtSign }),
6589
6974
  /* @__PURE__ */ jsx(Divider, {}),
6590
6975
  /* @__PURE__ */ jsx(
6591
6976
  ToolbarButton,
@@ -7449,6 +7834,387 @@ function ImageItemCard({
7449
7834
  ) })
7450
7835
  ] });
7451
7836
  }
7837
+ var REMOVE_BUTTON_CLASS = "flex h-6 w-6 shrink-0 items-center justify-center rounded text-muted-foreground hover:bg-destructive/10 hover:text-destructive disabled:cursor-not-allowed disabled:opacity-40";
7838
+ function CellGrid({
7839
+ label,
7840
+ help,
7841
+ rows,
7842
+ minRows,
7843
+ maxRows,
7844
+ maxColumns,
7845
+ lockFirstRow = false,
7846
+ blankCell,
7847
+ renderCell,
7848
+ renderColumnControl,
7849
+ onChange
7850
+ }) {
7851
+ const columnCount = rows[0]?.length ?? 0;
7852
+ const setCell = (rowIdx, colIdx, next) => {
7853
+ onChange(rows.map((row, r) => r === rowIdx ? row.map((cell, c) => c === colIdx ? next : cell) : row));
7854
+ };
7855
+ const addRow = () => {
7856
+ onChange([...rows, Array.from({ length: columnCount || 1 }, blankCell)]);
7857
+ };
7858
+ const removeRow = (rowIdx) => {
7859
+ if (rows.length <= minRows) {
7860
+ return;
7861
+ }
7862
+ onChange(rows.filter((_, r) => r !== rowIdx));
7863
+ };
7864
+ const addColumn = () => {
7865
+ onChange(
7866
+ rows.map((row) => [...row, blankCell()]),
7867
+ { kind: "add" }
7868
+ );
7869
+ };
7870
+ const removeColumn = (colIdx) => {
7871
+ if (columnCount <= 1) {
7872
+ return;
7873
+ }
7874
+ onChange(
7875
+ rows.map((row) => row.filter((_, c) => c !== colIdx)),
7876
+ { kind: "remove", index: colIdx }
7877
+ );
7878
+ };
7879
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3", children: [
7880
+ /* @__PURE__ */ jsx(EditorField, { label, help, children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2 overflow-x-auto", children: [
7881
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
7882
+ /* @__PURE__ */ jsx("span", { className: "w-6 shrink-0", "aria-hidden": "true" }),
7883
+ Array.from({ length: columnCount }, (_, c) => /* @__PURE__ */ jsxs("div", { className: "flex min-w-[120px] flex-1 items-center gap-1", children: [
7884
+ renderColumnControl ? renderColumnControl(c) : /* @__PURE__ */ jsxs("span", { className: "flex-1 truncate px-1.5 text-[11px] text-muted-foreground", children: [
7885
+ "Column ",
7886
+ c + 1
7887
+ ] }),
7888
+ /* @__PURE__ */ jsx(
7889
+ "button",
7890
+ {
7891
+ type: "button",
7892
+ "aria-label": `Remove column ${c + 1}`,
7893
+ onClick: () => removeColumn(c),
7894
+ disabled: columnCount <= 1,
7895
+ className: REMOVE_BUTTON_CLASS,
7896
+ children: /* @__PURE__ */ jsx(Trash2, { className: "h-3 w-3" })
7897
+ }
7898
+ )
7899
+ ] }, c))
7900
+ ] }),
7901
+ rows.map((row, r) => /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
7902
+ lockFirstRow && r === 0 ? /* @__PURE__ */ jsx("span", { className: "w-6 shrink-0", "aria-hidden": "true" }) : /* @__PURE__ */ jsx(
7903
+ "button",
7904
+ {
7905
+ type: "button",
7906
+ "aria-label": `Remove row ${r + 1}`,
7907
+ onClick: () => removeRow(r),
7908
+ disabled: rows.length <= minRows,
7909
+ className: REMOVE_BUTTON_CLASS,
7910
+ children: /* @__PURE__ */ jsx(Trash2, { className: "h-3 w-3" })
7911
+ }
7912
+ ),
7913
+ row.map((cell, c) => /* @__PURE__ */ jsx("div", { className: "flex min-w-[120px] flex-1 flex-col gap-0.5", children: renderCell(
7914
+ cell,
7915
+ { row: r, col: c, label: `Row ${r + 1}, column ${c + 1}` },
7916
+ (next) => setCell(r, c, next)
7917
+ ) }, c))
7918
+ ] }, r))
7919
+ ] }) }),
7920
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
7921
+ /* @__PURE__ */ jsxs(Button, { type: "button", size: "sm", onClick: addRow, disabled: rows.length >= maxRows, children: [
7922
+ /* @__PURE__ */ jsx(Plus, { className: "h-3.5 w-3.5" }),
7923
+ " Add row"
7924
+ ] }),
7925
+ /* @__PURE__ */ jsxs(Button, { type: "button", size: "sm", onClick: addColumn, disabled: columnCount >= maxColumns, children: [
7926
+ /* @__PURE__ */ jsx(Plus, { className: "h-3.5 w-3.5" }),
7927
+ " Add column"
7928
+ ] })
7929
+ ] })
7930
+ ] });
7931
+ }
7932
+ function CellTextInput({
7933
+ cell,
7934
+ label,
7935
+ placeholder,
7936
+ leading,
7937
+ richTextNote = "Was rich-text; editing replaces with plain text.",
7938
+ onChange
7939
+ }) {
7940
+ const input = /* @__PURE__ */ jsx(
7941
+ Input,
7942
+ {
7943
+ "aria-label": label,
7944
+ value: cell.type === "raw_text" ? cell.text : "[rich_text]",
7945
+ placeholder,
7946
+ onChange: (e) => onChange({ type: "raw_text", text: e.target.value }),
7947
+ className: "h-8 text-xs"
7948
+ }
7949
+ );
7950
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
7951
+ leading ? /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
7952
+ leading,
7953
+ input
7954
+ ] }) : input,
7955
+ cell.type === "rich_text" ? /* @__PURE__ */ jsx("span", { className: cn("text-[10px] text-muted-foreground", leading && "pl-7"), children: richTextNote }) : null
7956
+ ] });
7957
+ }
7958
+ function NumberInput({
7959
+ id,
7960
+ value,
7961
+ onCommit,
7962
+ ariaLabel,
7963
+ className
7964
+ }) {
7965
+ const [draft, setDraft] = useState(String(value));
7966
+ useEffect(() => {
7967
+ setDraft(String(value));
7968
+ }, [value]);
7969
+ return /* @__PURE__ */ jsx(
7970
+ Input,
7971
+ {
7972
+ id,
7973
+ type: "number",
7974
+ inputMode: "numeric",
7975
+ "aria-label": ariaLabel,
7976
+ value: draft,
7977
+ className,
7978
+ onChange: (e) => {
7979
+ const raw = e.target.value;
7980
+ setDraft(raw);
7981
+ if (raw === "" || raw === "-" || raw === "." || raw === "-.") {
7982
+ return;
7983
+ }
7984
+ const next = Number(raw);
7985
+ if (!Number.isNaN(next)) {
7986
+ onCommit(next);
7987
+ }
7988
+ },
7989
+ onBlur: () => {
7990
+ const next = Number(draft);
7991
+ const safe = draft.trim() === "" || Number.isNaN(next) ? 0 : next;
7992
+ onCommit(safe);
7993
+ setDraft(String(safe));
7994
+ }
7995
+ }
7996
+ );
7997
+ }
7998
+ var MAX_ROWS = 201;
7999
+ var MIN_ROWS = 2;
8000
+ var MAX_COLUMNS = 20;
8001
+ var MAX_PAGE_SIZE = 100;
8002
+ var SELECT_CLASS = "flex h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50";
8003
+ function withOptional(block, key, value) {
8004
+ const next = { ...block };
8005
+ if (value === void 0) {
8006
+ delete next[key];
8007
+ } else {
8008
+ next[key] = value;
8009
+ }
8010
+ return next;
8011
+ }
8012
+ function realignRowHeader(index, edit) {
8013
+ if (index === void 0 || edit.kind === "add" || edit.index > index) {
8014
+ return index;
8015
+ }
8016
+ return edit.index === index ? void 0 : index - 1;
8017
+ }
8018
+ function cellText(cell) {
8019
+ switch (cell?.type) {
8020
+ case "raw_text":
8021
+ return cell.text;
8022
+ case "raw_number":
8023
+ return cell.text ?? String(cell.value);
8024
+ case "action_cell":
8025
+ return cell.element.text.text;
8026
+ default:
8027
+ return "";
8028
+ }
8029
+ }
8030
+ function toNumberCell(cell) {
8031
+ const text = cell.text.trim();
8032
+ const parsed = text === "" ? Number.NaN : Number(text.replace(/,/g, ""));
8033
+ if (Number.isFinite(parsed)) {
8034
+ return String(parsed) === text ? { type: "raw_number", value: parsed } : { type: "raw_number", value: parsed, text };
8035
+ }
8036
+ return text ? { type: "raw_number", value: 0, text } : { type: "raw_number", value: 0 };
8037
+ }
8038
+ function toTextCell(cell) {
8039
+ return { type: "raw_text", text: cell.text ?? String(cell.value) };
8040
+ }
8041
+ function DataTableEditor({ block, onChange }) {
8042
+ const rows = block.rows ?? [];
8043
+ const header = rows[0] ?? [];
8044
+ const rowHeader = block.row_header_column_index ?? 0;
8045
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-4", children: [
8046
+ /* @__PURE__ */ jsx(EditorField, { label: "Caption", help: "Required. Names the table for screen readers.", htmlFor: "data-table-caption", children: /* @__PURE__ */ jsx(
8047
+ Input,
8048
+ {
8049
+ id: "data-table-caption",
8050
+ value: block.caption,
8051
+ placeholder: "e.g. Open deals by stage",
8052
+ onChange: (e) => onChange({ ...block, caption: e.target.value })
8053
+ }
8054
+ ) }),
8055
+ /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-2", children: [
8056
+ /* @__PURE__ */ jsx(
8057
+ EditorField,
8058
+ {
8059
+ label: "Rows per page",
8060
+ help: "Optional, 1-100. Slack shows 5 when empty.",
8061
+ htmlFor: "data-table-page-size",
8062
+ children: /* @__PURE__ */ jsx(
8063
+ Input,
8064
+ {
8065
+ id: "data-table-page-size",
8066
+ type: "number",
8067
+ min: 1,
8068
+ max: MAX_PAGE_SIZE,
8069
+ value: block.page_size ?? "",
8070
+ placeholder: "5",
8071
+ onChange: (e) => {
8072
+ const n = Math.trunc(Number(e.target.value));
8073
+ const next = e.target.value === "" || Number.isNaN(n) ? void 0 : Math.min(MAX_PAGE_SIZE, Math.max(1, n));
8074
+ onChange(withOptional(block, "page_size", next));
8075
+ }
8076
+ }
8077
+ )
8078
+ }
8079
+ ),
8080
+ /* @__PURE__ */ jsx(
8081
+ EditorField,
8082
+ {
8083
+ label: "Row header column",
8084
+ help: "The column that identifies each row for screen readers.",
8085
+ htmlFor: "data-table-row-header",
8086
+ children: /* @__PURE__ */ jsxs(
8087
+ "select",
8088
+ {
8089
+ id: "data-table-row-header",
8090
+ value: rowHeader,
8091
+ disabled: header.length === 0,
8092
+ onChange: (e) => {
8093
+ const index = Number(e.target.value);
8094
+ onChange(withOptional(block, "row_header_column_index", index === 0 ? void 0 : index));
8095
+ },
8096
+ className: SELECT_CLASS,
8097
+ children: [
8098
+ header.map((cell, c) => {
8099
+ const text = cellText(cell);
8100
+ return /* @__PURE__ */ jsx("option", { value: c, children: text ? `Column ${c + 1}: ${text}` : `Column ${c + 1}` }, c);
8101
+ }),
8102
+ rowHeader >= header.length ? /* @__PURE__ */ jsxs("option", { value: rowHeader, children: [
8103
+ "Column ",
8104
+ rowHeader + 1,
8105
+ " (missing)"
8106
+ ] }) : null
8107
+ ]
8108
+ }
8109
+ )
8110
+ }
8111
+ )
8112
+ ] }),
8113
+ /* @__PURE__ */ jsx(
8114
+ CellGrid,
8115
+ {
8116
+ label: "Cells",
8117
+ help: "The first row is the header. Up to 200 more rows and 20 columns. Use # to make a cell a number; a column of only numbers sorts numerically.",
8118
+ rows,
8119
+ minRows: MIN_ROWS,
8120
+ maxRows: MAX_ROWS,
8121
+ maxColumns: MAX_COLUMNS,
8122
+ lockFirstRow: true,
8123
+ blankCell: () => ({ type: "raw_text", text: "" }),
8124
+ onChange: (nextRows, edit) => {
8125
+ const next = { ...block, rows: nextRows };
8126
+ onChange(
8127
+ edit ? withOptional(next, "row_header_column_index", realignRowHeader(block.row_header_column_index, edit)) : next
8128
+ );
8129
+ },
8130
+ renderCell: (cell, position, update) => /* @__PURE__ */ jsx(DataTableCellField, { cell, position, onChange: update })
8131
+ }
8132
+ )
8133
+ ] });
8134
+ }
8135
+ function DataTableCellField({
8136
+ cell,
8137
+ position,
8138
+ onChange
8139
+ }) {
8140
+ const { row, col, label } = position;
8141
+ if (cell.type === "action_cell") {
8142
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
8143
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
8144
+ /* @__PURE__ */ jsx(ToggleSpacer, {}),
8145
+ /* @__PURE__ */ jsxs("div", { className: "flex h-8 min-w-0 flex-1 items-center gap-1.5 rounded-md border border-dashed px-2 text-xs text-muted-foreground", children: [
8146
+ /* @__PURE__ */ jsx(MousePointerClick, { className: "h-3 w-3 shrink-0", "aria-hidden": "true" }),
8147
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: cellText(cell) || "Button" })
8148
+ ] })
8149
+ ] }),
8150
+ /* @__PURE__ */ jsx("span", { className: "pl-7 text-[10px] text-muted-foreground", children: "Button cell. Edit it via View JSON." })
8151
+ ] });
8152
+ }
8153
+ if (cell.type === "raw_number") {
8154
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
8155
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
8156
+ /* @__PURE__ */ jsx(NumberToggle, { label, pressed: true, onClick: () => onChange(toTextCell(cell)) }),
8157
+ /* @__PURE__ */ jsx(
8158
+ NumberInput,
8159
+ {
8160
+ value: cell.value,
8161
+ ariaLabel: `${label} value`,
8162
+ onCommit: (value) => onChange({ ...cell, value }),
8163
+ className: "h-8 text-xs"
8164
+ }
8165
+ )
8166
+ ] }),
8167
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
8168
+ /* @__PURE__ */ jsx(ToggleSpacer, {}),
8169
+ /* @__PURE__ */ jsx(
8170
+ Input,
8171
+ {
8172
+ "aria-label": `${label} display text`,
8173
+ value: cell.text ?? "",
8174
+ placeholder: "Display as (optional)",
8175
+ onChange: (e) => {
8176
+ const { text: _omit, ...rest } = cell;
8177
+ onChange(e.target.value ? { ...rest, text: e.target.value } : rest);
8178
+ },
8179
+ className: "h-7 text-[11px]"
8180
+ }
8181
+ )
8182
+ ] })
8183
+ ] });
8184
+ }
8185
+ const isHeader = row === 0;
8186
+ return /* @__PURE__ */ jsx(
8187
+ CellTextInput,
8188
+ {
8189
+ cell,
8190
+ label,
8191
+ placeholder: isHeader ? `Header ${col + 1}` : "Cell text",
8192
+ richTextNote: isHeader ? "Header cells can't be rich text. Type to replace it." : void 0,
8193
+ leading: !isHeader && cell.type === "raw_text" ? /* @__PURE__ */ jsx(NumberToggle, { label, pressed: false, onClick: () => onChange(toNumberCell(cell)) }) : /* @__PURE__ */ jsx(ToggleSpacer, {}),
8194
+ onChange
8195
+ }
8196
+ );
8197
+ }
8198
+ function ToggleSpacer() {
8199
+ return /* @__PURE__ */ jsx("span", { className: "h-6 w-6 shrink-0", "aria-hidden": "true" });
8200
+ }
8201
+ function NumberToggle({ label, pressed, onClick }) {
8202
+ return /* @__PURE__ */ jsx(
8203
+ "button",
8204
+ {
8205
+ type: "button",
8206
+ "aria-label": `${label} as number`,
8207
+ "aria-pressed": pressed,
8208
+ title: pressed ? "Number cell. Click to make it text." : "Text cell. Click to make it a number.",
8209
+ onClick,
8210
+ className: cn(
8211
+ "flex h-6 w-6 shrink-0 items-center justify-center rounded transition-colors",
8212
+ pressed ? "bg-primary/10 text-foreground" : "text-muted-foreground hover:bg-accent hover:text-foreground"
8213
+ ),
8214
+ children: /* @__PURE__ */ jsx(Hash, { className: "h-3 w-3" })
8215
+ }
8216
+ );
8217
+ }
7452
8218
  var CHART_TYPES = ["line", "bar", "area", "pie"];
7453
8219
  function isCartesian(chart) {
7454
8220
  return chart.type !== "pie";
@@ -7503,46 +8269,6 @@ function DataVisualizationEditor({ block, onChange }) {
7503
8269
  isCartesian(chart) ? /* @__PURE__ */ jsx(CartesianFields, { chart, onChange: setChart }) : /* @__PURE__ */ jsx(PieFields, { chart, onChange: setChart })
7504
8270
  ] });
7505
8271
  }
7506
- function NumberInput({
7507
- id,
7508
- value,
7509
- onCommit,
7510
- ariaLabel,
7511
- className
7512
- }) {
7513
- const [draft, setDraft] = useState(String(value));
7514
- useEffect(() => {
7515
- setDraft(String(value));
7516
- }, [value]);
7517
- return /* @__PURE__ */ jsx(
7518
- Input,
7519
- {
7520
- id,
7521
- type: "number",
7522
- inputMode: "numeric",
7523
- "aria-label": ariaLabel,
7524
- value: draft,
7525
- className,
7526
- onChange: (e) => {
7527
- const raw = e.target.value;
7528
- setDraft(raw);
7529
- if (raw === "" || raw === "-" || raw === "." || raw === "-.") {
7530
- return;
7531
- }
7532
- const next = Number(raw);
7533
- if (!Number.isNaN(next)) {
7534
- onCommit(next);
7535
- }
7536
- },
7537
- onBlur: () => {
7538
- const next = Number(draft);
7539
- const safe = draft.trim() === "" || Number.isNaN(next) ? 0 : next;
7540
- onCommit(safe);
7541
- setDraft(String(safe));
7542
- }
7543
- }
7544
- );
7545
- }
7546
8272
  function CartesianFields({ chart, onChange }) {
7547
8273
  const series = chart.series;
7548
8274
  const categories = chart.axis_config?.categories && chart.axis_config.categories.length > 0 ? chart.axis_config.categories : series[0]?.data.map((d) => d.label) ?? [];
@@ -9085,45 +9811,13 @@ function ImageAccessoryFields({ image, onChange }) {
9085
9811
  ] });
9086
9812
  }
9087
9813
  var ALIGN_OPTIONS = ["left", "center", "right"];
9088
- function cellAsText(cell) {
9089
- if (cell.type === "raw_text") {
9090
- return cell.text;
9091
- }
9092
- return "[rich_text]";
9814
+ function alignColumnSettings(settings, edit) {
9815
+ return edit.kind === "add" ? [...settings, null] : settings.filter((_, c) => c !== edit.index);
9093
9816
  }
9094
9817
  function TableEditor({ block, onChange }) {
9095
9818
  const rows = block.rows ?? [];
9096
9819
  const columnCount = rows[0]?.length ?? 0;
9097
9820
  const columnSettings = block.column_settings ?? [];
9098
- const setCell = (rowIdx, colIdx, text) => {
9099
- const nextRows = rows.map(
9100
- (row, r) => r === rowIdx ? row.map((cell, c) => c === colIdx ? { type: "raw_text", text } : cell) : row
9101
- );
9102
- onChange({ ...block, rows: nextRows });
9103
- };
9104
- const addRow = () => {
9105
- const blankRow = Array.from({ length: columnCount || 1 }, () => ({ type: "raw_text", text: "" }));
9106
- onChange({ ...block, rows: [...rows, blankRow] });
9107
- };
9108
- const removeRow = (rowIdx) => {
9109
- if (rows.length <= 1) {
9110
- return;
9111
- }
9112
- onChange({ ...block, rows: rows.filter((_, r) => r !== rowIdx) });
9113
- };
9114
- const addColumn = () => {
9115
- const nextRows = rows.map((row) => [...row, { type: "raw_text", text: "" }]);
9116
- const nextSettings = [...columnSettings, null];
9117
- onChange({ ...block, rows: nextRows, column_settings: nextSettings });
9118
- };
9119
- const removeColumn = (colIdx) => {
9120
- if (columnCount <= 1) {
9121
- return;
9122
- }
9123
- const nextRows = rows.map((row) => row.filter((_, c) => c !== colIdx));
9124
- const nextSettings = columnSettings.filter((_, c) => c !== colIdx);
9125
- onChange({ ...block, rows: nextRows, column_settings: nextSettings });
9126
- };
9127
9821
  const setColumnAlign = (colIdx, align) => {
9128
9822
  const next = Array.from({ length: columnCount }, (_, i) => columnSettings[i] ?? null);
9129
9823
  if (align === "") {
@@ -9134,81 +9828,45 @@ function TableEditor({ block, onChange }) {
9134
9828
  }
9135
9829
  onChange({ ...block, column_settings: next });
9136
9830
  };
9137
- return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3", children: [
9138
- /* @__PURE__ */ jsx(EditorField, { label: "Cells", help: "Up to 100 rows and 20 columns. Slack allows one table per message.", children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2 overflow-x-auto", children: [
9139
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
9140
- /* @__PURE__ */ jsx("span", { className: "w-6 shrink-0", "aria-hidden": "true" }),
9141
- Array.from({ length: columnCount }, (_, c) => {
9142
- const setting = columnSettings[c];
9143
- const align = setting?.align ?? "";
9144
- return /* @__PURE__ */ jsxs("div", { className: "flex min-w-[120px] flex-1 items-center gap-1", children: [
9145
- /* @__PURE__ */ jsxs(
9146
- "select",
9147
- {
9148
- "aria-label": `Column ${c + 1} alignment`,
9149
- value: align,
9150
- onChange: (e) => setColumnAlign(c, e.target.value),
9151
- className: "h-7 flex-1 rounded-sm border border-input bg-background px-1.5 text-[11px] text-muted-foreground",
9152
- children: [
9153
- /* @__PURE__ */ jsx("option", { value: "", children: "Default" }),
9154
- ALIGN_OPTIONS.map((a) => /* @__PURE__ */ jsx("option", { value: a, children: a }, a))
9155
- ]
9156
- }
9157
- ),
9158
- /* @__PURE__ */ jsx(
9159
- "button",
9160
- {
9161
- type: "button",
9162
- "aria-label": `Remove column ${c + 1}`,
9163
- onClick: () => removeColumn(c),
9164
- disabled: columnCount <= 1,
9165
- className: "flex h-6 w-6 items-center justify-center rounded text-muted-foreground hover:bg-destructive/10 hover:text-destructive disabled:cursor-not-allowed disabled:opacity-40",
9166
- children: /* @__PURE__ */ jsx(Trash2, { className: "h-3 w-3" })
9167
- }
9168
- )
9169
- ] }, c);
9170
- })
9171
- ] }),
9172
- rows.map((row, r) => /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
9173
- /* @__PURE__ */ jsx(
9174
- "button",
9175
- {
9176
- type: "button",
9177
- "aria-label": `Remove row ${r + 1}`,
9178
- onClick: () => removeRow(r),
9179
- disabled: rows.length <= 1,
9180
- className: "flex h-6 w-6 shrink-0 items-center justify-center rounded text-muted-foreground hover:bg-destructive/10 hover:text-destructive disabled:cursor-not-allowed disabled:opacity-40",
9181
- children: /* @__PURE__ */ jsx(Trash2, { className: "h-3 w-3" })
9182
- }
9183
- ),
9184
- row.map((cell, c) => {
9185
- const isRichText = cell.type === "rich_text";
9186
- return /* @__PURE__ */ jsxs("div", { className: "flex min-w-[120px] flex-1 flex-col gap-0.5", children: [
9187
- /* @__PURE__ */ jsx(
9188
- Input,
9189
- {
9190
- value: cellAsText(cell),
9191
- placeholder: r === 0 ? `Header ${c + 1}` : "Cell text",
9192
- onChange: (e) => setCell(r, c, e.target.value),
9193
- className: "h-8 text-xs"
9194
- }
9195
- ),
9196
- isRichText ? /* @__PURE__ */ jsx("span", { className: "text-[10px] text-muted-foreground", children: "Was rich-text; editing replaces with plain text." }) : null
9197
- ] }, c);
9198
- })
9199
- ] }, r))
9200
- ] }) }),
9201
- /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
9202
- /* @__PURE__ */ jsxs(Button, { type: "button", size: "sm", onClick: addRow, disabled: rows.length >= 100, children: [
9203
- /* @__PURE__ */ jsx(Plus, { className: "h-3.5 w-3.5" }),
9204
- " Add row"
9205
- ] }),
9206
- /* @__PURE__ */ jsxs(Button, { type: "button", size: "sm", onClick: addColumn, disabled: columnCount >= 20, children: [
9207
- /* @__PURE__ */ jsx(Plus, { className: "h-3.5 w-3.5" }),
9208
- " Add column"
9209
- ] })
9210
- ] })
9211
- ] });
9831
+ return /* @__PURE__ */ jsx(
9832
+ CellGrid,
9833
+ {
9834
+ label: "Cells",
9835
+ help: "Up to 100 rows and 20 columns. Slack allows one table per message.",
9836
+ rows,
9837
+ minRows: 1,
9838
+ maxRows: 100,
9839
+ maxColumns: 20,
9840
+ blankCell: () => ({ type: "raw_text", text: "" }),
9841
+ onChange: (nextRows, edit) => onChange({
9842
+ ...block,
9843
+ rows: nextRows,
9844
+ ...edit ? { column_settings: alignColumnSettings(columnSettings, edit) } : {}
9845
+ }),
9846
+ renderColumnControl: (c) => /* @__PURE__ */ jsxs(
9847
+ "select",
9848
+ {
9849
+ "aria-label": `Column ${c + 1} alignment`,
9850
+ value: columnSettings[c]?.align ?? "",
9851
+ onChange: (e) => setColumnAlign(c, e.target.value),
9852
+ className: "h-7 flex-1 rounded-sm border border-input bg-background px-1.5 text-[11px] text-muted-foreground",
9853
+ children: [
9854
+ /* @__PURE__ */ jsx("option", { value: "", children: "Default" }),
9855
+ ALIGN_OPTIONS.map((a) => /* @__PURE__ */ jsx("option", { value: a, children: a }, a))
9856
+ ]
9857
+ }
9858
+ ),
9859
+ renderCell: (cell, { row, col, label }, update) => /* @__PURE__ */ jsx(
9860
+ CellTextInput,
9861
+ {
9862
+ cell,
9863
+ label,
9864
+ placeholder: row === 0 ? `Header ${col + 1}` : "Cell text",
9865
+ onChange: update
9866
+ }
9867
+ )
9868
+ }
9869
+ );
9212
9870
  }
9213
9871
  var STATUSES2 = ["pending", "in_progress", "complete", "error"];
9214
9872
  function TaskCardEditor({ block, onChange }) {
@@ -9549,6 +10207,8 @@ function dispatch(block, onChange) {
9549
10207
  return /* @__PURE__ */ jsx(DataVisualizationEditor, { block, onChange: (next) => onChange(next) });
9550
10208
  case "container":
9551
10209
  return /* @__PURE__ */ jsx(ContainerEditor, { block, onChange: (next) => onChange(next) });
10210
+ case "data_table":
10211
+ return /* @__PURE__ */ jsx(DataTableEditor, { block, onChange: (next) => onChange(next) });
9552
10212
  default:
9553
10213
  return null;
9554
10214
  }
@@ -9572,7 +10232,8 @@ var BLOCK_TYPE_LABELS = {
9572
10232
  plan: "Plan",
9573
10233
  task_card: "Task Card",
9574
10234
  data_visualization: "Data Visualization",
9575
- container: "Container"
10235
+ container: "Container",
10236
+ data_table: "Data Table"
9576
10237
  };
9577
10238
  function BlockRow({
9578
10239
  builderBlock,
@@ -10922,6 +11583,7 @@ function BlockKitchen(props) {
10922
11583
  onValidationChange,
10923
11584
  previewHooks,
10924
11585
  customEmojis,
11586
+ mentions,
10925
11587
  loadChannels,
10926
11588
  loadSendAsUserStatus,
10927
11589
  onSend,
@@ -11282,7 +11944,7 @@ function BlockKitchen(props) {
11282
11944
  }
11283
11945
  )
11284
11946
  ] }),
11285
- /* @__PURE__ */ jsx(
11947
+ /* @__PURE__ */ jsx(MentionsProvider, { mentions, children: /* @__PURE__ */ jsx(
11286
11948
  Surface,
11287
11949
  {
11288
11950
  blocks,
@@ -11302,7 +11964,7 @@ function BlockKitchen(props) {
11302
11964
  isPaletteDrag: activePaletteVariant !== null,
11303
11965
  onOpenPalette: () => setPaletteOpen(true)
11304
11966
  }
11305
- )
11967
+ ) })
11306
11968
  ] })
11307
11969
  ]
11308
11970
  }