@tightknitai/block-kitchen 0.8.2 → 0.8.4

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,12 +1,12 @@
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, GripVertical, Sun, Moon, MessageSquare, AppWindow, Home, Plus, ChevronDown, BookOpen, ExternalLink, AlertTriangle, Trash2, Code2, MoreHorizontal, Send, Search, CheckCircle2, Check, ChevronRight, LayoutGrid, ArrowUp, ArrowDown, Pencil, Copy, Type, Bold, Italic, Strikethrough, Code, List, ListOrdered, Quote, Undo2, Redo2, Link as Link$1 } from 'lucide-react';
3
+ import { X, Circle, Sparkles, FileText, AlignLeft, MousePointerClick, TextCursorInput, LayoutTemplate, Bell, Pilcrow, Image, Video, GalleryHorizontal, Table, GripVertical, Sun, Moon, MessageSquare, AppWindow, Home, Plus, ChevronDown, BookOpen, ExternalLink, AlertTriangle, Trash2, Code2, MoreHorizontal, Send, Search, CheckCircle2, Check, ChevronRight, LayoutGrid, ArrowUp, ArrowDown, Pencil, Copy, Type, Bold, Italic, Strikethrough, Code, List, ListOrdered, Quote, Undo2, Redo2, Link as Link$1, Smile } from 'lucide-react';
4
4
  import * as React from 'react';
5
- import { useMemo, useState, useCallback, useRef, useEffect, useId } from 'react';
5
+ import { createContext, useMemo, useState, useCallback, useRef, useEffect, useId, useContext } from 'react';
6
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
6
7
  import * as DialogPrimitive from '@radix-ui/react-dialog';
7
8
  import { clsx } from 'clsx';
8
9
  import { twMerge } from 'tailwind-merge';
9
- import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
10
10
  import * as TooltipPrimitive from '@radix-ui/react-tooltip';
11
11
  import { validateBlockKit, CARD_ACTIONS_MAX } from '@tightknitai/slack-block-kit-validator';
12
12
  import { nanoid } from 'nanoid';
@@ -17,12 +17,41 @@ import { CSS } from '@dnd-kit/utilities';
17
17
  import * as PopoverPrimitive from '@radix-ui/react-popover';
18
18
  import * as RadioGroupPrimitive from '@radix-ui/react-radio-group';
19
19
  import Link from '@tiptap/extension-link';
20
- import { useEditor, EditorContent } from '@tiptap/react';
20
+ import { Node, ReactNodeViewRenderer, mergeAttributes, NodeViewWrapper, useEditor, EditorContent } from '@tiptap/react';
21
21
  import StarterKit from '@tiptap/starter-kit';
22
22
  import 'slack-blocks-to-jsx/dist/style.css';
23
23
  import { Message } from 'slack-blocks-to-jsx';
24
24
 
25
25
  // src/components/block-kitchen.tsx
26
+ function makeEmojiHook(byName) {
27
+ return (data, parse) => {
28
+ const entry = byName.get(data.name);
29
+ if (entry) {
30
+ if (entry.url) {
31
+ return /* @__PURE__ */ jsx(
32
+ "img",
33
+ {
34
+ src: entry.url,
35
+ alt: `:${data.name}:`,
36
+ title: `:${data.name}:`,
37
+ className: "bk-custom-emoji",
38
+ style: {
39
+ display: "inline-block",
40
+ width: "1.25em",
41
+ height: "1.25em",
42
+ verticalAlign: "-0.25em",
43
+ objectFit: "contain"
44
+ }
45
+ }
46
+ );
47
+ }
48
+ if (entry.alias) {
49
+ return parse({ name: entry.alias, unicode: data.unicode, skin_tone: data.skin_tone });
50
+ }
51
+ }
52
+ return parse(data);
53
+ };
54
+ }
26
55
  var defaultPalette = [
27
56
  {
28
57
  name: "Agents",
@@ -1519,50 +1548,63 @@ function useIsMobile() {
1519
1548
  }, []);
1520
1549
  return isMobile;
1521
1550
  }
1551
+ var EMPTY = {
1552
+ customEmojis: [],
1553
+ byName: /* @__PURE__ */ new Map()
1554
+ };
1555
+ var CustomEmojiContext = createContext(EMPTY);
1556
+ function CustomEmojiProvider({ customEmojis, children }) {
1557
+ const value = useMemo(() => {
1558
+ if (!customEmojis || customEmojis.length === 0) {
1559
+ return EMPTY;
1560
+ }
1561
+ const byName = /* @__PURE__ */ new Map();
1562
+ for (const emoji of customEmojis) {
1563
+ byName.set(emoji.name, emoji);
1564
+ }
1565
+ return { customEmojis, byName };
1566
+ }, [customEmojis]);
1567
+ return /* @__PURE__ */ jsx(CustomEmojiContext.Provider, { value, children });
1568
+ }
1569
+ function useCustomEmojis() {
1570
+ return useContext(CustomEmojiContext);
1571
+ }
1522
1572
 
1523
1573
  // src/lib/error-grouping.ts
1524
1574
  function toValidatorSurface(surface) {
1525
1575
  return surface === "app_home" ? "home" : surface;
1526
1576
  }
