@tightknitai/block-kitchen 0.10.19 → 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/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 }; }
@@ -1096,6 +1097,58 @@ var defaultPalette = [
1096
1097
  ]
1097
1098
  ]
1098
1099
  })
1100
+ },
1101
+ {
1102
+ // Slack's documented example.
1103
+ id: "data_table_basic",
1104
+ label: "Data table",
1105
+ factory: () => ({
1106
+ type: "data_table",
1107
+ caption: "A Fabulous Table",
1108
+ rows: [
1109
+ [
1110
+ { type: "raw_text", text: "Name" },
1111
+ { type: "raw_text", text: "Department" },
1112
+ { type: "raw_text", text: "Badge" }
1113
+ ],
1114
+ [
1115
+ { type: "raw_text", text: "Data Refinement Department" },
1116
+ { type: "raw_text", text: "MDR" },
1117
+ {
1118
+ type: "rich_text",
1119
+ elements: [
1120
+ { type: "rich_text_section", elements: [{ type: "text", text: "Blue", style: { bold: true } }] }
1121
+ ]
1122
+ }
1123
+ ],
1124
+ [
1125
+ { type: "raw_text", text: "Art Sourcing Department" },
1126
+ { type: "raw_text", text: "O&D" },
1127
+ {
1128
+ type: "rich_text",
1129
+ elements: [
1130
+ {
1131
+ type: "rich_text_section",
1132
+ elements: [
1133
+ { type: "text", text: "Green " },
1134
+ { type: "text", text: "review", style: { italic: true } }
1135
+ ]
1136
+ }
1137
+ ]
1138
+ }
1139
+ ],
1140
+ [
1141
+ { type: "raw_text", text: "Wellness Department" },
1142
+ { type: "raw_text", text: "Wellness Center" },
1143
+ {
1144
+ type: "rich_text",
1145
+ elements: [
1146
+ { type: "rich_text_section", elements: [{ type: "text", text: "Limited", style: { bold: true } }] }
1147
+ ]
1148
+ }
1149
+ ]
1150
+ ]
1151
+ })
1099
1152
  }
1100
1153
  ]
1101
1154
  },
@@ -1760,6 +1813,8 @@ function labelForBlockType(type) {
1760
1813
  return "Data Visualization";
1761
1814
  case "container":
1762
1815
  return "Container";
1816
+ case "data_table":
1817
+ return "Data Table";
1763
1818
  }
1764
1819
  }
1765
1820
  var Sheet = DialogPrimitive__namespace.Root;
@@ -5220,15 +5275,16 @@ function makeEmojiImportResolver(customByName, index = getEmojiIndex()) {
5220
5275
 
5221
5276
  // src/lib/rich-text-tiptap.ts
5222
5277
  var USER_MENTION_NODE = "userMention";
5278
+ var CHANNEL_MENTION_NODE = "channelMention";
5223
5279
  var SUPPORTED_BLOCK_KINDS = /* @__PURE__ */ new Set([
5224
5280
  "rich_text_section",
5225
5281
  "rich_text_list",
5226
5282
  "rich_text_quote",
5227
5283
  "rich_text_preformatted"
5228
5284
  ]);
5229
- var SUPPORTED_INLINE_KINDS = /* @__PURE__ */ new Set(["text", "link", "emoji", "user"]);
5285
+ var SUPPORTED_INLINE_KINDS = /* @__PURE__ */ new Set(["text", "link", "emoji", "user", "channel"]);
5230
5286
  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"]);
5287
+ var SUPPORTED_MENTION_STYLE_KEYS = /* @__PURE__ */ new Set(["bold", "italic", "strike", "underline"]);
5232
5288
  var MAX_LIST_INDENT = 8;
5233
5289
  function detectLossy(block) {
5234
5290
  const out = [];
@@ -5283,10 +5339,13 @@ function scanInlines(inlines, where, out) {
5283
5339
  }
5284
5340
  }
5285
5341
  }
5286
- 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
+ }
5287
5346
  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 });
5347
+ if (!SUPPORTED_MENTION_STYLE_KEYS.has(k)) {
5348
+ out.push({ label: `${inline.type === "channel" ? "Channel" : "Mention"} style: ${k}`, where: sub });
5290
5349
  }
5291
5350
  }
5292
5351
  }
@@ -5412,6 +5471,20 @@ function inlinesToPM(inlines, resolveEmoji) {
5412
5471
  node.marks = marks;
5413
5472
  }
5414
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);
5415
5488
  } else if (inline.type === "link") {
5416
5489
  const link = inline;
5417
5490
  const text = link.text || link.url || "";
@@ -5551,13 +5624,26 @@ function proseMirrorInlinesToRichTextElements(nodes) {
5551
5624
  continue;
5552
5625
  }
5553
5626
  const user = { type: "user", user_id: userId };
5554
- const style2 = userStyle(node.marks ?? []);
5627
+ const style2 = mentionStyle(node.marks ?? []);
5555
5628
  if (Object.keys(style2).length > 0) {
5556
5629
  user.style = style2;
5557
5630
  }
5558
5631
  out.push(user);
5559
5632
  continue;
5560
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
+ }
5561
5647
  if (node.type === "hardBreak") {
5562
5648
  out.push({ type: "text", text: "\n" });
5563
5649
  continue;
@@ -5612,10 +5698,10 @@ function marksToStyle(marks) {
5612
5698
  }
5613
5699
  return style;
5614
5700
  }