1527
- function extractBlockIndex(path) {
1528
- if (!path || path === "(root)") {
1529
- return null;
1530
- }
1531
- const match = /^\/(\d+)(?:\/|$)/.exec(path);
1532
- if (!match) {
1533
- return null;
1577
+ var BLOCK_PREFIX = /^blocks\[(\d+)\]/;
1578
+ function extractBlockIndex(raw) {
1579
+ const match = BLOCK_PREFIX.exec(raw);
1580
+ return match ? Number.parseInt(match[1], 10) : null;
1581
+ }
1582
+ function toBlockMessage(raw) {
1583
+ const rest = raw.replace(BLOCK_PREFIX, "");
1584
+ if (rest.startsWith(":")) {
1585
+ return rest.replace(/^:\s*/, "");
1534
1586
  }
1535
- return Number.parseInt(match[1], 10);
1587
+ return rest.replace(/^\./, "");
1536
1588
  }
1537
- function stripBlockPrefix(path) {
1538
- return path.replace(/^\/\d+/, "") || "(root)";
1589
+ function toGeneralMessage(raw) {
1590
+ return raw.startsWith("(root)") ? raw.replace(/^\(root\):?\s*/, "") : raw;
1539
1591
  }
1540
1592
  function groupValidatorErrors(errors, blocks) {
1541
1593
  const byBlockId = /* @__PURE__ */ new Map();
1542
1594
  const general = [];
1543
1595
  for (const raw of errors) {
1544
- const space = raw.indexOf(" ");
1545
- const path = space === -1 ? "(root)" : raw.slice(0, space);
1546
- const message = space === -1 ? raw : raw.slice(space + 1);
1547
- const idx = extractBlockIndex(path);
1596
+ const idx = extractBlockIndex(raw);
1548
1597
  if (idx === null || idx >= blocks.length) {
1549
- general.push(raw);
1598
+ general.push(toGeneralMessage(raw));
1550
1599
  continue;
1551
1600
  }
1552
1601
  const id = blocks[idx].id;
1553
- const friendly = formatMessage(stripBlockPrefix(path), message);
1554
1602
  const bucket = byBlockId.get(id) ?? [];
1555
- bucket.push(friendly);
1603
+ bucket.push(toBlockMessage(raw));
1556
1604
  byBlockId.set(id, bucket);
1557
1605
  }
1558
1606
  return { byBlockId, general, total: errors.length };
1559
1607
  }
1560
- function formatMessage(relativePath, message) {
1561
- if (relativePath === "" || relativePath === "(root)") {
1562
- return message;
1563
- }
1564
- return `${relativePath}: ${message}`;
1565
- }
1566
1608
 
1567
1609
  // src/lib/url-safety.ts
1568
1610
  var SAFE_LINK_PROTOCOLS = /^(https?|ircs?|mailto|tel|sms|xmpp)$/i;
@@ -1682,10 +1724,47 @@ function toSlackBlocks(blocks) {
1682
1724
  });
1683
1725
  }
1684
1726
 
1727
+ // src/lib/validation-sanitize.ts
1728
+ var VALIDATION_EMOJI_PLACEHOLDER = "white_check_mark";
1729
+ function rewriteEmojiNames(value) {
1730
+ if (Array.isArray(value)) {
1731
+ let changed = false;
1732
+ const next = value.map((item) => {
1733
+ const rewritten = rewriteEmojiNames(item);
1734
+ if (rewritten !== item) {
1735
+ changed = true;
1736
+ }
1737
+ return rewritten;
1738
+ });
1739
+ return changed ? next : value;
1740
+ }
1741
+ if (value !== null && typeof value === "object") {
1742
+ const source = value;
1743
+ let changed = false;
1744
+ const next = {};
1745
+ for (const key of Object.keys(source)) {
1746
+ const rewritten = rewriteEmojiNames(source[key]);
1747
+ next[key] = rewritten;
1748
+ if (rewritten !== source[key]) {
1749
+ changed = true;
1750
+ }
1751
+ }
1752
+ if (next.type === "emoji" && typeof next.name === "string" && next.name !== VALIDATION_EMOJI_PLACEHOLDER) {
1753
+ next.name = VALIDATION_EMOJI_PLACEHOLDER;
1754
+ changed = true;
1755
+ }
1756
+ return changed ? next : value;
1757
+ }
1758
+ return value;
1759
+ }
1760
+ function stripCustomEmojiForValidation(blocks) {
1761
+ return blocks.map((block) => rewriteEmojiNames(block));
1762
+ }
1763
+
1685
1764
  // src/state/use-block-kit-validation.ts
1686
1765
  var DEBOUNCE_MS = 150;
1687
1766
  function computeValidation(blocks, surface) {
1688
- const payload = toSlackBlocks(blocks.map((b) => b.block));
1767
+ const payload = stripCustomEmojiForValidation(toSlackBlocks(blocks.map((b) => b.block)));
1689
1768
  const result = validateBlockKit(payload, {
1690
1769
  target: "blocks",
1691
1770
  surface: toValidatorSurface(surface)
@@ -3531,52 +3610,55 @@ function ContextEditor({ block, onChange }) {
3531
3610
  {
3532
3611
  label: "Items",
3533
3612
  help: "Shown in a horizontal row, usually as footnote-style content. Slack supports up to 10 items.",
3534
- children: /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-2", children: elements.map((el, idx) => {
3535
- if (el.type === "mrkdwn" || el.type === "plain_text") {
3536
- return /* @__PURE__ */ jsx(
3537
- TextItemCard,
3538
- {
3539
- index: idx,
3540
- element: el,
3541
- onChange: (next) => updateAt(idx, () => next),
3542
- onRemove: () => removeAt(idx)
3543
- },
3544
- idx
3545
- );
3546
- }
3547
- if (el.type === "image" && "image_url" in el) {
3548
- return /* @__PURE__ */ jsx(
3549
- ImageItemCard,
3613
+ children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", children: [
3614
+ elements.length === 0 ? /* @__PURE__ */ jsx("p", { className: "rounded-md border border-dashed bg-muted/20 px-3 py-2 text-xs text-muted-foreground", children: "No items yet \u2014 a context block needs at least one. Add text or an image below, or delete the block if you don't need it." }) : null,
3615
+ elements.map((el, idx) => {
3616
+ if (el.type === "mrkdwn" || el.type === "plain_text") {
3617
+ return /* @__PURE__ */ jsx(
3618
+ TextItemCard,
3619
+ {
3620
+ index: idx,
3621
+ element: el,
3622
+ onChange: (next) => updateAt(idx, () => next),
3623
+ onRemove: () => removeAt(idx)
3624
+ },
3625
+ idx
3626
+ );
3627
+ }
3628
+ if (el.type === "image" && "image_url" in el) {
3629
+ return /* @__PURE__ */ jsx(
3630
+ ImageItemCard,
3631
+ {
3632
+ index: idx,
3633
+ element: el,
3634
+ onChange: (next) => updateAt(idx, () => next),
3635
+ onRemove: () => removeAt(idx)
3636
+ },
3637
+ idx
3638
+ );
3639
+ }
3640
+ return /* @__PURE__ */ jsxs(
3641
+ "div",
3550
3642
  {
3551
- index: idx,
3552
- element: el,
3553
- onChange: (next) => updateAt(idx, () => next),
3554
- onRemove: () => removeAt(idx)
3643
+ className: "flex items-center justify-between rounded border bg-muted/30 px-2 py-1.5 text-xs text-muted-foreground",
3644
+ children: [
3645
+ /* @__PURE__ */ jsx("span", { children: "Item not editable in the visual builder" }),
3646
+ /* @__PURE__ */ jsx(
3647
+ "button",
3648
+ {
3649
+ type: "button",
3650
+ "aria-label": "Remove item",
3651
+ onClick: () => removeAt(idx),
3652
+ className: "text-muted-foreground hover:text-destructive",
3653
+ children: /* @__PURE__ */ jsx(Trash2, { className: "h-3.5 w-3.5" })
3654
+ }
3655
+ )
3656
+ ]
3555
3657
  },
3556
3658
  idx
3557
3659
  );
3558
- }
3559
- return /* @__PURE__ */ jsxs(
3560
- "div",
3561
- {
3562
- className: "flex items-center justify-between rounded border bg-muted/30 px-2 py-1.5 text-xs text-muted-foreground",
3563
- children: [
3564
- /* @__PURE__ */ jsx("span", { children: "Item not editable in the visual builder" }),
3565
- /* @__PURE__ */ jsx(
3566
- "button",
3567
- {
3568
- type: "button",
3569
- "aria-label": "Remove item",
3570
- onClick: () => removeAt(idx),
3571
- className: "text-muted-foreground hover:text-destructive",
3572
- children: /* @__PURE__ */ jsx(Trash2, { className: "h-3.5 w-3.5" })
3573
- }
3574
- )
3575
- ]
3576
- },
3577
- idx
3578
- );
3579
- }) })
3660
+ })
3661
+ ] })
3580
3662
  }
3581
3663
  ),
3582
3664
  /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
@@ -3715,12 +3797,423 @@ function ImageItemCard({
3715
3797
  function DividerEditor() {
3716
3798
  return /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: "A horizontal line that separates content. This block has no options to configure." });
3717
3799
  }
3800
+
3801
+ // src/lib/emoji-data.ts
3802
+ var TONE_CODEPOINT_TO_SKIN_TONE = {
3803
+ "1F3FB": 2,
3804
+ "1F3FC": 3,
3805
+ "1F3FD": 4,
3806
+ "1F3FE": 5,
3807
+ "1F3FF": 6
3808
+ };
3809
+ var CATEGORY_ORDER = [
3810
+ "Smileys & Emotion",
3811
+ "People & Body",
3812
+ "Animals & Nature",
3813
+ "Food & Drink",
3814
+ "Travel & Places",
3815
+ "Activities",
3816
+ "Objects",
3817
+ "Symbols",
3818
+ "Flags"
3819
+ ];
3820
+ function codepointsToGlyph(unified) {
3821
+ return unified.split("-").map((cp) => {
3822
+ const n = Number.parseInt(cp, 16);
3823
+ return Number.isFinite(n) ? String.fromCodePoint(n) : "";
3824
+ }).join("");
3825
+ }
3826
+ function unifiedToUnicode(unified) {
3827
+ return unified.toLowerCase();
3828
+ }
3829
+ function composeTextEmoji(name, skinTone) {
3830
+ if (skinTone && skinTone >= 2 && skinTone <= 6) {
3831
+ return `:${name}::skin-tone-${skinTone}:`;
3832
+ }
3833
+ return `:${name}:`;
3834
+ }
3835
+ function buildEmojiIndex(raw) {
3836
+ const all = [];
3837
+ const byName = /* @__PURE__ */ new Map();
3838
+ const byUnified = /* @__PURE__ */ new Map();
3839
+ for (const r of raw) {
3840
+ if (!CATEGORY_ORDER.includes(r.category)) {
3841
+ continue;
3842
+ }
3843
+ const shortNames = r.short_names?.length ? r.short_names : [r.short_name];
3844
+ const skinUnified = {};
3845
+ if (r.skin_variations) {
3846
+ for (const [toneKey, variation] of Object.entries(r.skin_variations)) {
3847
+ const skinTone = TONE_CODEPOINT_TO_SKIN_TONE[toneKey];
3848
+ if (skinTone && variation?.unified) {
3849
+ skinUnified[skinTone] = variation.unified;
3850
+ }
3851
+ }
3852
+ }
3853
+ const datum = {
3854
+ name: shortNames[0],
3855
+ shortNames,
3856
+ unified: r.unified,
3857
+ category: r.category,
3858
+ sortOrder: r.sort_order,
3859
+ skinUnified
3860
+ };
3861
+ all.push(datum);
3862
+ byUnified.set(r.unified.toUpperCase(), datum);
3863
+ for (const sn of shortNames) {
3864
+ if (!byName.has(sn)) {
3865
+ byName.set(sn, datum);
3866
+ }
3867
+ }
3868
+ }
3869
+ all.sort((a, b) => a.sortOrder - b.sortOrder);
3870
+ const byCategory = CATEGORY_ORDER.map((category) => ({
3871
+ category,
3872
+ emoji: all.filter((e) => e.category === category)
3873
+ })).filter((group) => group.emoji.length > 0);
3874
+ return { all, byName, byUnified, byCategory };
3875
+ }
3876
+ var indexPromise = null;
3877
+ var cachedIndex = null;
3878
+ async function loadEmojiIndex() {
3879
+ if (cachedIndex) {
3880
+ return cachedIndex;
3881
+ }
3882
+ if (!indexPromise) {
3883
+ indexPromise = import('emoji-datasource/emoji.json').then((mod) => {
3884
+ const raw = mod.default ?? mod;
3885
+ cachedIndex = buildEmojiIndex(raw);
3886
+ return cachedIndex;
3887
+ }).catch((err) => {
3888
+ indexPromise = null;
3889
+ throw err;
3890
+ });
3891
+ }
3892
+ return indexPromise;
3893
+ }
3894
+ function getEmojiIndex() {
3895
+ return cachedIndex;
3896
+ }
3897
+
3898
+ // src/lib/emoji-recents.ts
3899
+ var STORAGE_KEY = "bk:emoji-recents";
3900
+ var SKIN_TONE_KEY = "bk:emoji-skin-tone";
3901
+ var MAX_RECENTS = 24;
3902
+ function readEmojiSkinTone() {
3903
+ try {
3904
+ const raw = globalThis.localStorage?.getItem(SKIN_TONE_KEY);
3905
+ if (!raw) {
3906
+ return null;
3907
+ }
3908
+ const n = Number.parseInt(raw, 10);
3909
+ return n >= 2 && n <= 6 ? n : null;
3910
+ } catch {
3911
+ return null;
3912
+ }
3913
+ }
3914
+ function writeEmojiSkinTone(skinTone) {
3915
+ try {
3916
+ if (skinTone && skinTone >= 2 && skinTone <= 6) {
3917
+ globalThis.localStorage?.setItem(SKIN_TONE_KEY, String(skinTone));
3918
+ } else {
3919
+ globalThis.localStorage?.removeItem(SKIN_TONE_KEY);
3920
+ }
3921
+ } catch {
3922
+ }
3923
+ }
3924
+ function readEmojiRecents() {
3925
+ try {
3926
+ const raw = globalThis.localStorage?.getItem(STORAGE_KEY);
3927
+ if (!raw) {
3928
+ return [];
3929
+ }
3930
+ const parsed = JSON.parse(raw);
3931
+ return Array.isArray(parsed) ? parsed.filter((n) => typeof n === "string") : [];
3932
+ } catch {
3933
+ return [];
3934
+ }
3935
+ }
3936
+ function pushEmojiRecent(name) {
3937
+ const next = [name, ...readEmojiRecents().filter((n) => n !== name)].slice(0, MAX_RECENTS);
3938
+ try {
3939
+ globalThis.localStorage?.setItem(STORAGE_KEY, JSON.stringify(next));
3940
+ } catch {
3941
+ }
3942
+ return next;
3943
+ }
3944
+ var SKIN_TONES = [
3945
+ { tone: null, label: "Default", swatch: "\u270B" },
3946
+ { tone: 2, label: "Light", swatch: "\u270B\u{1F3FB}" },
3947
+ { tone: 3, label: "Medium-light", swatch: "\u270B\u{1F3FC}" },
3948
+ { tone: 4, label: "Medium", swatch: "\u270B\u{1F3FD}" },
3949
+ { tone: 5, label: "Medium-dark", swatch: "\u270B\u{1F3FE}" },
3950
+ { tone: 6, label: "Dark", swatch: "\u270B\u{1F3FF}" }
3951
+ ];
3952
+ function EmojiPicker({
3953
+ onSelect,
3954
+ autoFocus = true
3955
+ }) {
3956
+ const { customEmojis } = useCustomEmojis();
3957
+ const [index, setIndex] = useState(null);
3958
+ const [loadError, setLoadError] = useState(false);
3959
+ const [query, setQuery] = useState("");
3960
+ const [skinTone, setSkinTone] = useState(() => readEmojiSkinTone());
3961
+ const [recents, setRecents] = useState(() => readEmojiRecents());
3962
+ const searchRef = useRef(null);
3963
+ useEffect(() => {
3964
+ let active = true;
3965
+ loadEmojiIndex().then((idx) => {
3966
+ if (active) {
3967
+ setIndex(idx);
3968
+ }
3969
+ }).catch(() => {
3970
+ if (active) {
3971
+ setLoadError(true);
3972
+ }
3973
+ });
3974
+ return () => {
3975
+ active = false;
3976
+ };
3977
+ }, []);
3978
+ useEffect(() => {
3979
+ if (autoFocus) {
3980
+ searchRef.current?.focus();
3981
+ }
3982
+ }, [autoFocus]);
3983
+ const customByName = useMemo(() => {
3984
+ const map = /* @__PURE__ */ new Map();
3985
+ for (const e of customEmojis) {
3986
+ map.set(e.name, e);
3987
+ }
3988
+ return map;
3989
+ }, [customEmojis]);
3990
+ const customImageEmoji = useMemo(() => customEmojis.filter((e) => e.url), [customEmojis]);
3991
+ const handlePick = (selection) => {
3992
+ setRecents(pushEmojiRecent(selection.name));
3993
+ onSelect(selection);
3994
+ };
3995
+ const handleSkinTone = (tone) => {
3996
+ setSkinTone(tone);
3997
+ writeEmojiSkinTone(tone);
3998
+ };
3999
+ const sections = useMemo(() => {
4000
+ if (!index) {
4001
+ return [];
4002
+ }
4003
+ const trimmed = query.trim().toLowerCase();
4004
+ if (trimmed) {
4005
+ const standard = index.all.filter((d) => d.shortNames.some((sn) => sn.includes(trimmed))).slice(0, 120).map((datum) => ({ datum }));
4006
+ const custom = customImageEmoji.filter((e) => e.name.toLowerCase().includes(trimmed)).map((custom2) => ({ custom: custom2 }));
4007
+ return [{ key: "search", title: "Search results", emoji: [...custom, ...standard] }];
4008
+ }
4009
+ const out = [];
4010
+ if (recents.length > 0) {
4011
+ const recentEmoji = recents.map((name) => {
4012
+ const custom = customByName.get(name);
4013
+ if (custom?.url) {
4014
+ return { custom };
4015
+ }
4016
+ const datum = index.byName.get(name);
4017
+ return datum ? { datum } : null;
4018
+ }).filter((e) => e !== null);
4019
+ if (recentEmoji.length > 0) {
4020
+ out.push({ key: "recents", title: "Frequently used", emoji: recentEmoji });
4021
+ }
4022
+ }
4023
+ if (customImageEmoji.length > 0) {
4024
+ out.push({ key: "custom", title: "Custom", emoji: customImageEmoji.map((custom) => ({ custom })) });
4025
+ }
4026
+ for (const group of index.byCategory) {
4027
+ out.push({ key: group.category, title: group.category, emoji: group.emoji.map((datum) => ({ datum })) });
4028
+ }
4029
+ return out;
4030
+ }, [index, query, recents, customByName, customImageEmoji]);
4031
+ return /* @__PURE__ */ jsxs("div", { className: "bk-emoji-picker flex w-[320px] max-w-[90vw] flex-col gap-2", children: [
4032
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
4033
+ /* @__PURE__ */ jsx(
4034
+ Input,
4035
+ {
4036
+ ref: searchRef,
4037
+ value: query,
4038
+ placeholder: "Search emoji\u2026",
4039
+ "aria-label": "Search emoji",
4040
+ onChange: (e) => setQuery(e.target.value),
4041
+ className: "h-8"
4042
+ }
4043
+ ),
4044
+ /* @__PURE__ */ jsx(SkinToneSelector, { value: skinTone, onChange: handleSkinTone })
4045
+ ] }),
4046
+ /* @__PURE__ */ jsx("div", { className: "h-64 overflow-y-auto pr-1", children: loadError ? /* @__PURE__ */ jsx("p", { className: "p-4 text-center text-xs text-muted-foreground", children: "Couldn't load emoji." }) : !index ? /* @__PURE__ */ jsx("p", { className: "p-4 text-center text-xs text-muted-foreground", children: "Loading emoji\u2026" }) : sections.every((s) => s.emoji.length === 0) ? /* @__PURE__ */ jsx("p", { className: "p-4 text-center text-xs text-muted-foreground", children: "No emoji found." }) : sections.map((section) => /* @__PURE__ */ jsxs("div", { className: "mb-2", children: [
4047
+ /* @__PURE__ */ jsx("div", { className: "sticky top-0 z-10 bg-background px-1 py-1 text-[11px] font-medium text-muted-foreground", children: section.title }),
4048
+ /* @__PURE__ */ jsx("div", { className: "grid grid-cols-8 gap-0.5", children: section.emoji.map(
4049
+ (entry, i) => entry.custom ? /* @__PURE__ */ jsx(
4050
+ CustomEmojiButton,
4051
+ {
4052
+ emoji: entry.custom,
4053
+ onPick: handlePick
4054
+ },
4055
+ `${section.key}-${entry.custom.name}-${i}`
4056
+ ) : entry.datum ? /* @__PURE__ */ jsx(
4057
+ StandardEmojiButton,
4058
+ {
4059
+ datum: entry.datum,
4060
+ skinTone,
4061
+ onPick: handlePick
4062
+ },
4063
+ `${section.key}-${entry.datum.name}-${i}`
4064
+ ) : null
4065
+ ) })
4066
+ ] }, section.key)) })
4067
+ ] });
4068
+ }
4069
+ function resolveStandard(datum, skinTone) {
4070
+ const tonedUnified = skinTone ? datum.skinUnified[skinTone] : void 0;
4071
+ return {
4072
+ name: datum.name,
4073
+ unicode: unifiedToUnicode(tonedUnified ?? datum.unified),
4074
+ src: null,
4075
+ skinTone: tonedUnified ? skinTone : null
4076
+ };
4077
+ }
4078
+ var emojiButtonClass = "flex h-8 w-8 cursor-pointer items-center justify-center rounded text-lg leading-none hover:bg-accent";
4079
+ function StandardEmojiButton({
4080
+ datum,
4081
+ skinTone,
4082
+ onPick
4083
+ }) {
4084
+ const tonedUnified = skinTone && datum.skinUnified[skinTone] || datum.unified;
4085
+ return /* @__PURE__ */ jsx(
4086
+ "button",
4087
+ {
4088
+ type: "button",
4089
+ className: emojiButtonClass,
4090
+ title: `:${datum.name}:`,
4091
+ "aria-label": datum.name,
4092
+ onClick: () => onPick(resolveStandard(datum, skinTone)),
4093
+ children: codepointsToGlyph(tonedUnified)
4094
+ }
4095
+ );
4096
+ }
4097
+ function CustomEmojiButton({ emoji, onPick }) {
4098
+ return /* @__PURE__ */ jsx(
4099
+ "button",
4100
+ {
4101
+ type: "button",
4102
+ className: emojiButtonClass,
4103
+ title: `:${emoji.name}:`,
4104
+ "aria-label": emoji.name,
4105
+ onClick: () => onPick({ name: emoji.name, unicode: null, src: emoji.url, skinTone: null }),
4106
+ children: emoji.url ? /* @__PURE__ */ jsx("img", { src: emoji.url, alt: `:${emoji.name}:`, className: "h-5 w-5 object-contain" }) : /* @__PURE__ */ jsxs("span", { className: "text-[10px]", children: [
4107
+ ":",
4108
+ emoji.name,
4109
+ ":"
4110
+ ] })
4111
+ }
4112
+ );
4113
+ }
4114
+ function SkinToneSelector({ value, onChange }) {
4115
+ const [open, setOpen] = useState(false);
4116
+ const active = SKIN_TONES.find((t) => t.tone === value) ?? SKIN_TONES[0];
4117
+ return /* @__PURE__ */ jsxs("div", { className: "relative shrink-0", children: [
4118
+ /* @__PURE__ */ jsx(
4119
+ "button",
4120
+ {
4121
+ type: "button",
4122
+ "aria-label": `Skin tone: ${active.label}`,
4123
+ title: `Skin tone: ${active.label}`,
4124
+ className: "flex h-8 w-8 items-center justify-center rounded border text-lg leading-none hover:bg-accent",
4125
+ onClick: () => setOpen((o) => !o),
4126
+ children: active.swatch
4127
+ }
4128
+ ),
4129
+ open && /* @__PURE__ */ jsx("div", { className: "absolute right-0 z-20 mt-1 flex gap-0.5 rounded-md border bg-background p-1 shadow-md", children: SKIN_TONES.map((t) => /* @__PURE__ */ jsx(
4130
+ "button",
4131
+ {
4132
+ type: "button",
4133
+ "aria-label": t.label,
4134
+ title: t.label,
4135
+ className: cn(emojiButtonClass, value === t.tone && "bg-accent"),
4136
+ onClick: () => {
4137
+ onChange(t.tone);
4138
+ setOpen(false);
4139
+ },
4140
+ children: t.swatch
4141
+ },
4142
+ t.label
4143
+ )) })
4144
+ ] });
4145
+ }
4146
+ function EmojiPickerButton({
4147
+ onSelect,
4148
+ className,
4149
+ label = "Insert emoji",
4150
+ align = "start"
4151
+ }) {
4152
+ const [open, setOpen] = useState(false);
4153
+ return /* @__PURE__ */ jsxs(Popover, { open, onOpenChange: setOpen, children: [
4154
+ /* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
4155
+ "button",
4156
+ {
4157
+ type: "button",
4158
+ "aria-label": label,
4159
+ title: label,
4160
+ className: cn(
4161
+ "flex h-7 w-7 cursor-pointer items-center justify-center rounded text-muted-foreground transition-colors hover:bg-accent hover:text-foreground",
4162
+ className
4163
+ ),
4164
+ children: /* @__PURE__ */ jsx(Smile, { className: "h-3.5 w-3.5" })
4165
+ }
4166
+ ) }),
4167
+ /* @__PURE__ */ jsx(PopoverContent, { align, className: "w-auto p-2", children: /* @__PURE__ */ jsx(
4168
+ EmojiPicker,
4169
+ {
4170
+ onSelect: (selection) => {
4171
+ onSelect(selection);
4172
+ setOpen(false);
4173
+ }
4174
+ }
4175
+ ) })
4176
+ ] });
4177
+ }
4178
+ function EmojiTextInsertButton({
4179
+ targetRef,
4180
+ value,
4181
+ onChange,
4182
+ className,
4183
+ align = "end"
4184
+ }) {
4185
+ return /* @__PURE__ */ jsx(
4186
+ EmojiPickerButton,
4187
+ {
4188
+ align,
4189
+ label: "Insert emoji",
4190
+ className,
4191
+ onSelect: (sel) => {
4192
+ const el = targetRef.current;
4193
+ const insert = composeTextEmoji(sel.name, sel.skinTone ?? void 0);
4194
+ const start = el?.selectionStart ?? value.length;
4195
+ const end = el?.selectionEnd ?? value.length;
4196
+ const next = value.slice(0, start) + insert + value.slice(end);
4197
+ onChange(next);
4198
+ requestAnimationFrame(() => {
4199
+ if (el) {
4200
+ const caret = start + insert.length;
4201
+ el.focus();
4202
+ el.setSelectionRange(caret, caret);
4203
+ }
4204
+ });
4205
+ }
4206
+ }
4207
+ );
4208
+ }
3718
4209
  var HEADER_MAX = 150;