5615
- function userStyle(marks) {
5701
+ function mentionStyle(marks) {
5616
5702
  const style = {};
5617
5703
  for (const [key, value] of Object.entries(marksToStyle(marks))) {
5618
- if (SUPPORTED_USER_STYLE_KEYS.has(key)) {
5704
+ if (SUPPORTED_MENTION_STYLE_KEYS.has(key)) {
5619
5705
  style[key] = value;
5620
5706
  }
5621
5707
  }
@@ -5644,50 +5730,52 @@ function sameStyle(a, b) {
5644
5730
  }
5645
5731
  return ak.every((k, i) => k === bk[i]);
5646
5732
  }
5647
- function EmojiNodeView({ node }) {
5648
- const { name, src, unicode } = node.attrs;
5649
- const label = `:${name}:`;
5650
- return /* @__PURE__ */ jsxRuntime.jsx(react.NodeViewWrapper, { as: "span", className: "bk-emoji-node", "data-emoji": name, title: label, children: src ? /* @__PURE__ */ jsxRuntime.jsx(
5651
- "img",
5652
- {
5653
- src,
5654
- alt: label,
5655
- style: {
5656
- display: "inline-block",
5657
- width: "1.25em",
5658
- height: "1.25em",
5659
- verticalAlign: "-0.25em",
5660
- objectFit: "contain"
5661
- }
5662
- }
5663
- ) : /* @__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;
5664
5735
  }
5665
- var EmojiNode = react.Node.create({
5666
- name: "emoji",
5736
+ function ChannelMentionNodeView({ node }) {
5737
+ const mentions = useMentions();
5738
+ const attrs = node.attrs;
5739
+ return /* @__PURE__ */ jsxRuntime.jsxs(react.NodeViewWrapper, { as: "span", className: "bk-channel-mention", "data-channel-mention": attrs.channelId, children: [
5740
+ "#",
5741
+ channelMentionLabel(attrs, mentions?.getChannelName)
5742
+ ] });
5743
+ }
5744
+ var ChannelMentionNode = react.Node.create({
5745
+ name: CHANNEL_MENTION_NODE,
5667
5746
  group: "inline",
5668
5747
  inline: true,
5669
5748
  atom: true,
5670
5749
  selectable: true,
5671
5750
  addAttributes() {
5672
5751
  return {
5673
- name: { default: "" },
5674
- src: { default: null },
5675
- unicode: { default: null },
5676
- skinTone: { default: null }
5752
+ channelId: {
5753
+ default: "",
5754
+ parseHTML: (element) => element.getAttribute("data-channel-mention") ?? "",
5755
+ renderHTML: (attributes) => ({ "data-channel-mention": attributes.channelId })
5756
+ },
5757
+ name: {
5758
+ default: null,
5759
+ parseHTML: (element) => element.getAttribute("data-channel-name"),
5760
+ renderHTML: (attributes) => attributes.name ? { "data-channel-name": attributes.name } : {}
5761
+ }
5677
5762
  };
5678
5763
  },
5679
5764
  parseHTML() {
5680
- return [{ tag: "span[data-emoji]" }];
5765
+ return [{ tag: "span[data-channel-mention]" }];
5681
5766
  },
5682
- renderHTML({ HTMLAttributes }) {
5683
- const name = HTMLAttributes.name ?? "";
5684
- return ["span", react.mergeAttributes({ "data-emoji": name }, HTMLAttributes), `:${name}:`];
5767
+ renderHTML({ node, HTMLAttributes }) {
5768
+ return ["span", react.mergeAttributes(HTMLAttributes), `#${channelMentionLabel(node.attrs)}`];
5769
+ },
5770
+ renderText({ node }) {
5771
+ return `#${channelMentionLabel(node.attrs)}`;
5685
5772
  },
5686
5773
  addNodeView() {
5687
- return react.ReactNodeViewRenderer(EmojiNodeView);
5774
+ return react.ReactNodeViewRenderer(ChannelMentionNodeView);
5688
5775
  }
5689
5776
  });
5690
5777
  var MENTION_SEARCH_DEBOUNCE_MS = 200;
5778
+ var USER_MENTION_SUGGESTION_PLUGIN_KEY = new state.PluginKey("userMentionSuggestion");
5691
5779
  function insertUserMention(editor, range, user) {
5692
5780
  const to = editor.state.doc.textBetween(range.to, Math.min(range.to + 1, editor.state.doc.content.size)) === " " ? range.to + 1 : range.to;
5693
5781
  editor.chain().focus().insertContentAt({ from: range.from, to }, [
@@ -5696,13 +5784,16 @@ function insertUserMention(editor, range, user) {
5696
5784
  ]).run();
5697
5785
  }
5698
5786
  function insertMentionTrigger(editor) {
5787
+ insertSuggestionTrigger(editor, "@");
5788
+ }
5789
+ function insertSuggestionTrigger(editor, char) {
5699
5790
  const { from } = editor.state.selection;
5700
5791
  const before = from > 1 ? editor.state.doc.textBetween(from - 1, from, void 0, "\uFFFC") : "";
5701
5792
  const needsSpace = before !== "" && !/\s/.test(before);
5702
- editor.chain().focus().insertContent(needsSpace ? " @" : "@").run();
5793
+ editor.chain().focus().insertContent(needsSpace ? ` ${char}` : char).run();
5703
5794
  }
5704
5795
  function dismissMentionSuggestion(editor) {
5705
- suggestion.exitSuggestion(editor.view);
5796
+ suggestion.exitSuggestion(editor.view, USER_MENTION_SUGGESTION_PLUGIN_KEY);
5706
5797
  }
5707
5798
  var MentionSuggestion = react.Extension.create({
5708
5799
  name: "userMentionSuggestion",
@@ -5718,6 +5809,7 @@ var MentionSuggestion = react.Extension.create({
5718
5809
  return [
5719
5810
  suggestion.Suggestion({
5720
5811
  editor: this.editor,
5812
+ pluginKey: USER_MENTION_SUGGESTION_PLUGIN_KEY,
5721
5813
  char: "@",
5722
5814
  debounce: MENTION_SEARCH_DEBOUNCE_MS,
5723
5815
  allow: () => isEnabled(),
@@ -5728,6 +5820,90 @@ var MentionSuggestion = react.Extension.create({
5728
5820
  ];
5729
5821
  }
5730
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
+ });
5731
5907
  function userMentionLabel(attrs, getUserName) {
5732
5908
  return getUserName?.(attrs.userId) ?? attrs.name ?? attrs.userId;
5733
5909
  }
@@ -6063,6 +6239,7 @@ function nextMentionPickerState(props, prev) {
6063
6239
  }
6064
6240
  function MentionPicker({
6065
6241
  editor,
6242
+ kind,
6066
6243
  state,
6067
6244
  keyHandlerRef,
6068
6245
  onDismiss
@@ -6075,6 +6252,27 @@ function MentionPicker({
6075
6252
  setActiveIndex(0);
6076
6253
  }
6077
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]);
6078
6276
  React.useEffect(() => {
6079
6277
  keyHandlerRef.current = (event) => {
6080
6278
  if (event.key === "ArrowDown" || event.key === "ArrowUp") {
@@ -6085,11 +6283,11 @@ function MentionPicker({
6085
6283
  return true;
6086
6284
  }
6087
6285
  if (event.key === "Enter" || event.key === "Tab") {
6088
- const user = items[activeIndex];
6089
- if (!user) {
6286
+ const item = items[activeIndex];
6287
+ if (!item) {
6090
6288
  return false;
6091
6289
  }
6092
- command(user);
6290
+ command(item);
6093
6291
  return true;
6094
6292
  }
6095
6293
  return false;
@@ -6136,29 +6334,39 @@ function MentionPicker({
6136
6334
  e.preventDefault();
6137
6335
  }
6138
6336
  },
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
- )) })
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
+ )
6162
6370
  }
6163
6371
  )
6164
6372
  ]
@@ -6755,7 +6963,7 @@ function LossyBanner({ reasons }) {
6755
6963
  ] });
6756
6964
  }
6757
6965
  var RICH_TEXT_INPUT_RULE_ALLOWLIST = ["bold", "italic", "strike", "code"];
6758
- function buildRichTextExtensions(mentionSuggestion) {
6966
+ function buildRichTextExtensions(mentionSuggestion, channelSuggestion) {
6759
6967
  return [
6760
6968
  StarterKit__default.default.configure({
6761
6969
  heading: false,
@@ -6789,10 +6997,11 @@ function buildRichTextExtensions(mentionSuggestion) {
6789
6997
  shouldAutoLink: hasExplicitSafeScheme,
6790
6998
  HTMLAttributes: { rel: "noreferrer noopener", target: "_blank" }
6791
6999
  }),
6792
- // Always registered, so mentions already in the content round-trip with
6793
- // or without the picker.
7000
+ // Always registered, so mentions already in the content round-trip with or without pickers.
6794
7001
  UserMentionNode,
6795
- ...mentionSuggestion ? [MentionSuggestion.configure(mentionSuggestion)] : []
7002
+ ChannelMentionNode,
7003
+ ...mentionSuggestion ? [MentionSuggestion.configure(mentionSuggestion)] : [],
7004
+ ...channelSuggestion ? [ChannelSuggestion.configure(channelSuggestion)] : []
6796
7005
  ];
6797
7006
  }
6798
7007
  function RichTextWysiwygEditor({ block, onChange }) {
@@ -6820,24 +7029,39 @@ function RichTextWysiwygEditor({ block, onChange }) {
6820
7029
  [onChange]
6821
7030
  );
6822
7031
  const mentions = useMentions();
6823
- const mentionsEnabled = mentions !== null;
7032
+ const userMentionsEnabled = mentions !== null;
7033
+ const channelMentionsEnabled = mentions?.searchChannels !== void 0;
6824
7034
  const mentionsRef = React.useRef(mentions);
6825
7035
  React.useEffect(() => {
6826
7036
  mentionsRef.current = mentions;
6827
7037
  }, [mentions]);
6828
7038
  const [mentionPicker, setMentionPicker] = React.useState(null);
6829
7039
  const mentionKeyHandlerRef = React.useRef(() => false);
7040
+ const [channelPicker, setChannelPicker] = React.useState(null);
7041
+ const channelKeyHandlerRef = React.useRef(() => false);
6830
7042
  const editor = react.useEditor({
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
- }),
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
+ ),
6841
7065
  enableInputRules: RICH_TEXT_INPUT_RULE_ALLOWLIST,
6842
7066
  content: richTextToProseMirror(block, resolveEmoji),
6843
7067
  editorProps: {
@@ -6872,7 +7096,8 @@ function RichTextWysiwygEditor({ block, onChange }) {
6872
7096
  Toolbar,
6873
7097
  {
6874
7098
  editor,
6875
- mention: mentionsEnabled ? { active: mentionPicker !== null, insert: () => insertMentionTrigger(editor) } : null
7099
+ mention: userMentionsEnabled ? { active: mentionPicker !== null, insert: () => insertMentionTrigger(editor) } : null,
7100
+ channel: channelMentionsEnabled ? { active: channelPicker !== null, insert: () => insertChannelTrigger(editor) } : null
6876
7101
  }
6877
7102
  ),
6878
7103
  /* @__PURE__ */ jsxRuntime.jsx(react.EditorContent, { editor }),
@@ -6880,14 +7105,29 @@ function RichTextWysiwygEditor({ block, onChange }) {
6880
7105
  MentionPicker,
6881
7106
  {
6882
7107
  editor,
7108
+ kind: "user",
6883
7109
  state: mentionPicker,
6884
7110
  keyHandlerRef: mentionKeyHandlerRef,
6885
7111
  onDismiss: () => dismissMentionSuggestion(editor)
6886
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
+ }
6887
7123
  )
6888
7124
  ] });
6889
7125
  }
6890
- function Toolbar({ editor, mention }) {
7126
+ function Toolbar({
7127
+ editor,
7128
+ mention,
7129
+ channel
7130
+ }) {
6891
7131
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-center gap-0.5 rounded-md border bg-muted/30 p-1", children: [
6892
7132
  /* @__PURE__ */ jsxRuntime.jsx(
6893
7133
  ToolbarButton,
@@ -6947,6 +7187,7 @@ function Toolbar({ editor, mention }) {
6947
7187
  }
6948
7188
  ),
6949
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 }),
6950
7191
  /* @__PURE__ */ jsxRuntime.jsx(Divider, {}),
6951
7192
  /* @__PURE__ */ jsxRuntime.jsx(
6952
7193
  ToolbarButton,
@@ -7810,6 +8051,387 @@ function ImageItemCard({
7810
8051
  ) })
7811
8052
  ] });
7812
8053
  }
8054
+ 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";
8055
+ function CellGrid({
8056
+ label,
8057
+ help,
8058
+ rows,
8059
+ minRows,
8060
+ maxRows,
8061
+ maxColumns,
8062
+ lockFirstRow = false,
8063
+ blankCell,
8064
+ renderCell,
8065
+ renderColumnControl,
8066
+ onChange
8067
+ }) {
8068
+ const columnCount = rows[0]?.length ?? 0;
8069
+ const setCell = (rowIdx, colIdx, next) => {
8070
+ onChange(rows.map((row, r) => r === rowIdx ? row.map((cell, c) => c === colIdx ? next : cell) : row));
8071
+ };
8072
+ const addRow = () => {
8073
+ onChange([...rows, Array.from({ length: columnCount || 1 }, blankCell)]);
8074
+ };
8075
+ const removeRow = (rowIdx) => {
8076
+ if (rows.length <= minRows) {
8077
+ return;
8078
+ }
8079
+ onChange(rows.filter((_, r) => r !== rowIdx));
8080
+ };
8081
+ const addColumn = () => {
8082
+ onChange(
8083
+ rows.map((row) => [...row, blankCell()]),
8084
+ { kind: "add" }
8085
+ );
8086
+ };
8087
+ const removeColumn = (colIdx) => {
8088
+ if (columnCount <= 1) {
8089
+ return;
8090
+ }
8091
+ onChange(
8092
+ rows.map((row) => row.filter((_, c) => c !== colIdx)),
8093
+ { kind: "remove", index: colIdx }
8094
+ );
8095
+ };
8096
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-3", children: [
8097
+ /* @__PURE__ */ jsxRuntime.jsx(EditorField, { label, help, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-2 overflow-x-auto", children: [
8098
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [
8099
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "w-6 shrink-0", "aria-hidden": "true" }),
8100
+ Array.from({ length: columnCount }, (_, c) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex min-w-[120px] flex-1 items-center gap-1", children: [
8101
+ renderColumnControl ? renderColumnControl(c) : /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex-1 truncate px-1.5 text-[11px] text-muted-foreground", children: [
8102
+ "Column ",
8103
+ c + 1
8104
+ ] }),
8105
+ /* @__PURE__ */ jsxRuntime.jsx(
8106
+ "button",
8107
+ {
8108
+ type: "button",
8109
+ "aria-label": `Remove column ${c + 1}`,
8110
+ onClick: () => removeColumn(c),
8111
+ disabled: columnCount <= 1,
8112
+ className: REMOVE_BUTTON_CLASS,
8113
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Trash2, { className: "h-3 w-3" })
8114
+ }
8115
+ )
8116
+ ] }, c))
8117
+ ] }),
8118
+ rows.map((row, r) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [
8119
+ lockFirstRow && r === 0 ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "w-6 shrink-0", "aria-hidden": "true" }) : /* @__PURE__ */ jsxRuntime.jsx(
8120
+ "button",
8121
+ {
8122
+ type: "button",
8123
+ "aria-label": `Remove row ${r + 1}`,
8124
+ onClick: () => removeRow(r),
8125
+ disabled: rows.length <= minRows,
8126
+ className: REMOVE_BUTTON_CLASS,
8127
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Trash2, { className: "h-3 w-3" })
8128
+ }
8129
+ ),
8130
+ row.map((cell, c) => /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex min-w-[120px] flex-1 flex-col gap-0.5", children: renderCell(
8131
+ cell,
8132
+ { row: r, col: c, label: `Row ${r + 1}, column ${c + 1}` },
8133
+ (next) => setCell(r, c, next)
8134
+ ) }, c))
8135
+ ] }, r))
8136
+ ] }) }),
8137
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-2", children: [
8138
+ /* @__PURE__ */ jsxRuntime.jsxs(Button, { type: "button", size: "sm", onClick: addRow, disabled: rows.length >= maxRows, children: [
8139
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Plus, { className: "h-3.5 w-3.5" }),
8140
+ " Add row"
8141
+ ] }),
8142
+ /* @__PURE__ */ jsxRuntime.jsxs(Button, { type: "button", size: "sm", onClick: addColumn, disabled: columnCount >= maxColumns, children: [
8143
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Plus, { className: "h-3.5 w-3.5" }),
8144
+ " Add column"
8145
+ ] })
8146
+ ] })
8147
+ ] });
8148
+ }
8149
+ function CellTextInput({
8150
+ cell,
8151
+ label,
8152
+ placeholder,
8153
+ leading,
8154
+ richTextNote = "Was rich-text; editing replaces with plain text.",
8155
+ onChange
8156
+ }) {
8157
+ const input = /* @__PURE__ */ jsxRuntime.jsx(
8158
+ Input,
8159
+ {
8160
+ "aria-label": label,
8161
+ value: cell.type === "raw_text" ? cell.text : "[rich_text]",
8162
+ placeholder,
8163
+ onChange: (e) => onChange({ type: "raw_text", text: e.target.value }),
8164
+ className: "h-8 text-xs"
8165
+ }
8166
+ );
8167
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
8168
+ leading ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [
8169
+ leading,
8170
+ input
8171
+ ] }) : input,
8172
+ cell.type === "rich_text" ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: cn("text-[10px] text-muted-foreground", leading && "pl-7"), children: richTextNote }) : null
8173
+ ] });
8174
+ }
8175
+ function NumberInput({
8176
+ id,
8177
+ value,
8178
+ onCommit,
8179
+ ariaLabel,
8180
+ className
8181
+ }) {
8182
+ const [draft, setDraft] = React.useState(String(value));
8183
+ React.useEffect(() => {
8184
+ setDraft(String(value));
8185
+ }, [value]);
8186
+ return /* @__PURE__ */ jsxRuntime.jsx(
8187
+ Input,
8188
+ {
8189
+ id,
8190
+ type: "number",
8191
+ inputMode: "numeric",
8192
+ "aria-label": ariaLabel,
8193
+ value: draft,
8194
+ className,
8195
+ onChange: (e) => {
8196
+ const raw = e.target.value;
8197
+ setDraft(raw);
8198
+ if (raw === "" || raw === "-" || raw === "." || raw === "-.") {
8199
+ return;
8200
+ }
8201
+ const next = Number(raw);
8202
+ if (!Number.isNaN(next)) {
8203
+ onCommit(next);
8204
+ }
8205
+ },
8206
+ onBlur: () => {
8207
+ const next = Number(draft);
8208
+ const safe = draft.trim() === "" || Number.isNaN(next) ? 0 : next;
8209
+ onCommit(safe);
8210
+ setDraft(String(safe));
8211
+ }
8212
+ }
8213
+ );
8214
+ }
8215
+ var MAX_ROWS = 201;
8216
+ var MIN_ROWS = 2;
8217
+ var MAX_COLUMNS = 20;
8218
+ var MAX_PAGE_SIZE = 100;
8219
+ 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";
8220
+ function withOptional(block, key, value) {
8221
+ const next = { ...block };
8222
+ if (value === void 0) {
8223
+ delete next[key];
8224
+ } else {
8225
+ next[key] = value;
8226
+ }
8227
+ return next;
8228
+ }
8229
+ function realignRowHeader(index, edit) {
8230
+ if (index === void 0 || edit.kind === "add" || edit.index > index) {
8231
+ return index;
8232
+ }
8233
+ return edit.index === index ? void 0 : index - 1;
8234
+ }
8235
+ function cellText(cell) {
8236
+ switch (cell?.type) {
8237
+ case "raw_text":
8238
+ return cell.text;
8239
+ case "raw_number":
8240
+ return cell.text ?? String(cell.value);
8241
+ case "action_cell":
8242
+ return cell.element.text.text;
8243
+ default:
8244
+ return "";
8245
+ }
8246
+ }
8247
+ function toNumberCell(cell) {
8248
+ const text = cell.text.trim();
8249
+ const parsed = text === "" ? Number.NaN : Number(text.replace(/,/g, ""));
8250
+ if (Number.isFinite(parsed)) {
8251
+ return String(parsed) === text ? { type: "raw_number", value: parsed } : { type: "raw_number", value: parsed, text };
8252
+ }
8253
+ return text ? { type: "raw_number", value: 0, text } : { type: "raw_number", value: 0 };
8254
+ }
8255
+ function toTextCell(cell) {
8256
+ return { type: "raw_text", text: cell.text ?? String(cell.value) };
8257
+ }
8258
+ function DataTableEditor({ block, onChange }) {
8259
+ const rows = block.rows ?? [];
8260
+ const header = rows[0] ?? [];
8261
+ const rowHeader = block.row_header_column_index ?? 0;
8262
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-4", children: [
8263
+ /* @__PURE__ */ jsxRuntime.jsx(EditorField, { label: "Caption", help: "Required. Names the table for screen readers.", htmlFor: "data-table-caption", children: /* @__PURE__ */ jsxRuntime.jsx(
8264
+ Input,
8265
+ {
8266
+ id: "data-table-caption",
8267
+ value: block.caption,
8268
+ placeholder: "e.g. Open deals by stage",
8269
+ onChange: (e) => onChange({ ...block, caption: e.target.value })
8270
+ }
8271
+ ) }),
8272
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-2 gap-2", children: [
8273
+ /* @__PURE__ */ jsxRuntime.jsx(
8274
+ EditorField,
8275
+ {
8276
+ label: "Rows per page",
8277
+ help: "Optional, 1-100. Slack shows 5 when empty.",
8278
+ htmlFor: "data-table-page-size",
8279
+ children: /* @__PURE__ */ jsxRuntime.jsx(
8280
+ Input,
8281
+ {
8282
+ id: "data-table-page-size",
8283
+ type: "number",
8284
+ min: 1,
8285
+ max: MAX_PAGE_SIZE,
8286
+ value: block.page_size ?? "",
8287
+ placeholder: "5",
8288
+ onChange: (e) => {
8289
+ const n = Math.trunc(Number(e.target.value));
8290
+ const next = e.target.value === "" || Number.isNaN(n) ? void 0 : Math.min(MAX_PAGE_SIZE, Math.max(1, n));
8291
+ onChange(withOptional(block, "page_size", next));
8292
+ }
8293
+ }
8294
+ )
8295
+ }
8296
+ ),
8297
+ /* @__PURE__ */ jsxRuntime.jsx(
8298
+ EditorField,
8299
+ {
8300
+ label: "Row header column",
8301
+ help: "The column that identifies each row for screen readers.",
8302
+ htmlFor: "data-table-row-header",
8303
+ children: /* @__PURE__ */ jsxRuntime.jsxs(
8304
+ "select",
8305
+ {
8306
+ id: "data-table-row-header",
8307
+ value: rowHeader,
8308
+ disabled: header.length === 0,
8309
+ onChange: (e) => {
8310
+ const index = Number(e.target.value);
8311
+ onChange(withOptional(block, "row_header_column_index", index === 0 ? void 0 : index));
8312
+ },
8313
+ className: SELECT_CLASS,
8314
+ children: [
8315
+ header.map((cell, c) => {
8316
+ const text = cellText(cell);
8317
+ return /* @__PURE__ */ jsxRuntime.jsx("option", { value: c, children: text ? `Column ${c + 1}: ${text}` : `Column ${c + 1}` }, c);
8318
+ }),
8319
+ rowHeader >= header.length ? /* @__PURE__ */ jsxRuntime.jsxs("option", { value: rowHeader, children: [
8320
+ "Column ",
8321
+ rowHeader + 1,
8322
+ " (missing)"
8323
+ ] }) : null
8324
+ ]
8325
+ }
8326
+ )
8327
+ }
8328
+ )
8329
+ ] }),
8330
+ /* @__PURE__ */ jsxRuntime.jsx(
8331
+ CellGrid,
8332
+ {
8333
+ label: "Cells",
8334
+ 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.",
8335
+ rows,
8336
+ minRows: MIN_ROWS,
8337
+ maxRows: MAX_ROWS,
8338
+ maxColumns: MAX_COLUMNS,
8339
+ lockFirstRow: true,
8340
+ blankCell: () => ({ type: "raw_text", text: "" }),
8341
+ onChange: (nextRows, edit) => {
8342
+ const next = { ...block, rows: nextRows };
8343
+ onChange(
8344
+ edit ? withOptional(next, "row_header_column_index", realignRowHeader(block.row_header_column_index, edit)) : next
8345
+ );
8346
+ },
8347
+ renderCell: (cell, position, update) => /* @__PURE__ */ jsxRuntime.jsx(DataTableCellField, { cell, position, onChange: update })
8348
+ }
8349
+ )
8350
+ ] });
8351
+ }
8352
+ function DataTableCellField({
8353
+ cell,
8354
+ position,
8355
+ onChange
8356
+ }) {
8357
+ const { row, col, label } = position;
8358
+ if (cell.type === "action_cell") {
8359
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
8360
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [
8361
+ /* @__PURE__ */ jsxRuntime.jsx(ToggleSpacer, {}),
8362
+ /* @__PURE__ */ jsxRuntime.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: [
8363
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.MousePointerClick, { className: "h-3 w-3 shrink-0", "aria-hidden": "true" }),
8364
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: cellText(cell) || "Button" })
8365
+ ] })
8366
+ ] }),
8367
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "pl-7 text-[10px] text-muted-foreground", children: "Button cell. Edit it via View JSON." })
8368
+ ] });
8369
+ }
8370
+ if (cell.type === "raw_number") {
8371
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
8372
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [
8373
+ /* @__PURE__ */ jsxRuntime.jsx(NumberToggle, { label, pressed: true, onClick: () => onChange(toTextCell(cell)) }),
8374
+ /* @__PURE__ */ jsxRuntime.jsx(
8375
+ NumberInput,
8376
+ {
8377
+ value: cell.value,
8378
+ ariaLabel: `${label} value`,
8379
+ onCommit: (value) => onChange({ ...cell, value }),
8380
+ className: "h-8 text-xs"
8381
+ }
8382
+ )
8383
+ ] }),
8384
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [
8385
+ /* @__PURE__ */ jsxRuntime.jsx(ToggleSpacer, {}),
8386
+ /* @__PURE__ */ jsxRuntime.jsx(
8387
+ Input,
8388
+ {
8389
+ "aria-label": `${label} display text`,
8390
+ value: cell.text ?? "",
8391
+ placeholder: "Display as (optional)",
8392
+ onChange: (e) => {
8393
+ const { text: _omit, ...rest } = cell;
8394
+ onChange(e.target.value ? { ...rest, text: e.target.value } : rest);
8395
+ },
8396
+ className: "h-7 text-[11px]"
8397
+ }
8398
+ )
8399
+ ] })
8400
+ ] });
8401
+ }
8402
+ const isHeader = row === 0;
8403
+ return /* @__PURE__ */ jsxRuntime.jsx(
8404
+ CellTextInput,
8405
+ {
8406
+ cell,
8407
+ label,
8408
+ placeholder: isHeader ? `Header ${col + 1}` : "Cell text",
8409
+ richTextNote: isHeader ? "Header cells can't be rich text. Type to replace it." : void 0,
8410
+ leading: !isHeader && cell.type === "raw_text" ? /* @__PURE__ */ jsxRuntime.jsx(NumberToggle, { label, pressed: false, onClick: () => onChange(toNumberCell(cell)) }) : /* @__PURE__ */ jsxRuntime.jsx(ToggleSpacer, {}),
8411
+ onChange
8412
+ }
8413
+ );
8414
+ }
8415
+ function ToggleSpacer() {
8416
+ return /* @__PURE__ */ jsxRuntime.jsx("span", { className: "h-6 w-6 shrink-0", "aria-hidden": "true" });
8417
+ }
8418
+ function NumberToggle({ label, pressed, onClick }) {
8419
+ return /* @__PURE__ */ jsxRuntime.jsx(
8420
+ "button",
8421
+ {
8422
+ type: "button",
8423
+ "aria-label": `${label} as number`,
8424
+ "aria-pressed": pressed,
8425
+ title: pressed ? "Number cell. Click to make it text." : "Text cell. Click to make it a number.",
8426
+ onClick,
8427
+ className: cn(
8428
+ "flex h-6 w-6 shrink-0 items-center justify-center rounded transition-colors",
8429
+ pressed ? "bg-primary/10 text-foreground" : "text-muted-foreground hover:bg-accent hover:text-foreground"
8430
+ ),
8431
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Hash, { className: "h-3 w-3" })
8432
+ }
8433
+ );
8434
+ }
7813
8435
  var CHART_TYPES = ["line", "bar", "area", "pie"];