3719
4210
  var HEADER_LEVELS = [1, 2, 3, 4];
3720
4211
  function HeaderEditor({ block, onChange }) {
3721
4212
  const text = block.text?.text ?? "";
3722
4213
  const remaining = HEADER_MAX - text.length;
3723
4214
  const level = block.level;
4215
+ const inputRef = useRef(null);
4216
+ const setText = (next) => onChange({ ...block, text: { type: "plain_text", text: next, emoji: true } });
3724
4217
  return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3", children: [
3725
4218
  /* @__PURE__ */ jsx(
3726
4219
  EditorField,
@@ -3728,23 +4221,20 @@ function HeaderEditor({ block, onChange }) {
3728
4221
  label: "Heading text",
3729
4222
  help: `Plain text only. ${remaining} characters remaining.`,
3730
4223
  htmlFor: "header-text",
3731
- children: /* @__PURE__ */ jsx(
3732
- Input,
3733
- {
3734
- id: "header-text",
3735
- value: text,
3736
- maxLength: HEADER_MAX,
3737
- placeholder: "e.g. Weekly roundup",
3738
- onChange: (e) => onChange({
3739
- ...block,
3740
- text: {
3741
- type: "plain_text",
3742
- text: e.target.value,
3743
- emoji: true
3744
- }
3745
- })
3746
- }
3747
- )
4224
+ children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5", children: [
4225
+ /* @__PURE__ */ jsx(
4226
+ Input,
4227
+ {
4228
+ ref: inputRef,
4229
+ id: "header-text",
4230
+ value: text,
4231
+ maxLength: HEADER_MAX,
4232
+ placeholder: "e.g. Weekly roundup",
4233
+ onChange: (e) => setText(e.target.value)
4234
+ }
4235
+ ),
4236
+ /* @__PURE__ */ jsx(EmojiTextInsertButton, { targetRef: inputRef, value: text, onChange: setText, className: "shrink-0 border" })
4237
+ ] })
3748
4238
  }
3749
4239
  ),
3750
4240
  /* @__PURE__ */ jsx(EditorField, { label: "Level", help: "Optional. Builder-only extension; Slack's API ignores this value.", children: /* @__PURE__ */ jsxs(
@@ -4607,22 +5097,29 @@ function CheckboxField({
4607
5097
  ] });
4608
5098
  }
4609
5099
  function MarkdownEditor({ block, onChange }) {
5100
+ const textRef = useRef(null);
5101
+ const text = block.text ?? "";
5102
+ const setText = (next) => onChange({ ...block, text: next });
4610
5103
  return /* @__PURE__ */ jsx(
4611
5104
  EditorField,
4612
5105
  {
4613
5106
  label: "Markdown",
4614
5107
  help: "Standard markdown with GFM: **bold**, _italic_, lists, tables, ```code```, [links](url).",
4615
5108
  htmlFor: "markdown-text",
4616
- children: /* @__PURE__ */ jsx(
4617
- Textarea,
4618
- {
4619
- id: "markdown-text",
4620
- value: block.text ?? "",
4621
- rows: 8,
4622
- placeholder: "e.g. **Roadmap**\\n\\n- Item one\\n- Item two",
4623
- onChange: (e) => onChange({ ...block, text: e.target.value })
4624
- }
4625
- )
5109
+ children: /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-1.5", children: [
5110
+ /* @__PURE__ */ jsx(
5111
+ Textarea,
5112
+ {
5113
+ ref: textRef,
5114
+ id: "markdown-text",
5115
+ value: text,
5116
+ rows: 8,
5117
+ placeholder: "e.g. **Roadmap**\\n\\n- Item one\\n- Item two",
5118
+ onChange: (e) => setText(e.target.value)
5119
+ }
5120
+ ),
5121
+ /* @__PURE__ */ jsx(EmojiTextInsertButton, { targetRef: textRef, value: text, onChange: setText, className: "mt-1 shrink-0 border" })
5122
+ ] })
4626
5123
  }
4627
5124
  );
4628
5125
  }
@@ -4717,6 +5214,26 @@ function PlanEditor({ block, onChange }) {
4717
5214
  ] });
4718
5215
  }
4719
5216
 
5217
+ // src/lib/emoji-resolve.ts
5218
+ function makeEmojiImportResolver(customByName, index = getEmojiIndex()) {
5219
+ return (el) => {
5220
+ const name = el.name ?? "";
5221
+ const skinTone = el.skin_tone ?? null;
5222
+ const custom = customByName.get(name);
5223
+ if (custom?.url) {
5224
+ return { name, src: custom.url, unicode: null, skinTone };
5225
+ }
5226
+ const lookupName = custom?.alias ?? name;
5227
+ const datum = index?.byName.get(lookupName);
5228
+ let unicode = el.unicode ?? null;
5229
+ if (!unicode && datum) {
5230
+ const toned = skinTone ? datum.skinUnified[skinTone] : void 0;
5231
+ unicode = unifiedToUnicode(toned ?? datum.unified);
5232
+ }
5233
+ return { name, src: null, unicode, skinTone };
5234
+ };
5235
+ }
5236
+
4720
5237
  // src/lib/rich-text-tiptap.ts
4721
5238
  var SUPPORTED_BLOCK_KINDS = /* @__PURE__ */ new Set([
4722
5239
  "rich_text_section",
@@ -4724,7 +5241,7 @@ var SUPPORTED_BLOCK_KINDS = /* @__PURE__ */ new Set([
4724
5241
  "rich_text_quote",
4725
5242
  "rich_text_preformatted"
4726
5243
  ]);
4727
- var SUPPORTED_INLINE_KINDS = /* @__PURE__ */ new Set(["text", "link"]);
5244
+ var SUPPORTED_INLINE_KINDS = /* @__PURE__ */ new Set(["text", "link", "emoji"]);
4728
5245
  var SUPPORTED_TEXT_STYLE_KEYS = /* @__PURE__ */ new Set(["bold", "italic", "strike", "code"]);
4729
5246
  var MAX_LIST_INDENT = 8;
4730
5247
  function detectLossy(block) {
@@ -4771,16 +5288,22 @@ function scanInlines(inlines, where, out) {
4771
5288
  }
4772
5289
  });
4773
5290
  }