7814
8436
  function isCartesian(chart) {
7815
8437
  return chart.type !== "pie";
@@ -7864,46 +8486,6 @@ function DataVisualizationEditor({ block, onChange }) {
7864
8486
  isCartesian(chart) ? /* @__PURE__ */ jsxRuntime.jsx(CartesianFields, { chart, onChange: setChart }) : /* @__PURE__ */ jsxRuntime.jsx(PieFields, { chart, onChange: setChart })
7865
8487
  ] });
7866
8488
  }
7867
- function NumberInput({
7868
- id,
7869
- value,
7870
- onCommit,
7871
- ariaLabel,
7872
- className
7873
- }) {
7874
- const [draft, setDraft] = React.useState(String(value));
7875
- React.useEffect(() => {
7876
- setDraft(String(value));
7877
- }, [value]);
7878
- return /* @__PURE__ */ jsxRuntime.jsx(
7879
- Input,
7880
- {
7881
- id,
7882
- type: "number",
7883
- inputMode: "numeric",
7884
- "aria-label": ariaLabel,
7885
- value: draft,
7886
- className,
7887
- onChange: (e) => {
7888
- const raw = e.target.value;
7889
- setDraft(raw);
7890
- if (raw === "" || raw === "-" || raw === "." || raw === "-.") {
7891
- return;
7892
- }
7893
- const next = Number(raw);
7894
- if (!Number.isNaN(next)) {
7895
- onCommit(next);
7896
- }
7897
- },
7898
- onBlur: () => {
7899
- const next = Number(draft);
7900
- const safe = draft.trim() === "" || Number.isNaN(next) ? 0 : next;
7901
- onCommit(safe);
7902
- setDraft(String(safe));
7903
- }
7904
- }
7905
- );
7906
- }
7907
8489
  function CartesianFields({ chart, onChange }) {
7908
8490
  const series = chart.series;
7909
8491
  const categories = chart.axis_config?.categories && chart.axis_config.categories.length > 0 ? chart.axis_config.categories : series[0]?.data.map((d) => d.label) ?? [];
@@ -9446,45 +10028,13 @@ function ImageAccessoryFields({ image, onChange }) {
9446
10028
  ] });