4774
- function richTextToProseMirror(block) {
5291
+ var defaultEmojiResolver = (el) => ({
5292
+ name: el.name ?? "",
5293
+ src: null,
5294
+ unicode: el.unicode ?? null,
5295
+ skinTone: el.skin_tone ?? null
5296
+ });
5297
+ function richTextToProseMirror(block, resolveEmoji = defaultEmojiResolver) {
4775
5298
  const content = [];
4776
5299
  const listsByIndent = /* @__PURE__ */ new Map();
4777
5300
  for (const el of block.elements ?? []) {
4778
5301
  if (el.type === "rich_text_list") {
4779
- appendRichTextList(el, content, listsByIndent);
5302
+ appendRichTextList(el, content, listsByIndent, resolveEmoji);
4780
5303
  continue;
4781
5304
  }
4782
5305
  listsByIndent.clear();
4783
- const node = blockElementToPM(el);
5306
+ const node = blockElementToPM(el, resolveEmoji);
4784
5307
  if (node) {
4785
5308
  content.push(node);
4786
5309
  }
@@ -4790,12 +5313,12 @@ function richTextToProseMirror(block) {
4790
5313
  content: content.length > 0 ? content : [{ type: "paragraph" }]
4791
5314
  };
4792
5315
  }
4793
- function appendRichTextList(el, topLevel, listsByIndent) {
5316
+ function appendRichTextList(el, topLevel, listsByIndent, resolveEmoji) {
4794
5317
  const indent = el.indent ?? 0;
4795
5318
  const listType = el.style === "ordered" ? "orderedList" : "bulletList";
4796
5319
  const items = (el.elements ?? []).map((section) => ({
4797
5320
  type: "listItem",
4798
- content: [{ type: "paragraph", content: inlinesToPM(section.elements ?? []) }]
5321
+ content: [{ type: "paragraph", content: inlinesToPM(section.elements ?? [], resolveEmoji) }]
4799
5322
  }));
4800
5323
  for (const k of Array.from(listsByIndent.keys())) {
4801
5324
  if (k > indent) {
@@ -4821,11 +5344,11 @@ function appendRichTextList(el, topLevel, listsByIndent) {
4821
5344
  const parentLastItem = parent.content[parent.content.length - 1];
4822
5345
  parentLastItem.content = [...parentLastItem.content ?? [], list];
4823
5346
  }
4824
- function blockElementToPM(el) {
5347
+ function blockElementToPM(el, resolveEmoji) {
4825
5348
  if (el.type === "rich_text_section") {
4826
5349
  return {
4827
5350
  type: "paragraph",
4828
- content: inlinesToPM(el.elements ?? [])
5351
+ content: inlinesToPM(el.elements ?? [], resolveEmoji)
4829
5352
  };
4830
5353
  }
4831
5354
  if (el.type === "rich_text_quote") {
@@ -4834,7 +5357,7 @@ function blockElementToPM(el) {
4834
5357
  content: [
4835
5358
  {
4836
5359
  type: "paragraph",
4837
- content: inlinesToPM(el.elements ?? [])
5360
+ content: inlinesToPM(el.elements ?? [], resolveEmoji)
4838
5361
  }
4839
5362
  ]
4840
5363
  };
@@ -4849,7 +5372,7 @@ function blockElementToPM(el) {
4849
5372
  }
4850
5373
  return null;
4851
5374
  }
4852
- function inlinesToPM(inlines) {
5375
+ function inlinesToPM(inlines, resolveEmoji) {
4853
5376
  const out = [];
4854
5377
  for (const inline of inlines) {
4855
5378
  if (inline.type === "text") {
@@ -4858,6 +5381,12 @@ function inlinesToPM(inlines) {
4858
5381
  }
4859
5382
  const marks = styleToMarks(inline.style);
4860
5383
  out.push({ type: "text", text: inline.text, marks });
5384
+ } else if (inline.type === "emoji") {
5385
+ const attrs = resolveEmoji(inline);
5386
+ if (!attrs.name) {
5387
+ continue;
5388
+ }
5389
+ out.push({ type: "emoji", attrs });
4861
5390
  } else if (inline.type === "link") {
4862
5391
  const link = inline;
4863
5392
  const text = link.text || link.url || "";
@@ -4975,6 +5504,19 @@ function flattenList(listNode, indent, out) {
4975
5504
  function proseMirrorInlinesToRichTextElements(nodes) {
4976
5505
  const out = [];
4977
5506
  for (const node of nodes) {
5507
+ if (node.type === "emoji") {
5508
+ const name = String(node.attrs?.name ?? "");
5509
+ if (!name) {
5510
+ continue;
5511
+ }
5512
+ const emoji = { type: "emoji", name };
5513
+ const skinTone = node.attrs?.skinTone;
5514
+ if (typeof skinTone === "number" && skinTone >= 2 && skinTone <= 6) {
5515
+ emoji.skin_tone = skinTone;
5516
+ }
5517
+ out.push(emoji);
5518
+ continue;
5519
+ }
4978
5520
  if (node.type !== "text" || !node.text) {
4979
5521
  continue;
4980
5522
  }
@@ -5045,6 +5587,49 @@ function sameStyle(a, b) {
5045
5587
  }
5046
5588
  return ak.every((k, i) => k === bk[i]);
5047
5589
  }
5590
+ function EmojiNodeView({ node }) {
5591
+ const { name, src, unicode } = node.attrs;
5592
+ const label = `:${name}:`;
5593
+ return /* @__PURE__ */ jsx(NodeViewWrapper, { as: "span", className: "bk-emoji-node", "data-emoji": name, title: label, children: src ? /* @__PURE__ */ jsx(
5594
+ "img",
5595
+ {
5596
+ src,
5597
+ alt: label,
5598
+ style: {
5599
+ display: "inline-block",
5600
+ width: "1.25em",
5601
+ height: "1.25em",
5602
+ verticalAlign: "-0.25em",
5603
+ objectFit: "contain"
5604
+ }
5605
+ }
5606
+ ) : /* @__PURE__ */ jsx("span", { "aria-label": label, role: "img", children: unicode ? codepointsToGlyph(unicode) : label }) });
5607
+ }
5608
+ var EmojiNode = Node.create({
5609
+ name: "emoji",
5610
+ group: "inline",
5611
+ inline: true,
5612
+ atom: true,
5613
+ selectable: true,
5614
+ addAttributes() {
5615
+ return {
5616
+ name: { default: "" },
5617
+ src: { default: null },
5618
+ unicode: { default: null },
5619
+ skinTone: { default: null }
5620
+ };
5621
+ },
5622
+ parseHTML() {
5623
+ return [{ tag: "span[data-emoji]" }];
5624
+ },
5625
+ renderHTML({ HTMLAttributes }) {
5626
+ const name = HTMLAttributes.name ?? "";
5627
+ return ["span", mergeAttributes({ "data-emoji": name }, HTMLAttributes), `:${name}:`];
5628
+ },
5629
+ addNodeView() {
5630
+ return ReactNodeViewRenderer(EmojiNodeView);
5631
+ }
5632
+ });
5048
5633
  var BLOCK_ELEMENT_LABEL = {
5049
5634
  rich_text_section: "Section",
5050
5635
  rich_text_list: "List",
@@ -5383,15 +5968,34 @@ function InlineFields({
5383
5968
  }
5384
5969
  if (kind === "emoji") {
5385
5970
  const emoji = element;
5386
- return /* @__PURE__ */ jsx(EditorField, { label: "Name", help: "Emoji shortcode without colons, e.g. wave", htmlFor: `${idPrefix}-emoji`, children: /* @__PURE__ */ jsx(
5387
- Input,
5971
+ return /* @__PURE__ */ jsx(
5972
+ EditorField,
5388
5973
  {
5389
- id: `${idPrefix}-emoji`,
5390
- value: emoji.name ?? "",
5391
- placeholder: "e.g. wave",
5392
- onChange: (e) => onChange({ ...emoji, name: e.target.value })
5974
+ label: "Name",
5975
+ help: "Pick from the emoji list, or type a shortcode (without colons).",
5976
+ htmlFor: `${idPrefix}-emoji`,
5977
+ children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5", children: [
5978
+ /* @__PURE__ */ jsx(
5979
+ Input,
5980
+ {
5981
+ id: `${idPrefix}-emoji`,
5982
+ value: emoji.name ?? "",
5983
+ placeholder: "e.g. wave",
5984
+ onChange: (e) => onChange({ ...emoji, name: e.target.value })
5985
+ }
5986
+ ),
5987
+ /* @__PURE__ */ jsx(
5988
+ EmojiPickerButton,
5989
+ {
5990
+ align: "end",
5991
+ label: "Pick emoji",
5992
+ className: "shrink-0 border",
5993
+ onSelect: (sel) => onChange({ ...emoji, name: sel.name, skin_tone: sel.skinTone ?? void 0, unicode: void 0 })
5994
+ }
5995
+ )
5996
+ ] })
5393
5997
  }
5394
- ) });
5998
+ );
5395
5999
  }
5396
6000
  if (kind === "user") {
5397
6001
  const user = element;
@@ -5602,6 +6206,20 @@ function LossyBanner({ reasons }) {
5602
6206
  }
5603
6207
  function RichTextWysiwygEditor({ block, onChange }) {
5604
6208
  const lastEmittedRef = useRef(null);
6209
+ const { byName: customByName } = useCustomEmojis();
6210
+ const [emojiIndex, setEmojiIndex] = useState(null);
6211
+ useEffect(() => {
6212
+ let active = true;
6213
+ loadEmojiIndex().then((idx) => active && setEmojiIndex(idx)).catch(() => {
6214
+ });
6215
+ return () => {
6216
+ active = false;
6217
+ };
6218
+ }, []);
6219
+ const resolveEmoji = useMemo(
6220
+ () => makeEmojiImportResolver(customByName, emojiIndex),
6221
+ [customByName, emojiIndex]
6222
+ );
5605
6223
  const handleUpdate = useCallback(
5606
6224
  (editor2) => {
5607
6225
  const next = proseMirrorToRichText(editor2.getJSON());
@@ -5616,6 +6234,7 @@ function RichTextWysiwygEditor({ block, onChange }) {
5616
6234
  heading: false,
5617
6235
  horizontalRule: false
5618
6236
  }),
6237
+ EmojiNode,
5619
6238
  Link.configure({
5620
6239
  openOnClick: false,
5621
6240
  autolink: true,
@@ -5632,7 +6251,7 @@ function RichTextWysiwygEditor({ block, onChange }) {
5632
6251
  HTMLAttributes: { rel: "noreferrer noopener", target: "_blank" }
5633
6252
  })
5634
6253
  ],
5635
- content: richTextToProseMirror(block),
6254
+ content: richTextToProseMirror(block, resolveEmoji),
5636
6255
  editorProps: {
5637
6256
  attributes: {
5638
6257
  class: "bk-rich-editor min-h-[120px] rounded-md border border-input bg-background px-3 py-2 text-sm focus:outline-none focus-visible:ring-1 focus-visible:ring-ring [&_p]:my-1 [&_ul]:my-1 [&_ul]:list-disc [&_ul]:pl-5 [&_ol]:my-1 [&_ol]:pl-5 [&_li]:my-0 [&_pre]:bg-muted [&_pre]:rounded [&_pre]:p-2 [&_code]:rounded [&_code]:bg-muted [&_code]:px-1 [&_blockquote]:border-l-2 [&_blockquote]:border-border [&_blockquote]:pl-3 [&_blockquote]:text-muted-foreground"
@@ -5647,10 +6266,10 @@ function RichTextWysiwygEditor({ block, onChange }) {
5647
6266
  if (lastEmittedRef.current && JSON.stringify(lastEmittedRef.current) === JSON.stringify(block)) {
5648
6267
  return;
5649
6268
  }
5650
- editor.commands.setContent(richTextToProseMirror(block), {
6269
+ editor.commands.setContent(richTextToProseMirror(block, resolveEmoji), {
5651
6270
  emitUpdate: false
5652
6271
  });
5653
- }, [block, editor]);
6272
+ }, [block, editor, resolveEmoji]);
5654
6273
  if (!editor) {
5655
6274
  return /* @__PURE__ */ jsx("div", { className: "min-h-[120px] rounded-md border border-input bg-background" });
5656
6275
  }
@@ -5699,6 +6318,16 @@ function Toolbar({ editor }) {
5699
6318
  ),
5700
6319
  /* @__PURE__ */ jsx(Divider, {}),
5701
6320
  /* @__PURE__ */ jsx(LinkPopover, { editor }),
6321
+ /* @__PURE__ */ jsx(
6322
+ EmojiPickerButton,
6323
+ {
6324
+ align: "start",
6325
+ onSelect: (sel) => editor.chain().focus().insertContent({
6326
+ type: "emoji",
6327
+ attrs: { name: sel.name, src: sel.src, unicode: sel.unicode, skinTone: sel.skinTone }
6328
+ }).run()
6329
+ }
6330
+ ),
5702
6331
  /* @__PURE__ */ jsx(Divider, {}),
5703
6332
  /* @__PURE__ */ jsx(
5704
6333
  ToolbarButton,
@@ -5897,6 +6526,8 @@ function defaultImage() {
5897
6526
  function SectionEditor({ block, onChange }) {
5898
6527
  const text = block.text?.text ?? "";
5899
6528
  const accessoryKind = detectAccessory(block.accessory);
6529
+ const textRef = useRef(null);
6530
+ const setText = (next) => onChange({ ...block, text: { type: "mrkdwn", text: next } });
5900
6531
  const setAccessoryKind = (next) => {
5901
6532
  if (next === accessoryKind) {
5902
6533
  return;
@@ -5920,19 +6551,20 @@ function SectionEditor({ block, onChange }) {
5920
6551
  label: "Text",
5921
6552
  help: "Supports *bold*, _italic_, ~strike~, `code`, and <url|link> formatting.",
5922
6553
  htmlFor: "section-text",
5923
- children: /* @__PURE__ */ jsx(
5924
- Textarea,
5925
- {
5926
- id: "section-text",
5927
- value: text,
5928
- rows: 4,
5929
- placeholder: "e.g. Welcome to the channel! Ping <@U123> with any questions.",
5930
- onChange: (e) => onChange({
5931
- ...block,
5932
- text: { type: "mrkdwn", text: e.target.value }
5933
- })
5934
- }
5935
- )
6554
+ children: /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-1.5", children: [
6555
+ /* @__PURE__ */ jsx(
6556
+ Textarea,
6557
+ {
6558
+ ref: textRef,
6559
+ id: "section-text",
6560
+ value: text,
6561
+ rows: 4,
6562
+ placeholder: "e.g. Welcome to the channel! Ping <@U123> with any questions.",
6563
+ onChange: (e) => setText(e.target.value)
6564
+ }
6565
+ ),
6566
+ /* @__PURE__ */ jsx(EmojiTextInsertButton, { targetRef: textRef, value: text, onChange: setText, className: "mt-1 shrink-0 border" })
6567
+ ] })
5936
6568
  }
5937
6569
  ),
5938
6570
  /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3 rounded-md border bg-muted/20 p-3", children: [
@@ -5973,19 +6605,22 @@ function ButtonAccessoryFields({ button, onChange }) {
5973
6605
  const url = button.url ?? "";
5974
6606
  const value = button.value ?? "";
5975
6607
  const style = button.style === "primary" || button.style === "danger" ? button.style : "default";
6608
+ const labelRef = useRef(null);
6609
+ const setLabel = (next) => onChange({ ...button, text: { type: "plain_text", text: next, emoji: true } });
5976
6610
  return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3", children: [
5977
- /* @__PURE__ */ jsx(EditorField, { label: "Label", htmlFor: "section-acc-btn-label", children: /* @__PURE__ */ jsx(
5978
- Input,
5979
- {
5980
- id: "section-acc-btn-label",
5981
- value: label,
5982
- placeholder: "e.g. Learn more",
5983
- onChange: (e) => onChange({
5984
- ...button,
5985
- text: { type: "plain_text", text: e.target.value, emoji: true }
5986
- })
5987
- }
5988
- ) }),
6611
+ /* @__PURE__ */ jsx(EditorField, { label: "Label", htmlFor: "section-acc-btn-label", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5", children: [
6612
+ /* @__PURE__ */ jsx(
6613
+ Input,
6614
+ {
6615
+ ref: labelRef,
6616
+ id: "section-acc-btn-label",
6617
+ value: label,
6618
+ placeholder: "e.g. Learn more",
6619
+ onChange: (e) => setLabel(e.target.value)
6620
+ }
6621
+ ),
6622
+ /* @__PURE__ */ jsx(EmojiTextInsertButton, { targetRef: labelRef, value: label, onChange: setLabel, className: "shrink-0 border" })
6623
+ ] }) }),
5989
6624
  /* @__PURE__ */ jsx(
5990
6625
  EditorField,
5991
6626
  {
@@ -6207,10 +6842,14 @@ function TableEditor({ block, onChange }) {
6207
6842
  ] });
6208
6843
  }
6209
6844
  var STATUSES2 = ["pending", "in_progress", "complete", "error"];
6845
+ var EMPTY_RICH_TEXT = {
6846
+ type: "rich_text",
6847
+ elements: [{ type: "rich_text_section", elements: [{ type: "text", text: "" }] }]
6848
+ };
6210
6849
  function TaskCardEditor({ block, onChange }) {
6211
6850
  const status = block.status ?? "pending";
6212
6851
  return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-4", children: [
6213
- /* @__PURE__ */ jsx(EditorField, { label: "Title", help: "Short label shown at the top of the card.", htmlFor: "task-card-title", children: /* @__PURE__ */ jsx(
6852
+ /* @__PURE__ */ jsx(EditorField, { label: "Title", help: "Title of the task in plain text.", htmlFor: "task-card-title", children: /* @__PURE__ */ jsx(
6214
6853
  Input,
6215
6854
  {
6216
6855
  id: "task-card-title",
@@ -6219,24 +6858,16 @@ function TaskCardEditor({ block, onChange }) {
6219
6858
  onChange: (e) => onChange({ ...block, title: e.target.value })
6220
6859
  }
6221
6860
  ) }),
6222
- /* @__PURE__ */ jsx(
6223
- EditorField,
6861
+ /* @__PURE__ */ jsx(EditorField, { label: "Task ID", help: "ID for the task.", htmlFor: "task-card-id", children: /* @__PURE__ */ jsx(
6862
+ Input,
6224
6863
  {
6225
- label: "Task ID",
6226
- help: "Stable identifier you can reference from interaction payloads.",
6227
- htmlFor: "task-card-id",
6228
- children: /* @__PURE__ */ jsx(
6229
- Input,
6230
- {
6231
- id: "task-card-id",
6232
- value: block.task_id,
6233
- placeholder: "e.g. task_1",
6234
- onChange: (e) => onChange({ ...block, task_id: e.target.value })
6235
- }
6236
- )
6864
+ id: "task-card-id",
6865
+ value: block.task_id,
6866
+ placeholder: "e.g. task_1",
6867
+ onChange: (e) => onChange({ ...block, task_id: e.target.value })
6237
6868
  }
6238
- ),
6239
- /* @__PURE__ */ jsx(EditorField, { label: "Status", help: "Slack renders a matching status chip on the card.", children: /* @__PURE__ */ jsx(
6869
+ ) }),
6870
+ /* @__PURE__ */ jsx(EditorField, { label: "Status", help: 'The state of a task. Can be "pending", "in_progress", "complete", or "error".', children: /* @__PURE__ */ jsx(
6240
6871
  RadioGroup,
6241
6872
  {
6242
6873
  value: status,
@@ -6248,6 +6879,24 @@ function TaskCardEditor({ block, onChange }) {
6248
6879
  ] }, s))
6249
6880
  }
6250
6881
  ) }),
6882
+ /* @__PURE__ */ jsx(
6883
+ RichTextField,
6884
+ {
6885
+ label: "Details",
6886
+ help: "Details of the task in the form of a single rich_text entity.",
6887
+ value: block.details,
6888
+ onChange: (next) => onChange({ ...block, details: next })
6889
+ }
6890
+ ),
6891
+ /* @__PURE__ */ jsx(
6892
+ RichTextField,
6893
+ {
6894
+ label: "Output",
6895
+ help: "Output of the task in the form of a single rich_text entity.",
6896
+ value: block.output,
6897
+ onChange: (next) => onChange({ ...block, output: next })
6898
+ }
6899
+ ),
6251
6900
  /* @__PURE__ */ jsx(
6252
6901
  SourcesField,
6253
6902
  {
@@ -6257,6 +6906,51 @@ function TaskCardEditor({ block, onChange }) {
6257
6906
  )
6258
6907
  ] });
6259
6908
  }
6909
+ function RichTextField({
6910
+ label,
6911
+ help,
6912
+ value,
6913
+ onChange
6914
+ }) {
6915
+ if (!value) {
6916
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1.5", children: [
6917
+ /* @__PURE__ */ jsx(Label, { children: label }),
6918
+ /* @__PURE__ */ jsxs(
6919
+ Button,
6920
+ {
6921
+ type: "button",
6922
+ size: "sm",
6923
+ variant: "outline",
6924
+ className: "self-start",
6925
+ onClick: () => onChange(EMPTY_RICH_TEXT),
6926
+ children: [
6927
+ /* @__PURE__ */ jsx(Plus, { className: "h-3.5 w-3.5" }),
6928
+ " Add ",
6929
+ label.toLowerCase()
6930
+ ]
6931
+ }
6932
+ ),
6933
+ help && /* @__PURE__ */ jsx("p", { className: "text-[11px] leading-snug text-muted-foreground", children: help })
6934
+ ] });
6935
+ }
6936
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1.5", children: [
6937
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
6938
+ /* @__PURE__ */ jsx(Label, { children: label }),
6939
+ /* @__PURE__ */ jsx(
6940
+ "button",
6941
+ {
6942
+ type: "button",
6943
+ "aria-label": `Remove ${label.toLowerCase()}`,
6944
+ onClick: () => onChange(void 0),
6945
+ className: "rounded p-1 text-muted-foreground hover:bg-destructive/10 hover:text-destructive",
6946
+ children: /* @__PURE__ */ jsx(Trash2, { className: "h-3.5 w-3.5" })
6947
+ }
6948
+ )
6949
+ ] }),
6950
+ /* @__PURE__ */ jsx(RichTextEditor, { block: value, onChange }),
6951
+ help && /* @__PURE__ */ jsx("p", { className: "text-[11px] leading-snug text-muted-foreground", children: help })
6952
+ ] });
6953
+ }
6260
6954
  function SourcesField({
6261
6955
  sources,
6262
6956
  onChange
@@ -6270,7 +6964,7 @@ function SourcesField({
6270
6964
  };
6271
6965
  return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2 rounded-md border bg-muted/20 p-3", children: [
6272
6966
  /* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-foreground", children: "Sources" }),
6273
- sources.length === 0 ? /* @__PURE__ */ jsx("p", { className: "text-[11px] leading-snug text-muted-foreground", children: "No sources. Add one to cite a document or link beneath the card." }) : null,
6967
+ /* @__PURE__ */ jsx("p", { className: "text-[11px] leading-snug text-muted-foreground", children: "Array of URL source elements used to generate a response." }),
6274
6968
  sources.map((src, idx) => /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2 rounded border bg-background p-2", children: [
6275
6969
  /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
6276
6970
  /* @__PURE__ */ jsxs("span", { className: "text-xs font-medium text-foreground", children: [
@@ -7481,6 +8175,7 @@ function BlockKitchen(props) {
7481
8175
  initialBlocks,
7482
8176
  onChange,
7483
8177
  previewHooks,
8178
+ customEmojis,
7484
8179
  loadChannels,
7485
8180
  loadSendAsUserStatus,
7486
8181
  onSend,
@@ -7524,6 +8219,13 @@ function BlockKitchen(props) {
7524
8219
  const [previewSurface, setPreviewSurface] = useState(allowedSurfaces[0]);
7525
8220
  const [activePaletteVariantId, setActivePaletteVariantId] = useState(null);
7526
8221
  const isMobile = useIsMobile();
8222
+ const mergedPreviewHooks = useMemo(() => {
8223
+ if (!customEmojis || customEmojis.length === 0) {
8224
+ return previewHooks;
8225
+ }
8226
+ const byName = new Map(customEmojis.map((emoji) => [emoji.name, emoji]));
8227
+ return { emoji: makeEmojiHook(byName), ...previewHooks };
8228
+ }, [customEmojis, previewHooks]);
7527
8229
  const validation = useBlockKitValidation(blocks, "message");
7528
8230
  const sensors = useSensors(
7529
8231
  useSensor(PointerSensor, { activationConstraint: { distance: 4 } }),
@@ -7573,7 +8275,7 @@ function BlockKitchen(props) {
7573
8275
  }, []);
7574
8276
  const activePaletteVariant = activePaletteVariantId ? variantById.get(activePaletteVariantId) : null;
7575
8277
  const blockPayloads = blocks.map((b) => b.block);
7576
- return /* @__PURE__ */ jsx(BrandThemeScope, { theme, children: /* @__PURE__ */ jsx(TooltipProvider, { delayDuration: 200, children: /* @__PURE__ */ jsxs(
8278
+ return /* @__PURE__ */ jsx(BrandThemeScope, { theme, children: /* @__PURE__ */ jsx(CustomEmojiProvider, { customEmojis, children: /* @__PURE__ */ jsx(TooltipProvider, { delayDuration: 200, children: /* @__PURE__ */ jsxs(
7577
8279
  DndContext,
7578
8280
  {
7579
8281
  sensors,
@@ -7619,7 +8321,7 @@ function BlockKitchen(props) {
7619
8321
  {
7620
8322
  blocks,
7621
8323
  workspaceName,
7622
- previewHooks,
8324
+ previewHooks: mergedPreviewHooks,
7623
8325
  previewTheme,
7624
8326
  previewSurface,
7625
8327
  errorsByBlockId: validation.byBlockId,
@@ -7695,7 +8397,7 @@ function BlockKitchen(props) {
7695
8397
  )
7696
8398
  ]
7697
8399
  }
7698
- ) }) });
8400
+ ) }) }) });
7699
8401
  }
7700
8402
  var UNCATEGORIZED = "Other";
7701
8403
  function TemplateThumbnail({ blocks, theme }) {