9447
10029
  }
9448
10030
  var ALIGN_OPTIONS = ["left", "center", "right"];
9449
- function cellAsText(cell) {
9450
- if (cell.type === "raw_text") {
9451
- return cell.text;
9452
- }
9453
- return "[rich_text]";
10031
+ function alignColumnSettings(settings, edit) {
10032
+ return edit.kind === "add" ? [...settings, null] : settings.filter((_, c) => c !== edit.index);
9454
10033
  }
9455
10034
  function TableEditor({ block, onChange }) {
9456
10035
  const rows = block.rows ?? [];
9457
10036
  const columnCount = rows[0]?.length ?? 0;
9458
10037
  const columnSettings = block.column_settings ?? [];
9459
- const setCell = (rowIdx, colIdx, text) => {
9460
- const nextRows = rows.map(
9461
- (row, r) => r === rowIdx ? row.map((cell, c) => c === colIdx ? { type: "raw_text", text } : cell) : row
9462
- );
9463
- onChange({ ...block, rows: nextRows });
9464
- };
9465
- const addRow = () => {
9466
- const blankRow = Array.from({ length: columnCount || 1 }, () => ({ type: "raw_text", text: "" }));
9467
- onChange({ ...block, rows: [...rows, blankRow] });
9468
- };
9469
- const removeRow = (rowIdx) => {
9470
- if (rows.length <= 1) {
9471
- return;
9472
- }
9473
- onChange({ ...block, rows: rows.filter((_, r) => r !== rowIdx) });
9474
- };
9475
- const addColumn = () => {
9476
- const nextRows = rows.map((row) => [...row, { type: "raw_text", text: "" }]);
9477
- const nextSettings = [...columnSettings, null];
9478
- onChange({ ...block, rows: nextRows, column_settings: nextSettings });
9479
- };
9480
- const removeColumn = (colIdx) => {
9481
- if (columnCount <= 1) {
9482
- return;
9483
- }
9484
- const nextRows = rows.map((row) => row.filter((_, c) => c !== colIdx));
9485
- const nextSettings = columnSettings.filter((_, c) => c !== colIdx);
9486
- onChange({ ...block, rows: nextRows, column_settings: nextSettings });
9487
- };
9488
10038
  const setColumnAlign = (colIdx, align) => {
9489
10039
  const next = Array.from({ length: columnCount }, (_, i) => columnSettings[i] ?? null);
9490
10040
  if (align === "") {
@@ -9495,81 +10045,45 @@ function TableEditor({ block, onChange }) {
9495
10045
  }
9496
10046
  onChange({ ...block, column_settings: next });
9497
10047
  };
9498
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-3", children: [
9499
- /* @__PURE__ */ jsxRuntime.jsx(EditorField, { label: "Cells", help: "Up to 100 rows and 20 columns. Slack allows one table per message.", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-2 overflow-x-auto", children: [
9500
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [
9501
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "w-6 shrink-0", "aria-hidden": "true" }),
9502
- Array.from({ length: columnCount }, (_, c) => {
9503
- const setting = columnSettings[c];
9504
- const align = setting?.align ?? "";
9505
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex min-w-[120px] flex-1 items-center gap-1", children: [
9506
- /* @__PURE__ */ jsxRuntime.jsxs(
9507
- "select",
9508
- {
9509
- "aria-label": `Column ${c + 1} alignment`,
9510
- value: align,
9511
- onChange: (e) => setColumnAlign(c, e.target.value),
9512
- className: "h-7 flex-1 rounded-sm border border-input bg-background px-1.5 text-[11px] text-muted-foreground",
9513
- children: [
9514
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "", children: "Default" }),
9515
- ALIGN_OPTIONS.map((a) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: a, children: a }, a))
9516
- ]
9517
- }
9518
- ),
9519
- /* @__PURE__ */ jsxRuntime.jsx(
9520
- "button",
9521
- {
9522
- type: "button",
9523
- "aria-label": `Remove column ${c + 1}`,
9524
- onClick: () => removeColumn(c),
9525
- disabled: columnCount <= 1,
9526
- 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",
9527
- children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Trash2, { className: "h-3 w-3" })
9528
- }
9529
- )
9530
- ] }, c);
9531
- })
9532
- ] }),
9533
- rows.map((row, r) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [
9534
- /* @__PURE__ */ jsxRuntime.jsx(
9535
- "button",
9536
- {
9537
- type: "button",
9538
- "aria-label": `Remove row ${r + 1}`,
9539
- onClick: () => removeRow(r),
9540
- disabled: rows.length <= 1,
9541
- 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",
9542
- children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Trash2, { className: "h-3 w-3" })
9543
- }
9544
- ),
9545
- row.map((cell, c) => {
9546
- const isRichText = cell.type === "rich_text";
9547
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex min-w-[120px] flex-1 flex-col gap-0.5", children: [
9548
- /* @__PURE__ */ jsxRuntime.jsx(
9549
- Input,
9550
- {
9551
- value: cellAsText(cell),
9552
- placeholder: r === 0 ? `Header ${c + 1}` : "Cell text",
9553
- onChange: (e) => setCell(r, c, e.target.value),
9554
- className: "h-8 text-xs"
9555
- }
9556
- ),
9557
- isRichText ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[10px] text-muted-foreground", children: "Was rich-text; editing replaces with plain text." }) : null
9558
- ] }, c);
9559
- })
9560
- ] }, r))
9561
- ] }) }),
9562
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-2", children: [
9563
- /* @__PURE__ */ jsxRuntime.jsxs(Button, { type: "button", size: "sm", onClick: addRow, disabled: rows.length >= 100, children: [
9564
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Plus, { className: "h-3.5 w-3.5" }),
9565
- " Add row"
9566
- ] }),
9567
- /* @__PURE__ */ jsxRuntime.jsxs(Button, { type: "button", size: "sm", onClick: addColumn, disabled: columnCount >= 20, children: [
9568
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Plus, { className: "h-3.5 w-3.5" }),
9569
- " Add column"
9570
- ] })
9571
- ] })
9572
- ] });
10048
+ return /* @__PURE__ */ jsxRuntime.jsx(
10049
+ CellGrid,
10050
+ {
10051
+ label: "Cells",
10052
+ help: "Up to 100 rows and 20 columns. Slack allows one table per message.",
10053
+ rows,
10054
+ minRows: 1,
10055
+ maxRows: 100,
10056
+ maxColumns: 20,
10057
+ blankCell: () => ({ type: "raw_text", text: "" }),
10058
+ onChange: (nextRows, edit) => onChange({
10059
+ ...block,
10060
+ rows: nextRows,
10061
+ ...edit ? { column_settings: alignColumnSettings(columnSettings, edit) } : {}
10062
+ }),
10063
+ renderColumnControl: (c) => /* @__PURE__ */ jsxRuntime.jsxs(
10064
+ "select",
10065
+ {
10066
+ "aria-label": `Column ${c + 1} alignment`,
10067
+ value: columnSettings[c]?.align ?? "",
10068
+ onChange: (e) => setColumnAlign(c, e.target.value),
10069
+ className: "h-7 flex-1 rounded-sm border border-input bg-background px-1.5 text-[11px] text-muted-foreground",
10070
+ children: [
10071
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "", children: "Default" }),
10072
+ ALIGN_OPTIONS.map((a) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: a, children: a }, a))
10073
+ ]
10074
+ }
10075
+ ),
10076
+ renderCell: (cell, { row, col, label }, update) => /* @__PURE__ */ jsxRuntime.jsx(
10077
+ CellTextInput,
10078
+ {
10079
+ cell,
10080
+ label,
10081
+ placeholder: row === 0 ? `Header ${col + 1}` : "Cell text",
10082
+ onChange: update
10083
+ }
10084
+ )
10085
+ }
10086
+ );
9573
10087
  }
9574
10088
  var STATUSES2 = ["pending", "in_progress", "complete", "error"];
9575
10089
  function TaskCardEditor({ block, onChange }) {
@@ -9910,6 +10424,8 @@ function dispatch(block, onChange) {
9910
10424
  return /* @__PURE__ */ jsxRuntime.jsx(DataVisualizationEditor, { block, onChange: (next) => onChange(next) });
9911
10425
  case "container":
9912
10426
  return /* @__PURE__ */ jsxRuntime.jsx(ContainerEditor, { block, onChange: (next) => onChange(next) });
10427
+ case "data_table":
10428
+ return /* @__PURE__ */ jsxRuntime.jsx(DataTableEditor, { block, onChange: (next) => onChange(next) });
9913
10429
  default:
9914
10430
  return null;
9915
10431
  }
@@ -9933,7 +10449,8 @@ var BLOCK_TYPE_LABELS = {
9933
10449
  plan: "Plan",
9934
10450
  task_card: "Task Card",
9935
10451
  data_visualization: "Data Visualization",
9936
- container: "Container"
10452
+ container: "Container",
10453
+ data_table: "Data Table"
9937
10454
  };
9938
10455
  function BlockRow({
9939
10456
  builderBlock,