@tightknitai/block-kitchen 0.8.3 → 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.cjs CHANGED
@@ -4,10 +4,10 @@ var core = require('@dnd-kit/core');
4
4
  var sortable = require('@dnd-kit/sortable');
5
5
  var lucideReact = require('lucide-react');
6
6
  var React = require('react');
7
+ var jsxRuntime = require('react/jsx-runtime');
7
8
  var DialogPrimitive = require('@radix-ui/react-dialog');
8
9
  var clsx = require('clsx');
9
10
  var tailwindMerge = require('tailwind-merge');
10
- var jsxRuntime = require('react/jsx-runtime');
11
11
  var TooltipPrimitive = require('@radix-ui/react-tooltip');
12
12
  var slackBlockKitValidator = require('@tightknitai/slack-block-kit-validator');
13
13
  var nanoid = require('nanoid');
@@ -53,6 +53,35 @@ var Link__default = /*#__PURE__*/_interopDefault(Link);
53
53
  var StarterKit__default = /*#__PURE__*/_interopDefault(StarterKit);
54
54
 
55
55
  // src/components/block-kitchen.tsx
56
+ function makeEmojiHook(byName) {
57
+ return (data, parse) => {
58
+ const entry = byName.get(data.name);
59
+ if (entry) {
60
+ if (entry.url) {
61
+ return /* @__PURE__ */ jsxRuntime.jsx(
62
+ "img",
63
+ {
64
+ src: entry.url,
65
+ alt: `:${data.name}:`,
66
+ title: `:${data.name}:`,
67
+ className: "bk-custom-emoji",
68
+ style: {
69
+ display: "inline-block",
70
+ width: "1.25em",
71
+ height: "1.25em",
72
+ verticalAlign: "-0.25em",
73
+ objectFit: "contain"
74
+ }
75
+ }
76
+ );
77
+ }
78
+ if (entry.alias) {
79
+ return parse({ name: entry.alias, unicode: data.unicode, skin_tone: data.skin_tone });
80
+ }
81
+ }
82
+ return parse(data);
83
+ };
84
+ }
56
85
  var defaultPalette = [
57
86
  {
58
87
  name: "Agents",
@@ -1549,50 +1578,63 @@ function useIsMobile() {
1549
1578
  }, []);
1550
1579
  return isMobile;
1551
1580
  }
1581
+ var EMPTY = {
1582
+ customEmojis: [],
1583
+ byName: /* @__PURE__ */ new Map()
1584
+ };
1585
+ var CustomEmojiContext = React.createContext(EMPTY);
1586
+ function CustomEmojiProvider({ customEmojis, children }) {
1587
+ const value = React.useMemo(() => {
1588
+ if (!customEmojis || customEmojis.length === 0) {
1589
+ return EMPTY;
1590
+ }
1591
+ const byName = /* @__PURE__ */ new Map();
1592
+ for (const emoji of customEmojis) {
1593
+ byName.set(emoji.name, emoji);
1594
+ }
1595
+ return { customEmojis, byName };
1596
+ }, [customEmojis]);
1597
+ return /* @__PURE__ */ jsxRuntime.jsx(CustomEmojiContext.Provider, { value, children });
1598
+ }
1599
+ function useCustomEmojis() {
1600
+ return React.useContext(CustomEmojiContext);
1601
+ }
1552
1602
 
1553
1603
  // src/lib/error-grouping.ts
1554
1604
  function toValidatorSurface(surface) {
1555
1605
  return surface === "app_home" ? "home" : surface;
1556
1606
  }
1557
- function extractBlockIndex(path) {
1558
- if (!path || path === "(root)") {
1559
- return null;
1560
- }
1561
- const match = /^\/(\d+)(?:\/|$)/.exec(path);
1562
- if (!match) {
1563
- return null;
1607
+ var BLOCK_PREFIX = /^blocks\[(\d+)\]/;
1608
+ function extractBlockIndex(raw) {
1609
+ const match = BLOCK_PREFIX.exec(raw);
1610
+ return match ? Number.parseInt(match[1], 10) : null;
1611
+ }
1612
+ function toBlockMessage(raw) {
1613
+ const rest = raw.replace(BLOCK_PREFIX, "");
1614
+ if (rest.startsWith(":")) {
1615
+ return rest.replace(/^:\s*/, "");
1564
1616
  }
1565
- return Number.parseInt(match[1], 10);
1617
+ return rest.replace(/^\./, "");
1566
1618
  }
1567
- function stripBlockPrefix(path) {
1568
- return path.replace(/^\/\d+/, "") || "(root)";
1619
+ function toGeneralMessage(raw) {
1620
+ return raw.startsWith("(root)") ? raw.replace(/^\(root\):?\s*/, "") : raw;
1569
1621
  }
1570
1622
  function groupValidatorErrors(errors, blocks) {
1571
1623
  const byBlockId = /* @__PURE__ */ new Map();
1572
1624
  const general = [];
1573
1625
  for (const raw of errors) {
1574
- const space = raw.indexOf(" ");
1575
- const path = space === -1 ? "(root)" : raw.slice(0, space);
1576
- const message = space === -1 ? raw : raw.slice(space + 1);
1577
- const idx = extractBlockIndex(path);
1626
+ const idx = extractBlockIndex(raw);
1578
1627
  if (idx === null || idx >= blocks.length) {
1579
- general.push(raw);
1628
+ general.push(toGeneralMessage(raw));
1580
1629
  continue;
1581
1630
  }
1582
1631
  const id = blocks[idx].id;
1583
- const friendly = formatMessage(stripBlockPrefix(path), message);
1584
1632
  const bucket = byBlockId.get(id) ?? [];
1585
- bucket.push(friendly);
1633
+ bucket.push(toBlockMessage(raw));
1586
1634
  byBlockId.set(id, bucket);
1587
1635
  }
1588
1636
  return { byBlockId, general, total: errors.length };
1589
1637
  }
1590
- function formatMessage(relativePath, message) {
1591
- if (relativePath === "" || relativePath === "(root)") {
1592
- return message;
1593
- }
1594
- return `${relativePath}: ${message}`;
1595
- }
1596
1638
 
1597
1639
  // src/lib/url-safety.ts
1598
1640
  var SAFE_LINK_PROTOCOLS = /^(https?|ircs?|mailto|tel|sms|xmpp)$/i;
@@ -1712,10 +1754,47 @@ function toSlackBlocks(blocks) {
1712
1754
  });
1713
1755
  }
1714
1756
 
1757
+ // src/lib/validation-sanitize.ts
1758
+ var VALIDATION_EMOJI_PLACEHOLDER = "white_check_mark";
1759
+ function rewriteEmojiNames(value) {
1760
+ if (Array.isArray(value)) {
1761
+ let changed = false;
1762
+ const next = value.map((item) => {
1763
+ const rewritten = rewriteEmojiNames(item);
1764
+ if (rewritten !== item) {
1765
+ changed = true;
1766
+ }
1767
+ return rewritten;
1768
+ });
1769
+ return changed ? next : value;
1770
+ }
1771
+ if (value !== null && typeof value === "object") {
1772
+ const source = value;
1773
+ let changed = false;
1774
+ const next = {};
1775
+ for (const key of Object.keys(source)) {
1776
+ const rewritten = rewriteEmojiNames(source[key]);
1777
+ next[key] = rewritten;
1778
+ if (rewritten !== source[key]) {
1779
+ changed = true;
1780
+ }
1781
+ }
1782
+ if (next.type === "emoji" && typeof next.name === "string" && next.name !== VALIDATION_EMOJI_PLACEHOLDER) {
1783
+ next.name = VALIDATION_EMOJI_PLACEHOLDER;
1784
+ changed = true;
1785
+ }
1786
+ return changed ? next : value;
1787
+ }
1788
+ return value;
1789
+ }
1790
+ function stripCustomEmojiForValidation(blocks) {
1791
+ return blocks.map((block) => rewriteEmojiNames(block));
1792
+ }
1793
+
1715
1794
  // src/state/use-block-kit-validation.ts
1716
1795
  var DEBOUNCE_MS = 150;
1717
1796
  function computeValidation(blocks, surface) {
1718
- const payload = toSlackBlocks(blocks.map((b) => b.block));
1797
+ const payload = stripCustomEmojiForValidation(toSlackBlocks(blocks.map((b) => b.block)));
1719
1798
  const result = slackBlockKitValidator.validateBlockKit(payload, {
1720
1799
  target: "blocks",
1721
1800
  surface: toValidatorSurface(surface)
@@ -3561,52 +3640,55 @@ function ContextEditor({ block, onChange }) {
3561
3640
  {
3562
3641
  label: "Items",
3563
3642
  help: "Shown in a horizontal row, usually as footnote-style content. Slack supports up to 10 items.",
3564
- children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-col gap-2", children: elements.map((el, idx) => {
3565
- if (el.type === "mrkdwn" || el.type === "plain_text") {
3566
- return /* @__PURE__ */ jsxRuntime.jsx(
3567
- TextItemCard,
3568
- {
3569
- index: idx,
3570
- element: el,
3571
- onChange: (next) => updateAt(idx, () => next),
3572
- onRemove: () => removeAt(idx)
3573
- },
3574
- idx
3575
- );
3576
- }
3577
- if (el.type === "image" && "image_url" in el) {
3578
- return /* @__PURE__ */ jsxRuntime.jsx(
3579
- ImageItemCard,
3643
+ children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-2", children: [
3644
+ elements.length === 0 ? /* @__PURE__ */ jsxRuntime.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,
3645
+ elements.map((el, idx) => {
3646
+ if (el.type === "mrkdwn" || el.type === "plain_text") {
3647
+ return /* @__PURE__ */ jsxRuntime.jsx(
3648
+ TextItemCard,
3649
+ {
3650
+ index: idx,
3651
+ element: el,
3652
+ onChange: (next) => updateAt(idx, () => next),
3653
+ onRemove: () => removeAt(idx)
3654
+ },
3655
+ idx
3656
+ );
3657
+ }
3658
+ if (el.type === "image" && "image_url" in el) {
3659
+ return /* @__PURE__ */ jsxRuntime.jsx(
3660
+ ImageItemCard,
3661
+ {
3662
+ index: idx,
3663
+ element: el,
3664
+ onChange: (next) => updateAt(idx, () => next),
3665
+ onRemove: () => removeAt(idx)
3666
+ },
3667
+ idx
3668
+ );
3669
+ }
3670
+ return /* @__PURE__ */ jsxRuntime.jsxs(
3671
+ "div",
3580
3672
  {
3581
- index: idx,
3582
- element: el,
3583
- onChange: (next) => updateAt(idx, () => next),
3584
- onRemove: () => removeAt(idx)
3673
+ className: "flex items-center justify-between rounded border bg-muted/30 px-2 py-1.5 text-xs text-muted-foreground",
3674
+ children: [
3675
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: "Item not editable in the visual builder" }),
3676
+ /* @__PURE__ */ jsxRuntime.jsx(
3677
+ "button",
3678
+ {
3679
+ type: "button",
3680
+ "aria-label": "Remove item",
3681
+ onClick: () => removeAt(idx),
3682
+ className: "text-muted-foreground hover:text-destructive",
3683
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Trash2, { className: "h-3.5 w-3.5" })
3684
+ }
3685
+ )
3686
+ ]
3585
3687
  },
3586
3688
  idx
3587
3689
  );
3588
- }
3589
- return /* @__PURE__ */ jsxRuntime.jsxs(
3590
- "div",
3591
- {
3592
- className: "flex items-center justify-between rounded border bg-muted/30 px-2 py-1.5 text-xs text-muted-foreground",
3593
- children: [
3594
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: "Item not editable in the visual builder" }),
3595
- /* @__PURE__ */ jsxRuntime.jsx(
3596
- "button",
3597
- {
3598
- type: "button",
3599
- "aria-label": "Remove item",
3600
- onClick: () => removeAt(idx),
3601
- className: "text-muted-foreground hover:text-destructive",
3602
- children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Trash2, { className: "h-3.5 w-3.5" })
3603
- }
3604
- )
3605
- ]
3606
- },
3607
- idx
3608
- );
3609
- }) })
3690
+ })
3691
+ ] })
3610
3692
  }
3611
3693
  ),
3612
3694
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-2", children: [
@@ -3745,12 +3827,423 @@ function ImageItemCard({
3745
3827
  function DividerEditor() {
3746
3828
  return /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-muted-foreground", children: "A horizontal line that separates content. This block has no options to configure." });
3747
3829
  }
3830
+
3831
+ // src/lib/emoji-data.ts
3832
+ var TONE_CODEPOINT_TO_SKIN_TONE = {
3833
+ "1F3FB": 2,
3834
+ "1F3FC": 3,
3835
+ "1F3FD": 4,
3836
+ "1F3FE": 5,
3837
+ "1F3FF": 6
3838
+ };
3839
+ var CATEGORY_ORDER = [
3840
+ "Smileys & Emotion",
3841
+ "People & Body",
3842
+ "Animals & Nature",
3843
+ "Food & Drink",
3844
+ "Travel & Places",
3845
+ "Activities",
3846
+ "Objects",
3847
+ "Symbols",
3848
+ "Flags"
3849
+ ];
3850
+ function codepointsToGlyph(unified) {
3851
+ return unified.split("-").map((cp) => {
3852
+ const n = Number.parseInt(cp, 16);
3853
+ return Number.isFinite(n) ? String.fromCodePoint(n) : "";
3854
+ }).join("");
3855
+ }
3856
+ function unifiedToUnicode(unified) {
3857
+ return unified.toLowerCase();
3858
+ }
3859
+ function composeTextEmoji(name, skinTone) {
3860
+ if (skinTone && skinTone >= 2 && skinTone <= 6) {
3861
+ return `:${name}::skin-tone-${skinTone}:`;
3862
+ }
3863
+ return `:${name}:`;
3864
+ }
3865
+ function buildEmojiIndex(raw) {
3866
+ const all = [];
3867
+ const byName = /* @__PURE__ */ new Map();
3868
+ const byUnified = /* @__PURE__ */ new Map();
3869
+ for (const r of raw) {
3870
+ if (!CATEGORY_ORDER.includes(r.category)) {
3871
+ continue;
3872
+ }
3873
+ const shortNames = r.short_names?.length ? r.short_names : [r.short_name];
3874
+ const skinUnified = {};
3875
+ if (r.skin_variations) {
3876
+ for (const [toneKey, variation] of Object.entries(r.skin_variations)) {
3877
+ const skinTone = TONE_CODEPOINT_TO_SKIN_TONE[toneKey];
3878
+ if (skinTone && variation?.unified) {
3879
+ skinUnified[skinTone] = variation.unified;
3880
+ }
3881
+ }
3882
+ }
3883
+ const datum = {
3884
+ name: shortNames[0],
3885
+ shortNames,
3886
+ unified: r.unified,
3887
+ category: r.category,
3888
+ sortOrder: r.sort_order,
3889
+ skinUnified
3890
+ };
3891
+ all.push(datum);
3892
+ byUnified.set(r.unified.toUpperCase(), datum);
3893
+ for (const sn of shortNames) {
3894
+ if (!byName.has(sn)) {
3895
+ byName.set(sn, datum);
3896
+ }
3897
+ }
3898
+ }
3899
+ all.sort((a, b) => a.sortOrder - b.sortOrder);
3900
+ const byCategory = CATEGORY_ORDER.map((category) => ({
3901
+ category,
3902
+ emoji: all.filter((e) => e.category === category)
3903
+ })).filter((group) => group.emoji.length > 0);
3904
+ return { all, byName, byUnified, byCategory };
3905
+ }
3906
+ var indexPromise = null;
3907
+ var cachedIndex = null;
3908
+ async function loadEmojiIndex() {
3909
+ if (cachedIndex) {
3910
+ return cachedIndex;
3911
+ }
3912
+ if (!indexPromise) {
3913
+ indexPromise = import('emoji-datasource/emoji.json').then((mod) => {
3914
+ const raw = mod.default ?? mod;
3915
+ cachedIndex = buildEmojiIndex(raw);
3916
+ return cachedIndex;
3917
+ }).catch((err) => {
3918
+ indexPromise = null;
3919
+ throw err;
3920
+ });
3921
+ }
3922
+ return indexPromise;
3923
+ }
3924
+ function getEmojiIndex() {
3925
+ return cachedIndex;
3926
+ }
3927
+
3928
+ // src/lib/emoji-recents.ts
3929
+ var STORAGE_KEY = "bk:emoji-recents";
3930
+ var SKIN_TONE_KEY = "bk:emoji-skin-tone";
3931
+ var MAX_RECENTS = 24;
3932
+ function readEmojiSkinTone() {
3933
+ try {
3934
+ const raw = globalThis.localStorage?.getItem(SKIN_TONE_KEY);
3935
+ if (!raw) {
3936
+ return null;
3937
+ }
3938
+ const n = Number.parseInt(raw, 10);
3939
+ return n >= 2 && n <= 6 ? n : null;
3940
+ } catch {
3941
+ return null;
3942
+ }
3943
+ }
3944
+ function writeEmojiSkinTone(skinTone) {
3945
+ try {
3946
+ if (skinTone && skinTone >= 2 && skinTone <= 6) {
3947
+ globalThis.localStorage?.setItem(SKIN_TONE_KEY, String(skinTone));
3948
+ } else {
3949
+ globalThis.localStorage?.removeItem(SKIN_TONE_KEY);
3950
+ }
3951
+ } catch {
3952
+ }
3953
+ }
3954
+ function readEmojiRecents() {
3955
+ try {
3956
+ const raw = globalThis.localStorage?.getItem(STORAGE_KEY);
3957
+ if (!raw) {
3958
+ return [];
3959
+ }
3960
+ const parsed = JSON.parse(raw);
3961
+ return Array.isArray(parsed) ? parsed.filter((n) => typeof n === "string") : [];
3962
+ } catch {
3963
+ return [];
3964
+ }
3965
+ }
3966
+ function pushEmojiRecent(name) {
3967
+ const next = [name, ...readEmojiRecents().filter((n) => n !== name)].slice(0, MAX_RECENTS);
3968
+ try {
3969
+ globalThis.localStorage?.setItem(STORAGE_KEY, JSON.stringify(next));
3970
+ } catch {
3971
+ }
3972
+ return next;
3973
+ }
3974
+ var SKIN_TONES = [
3975
+ { tone: null, label: "Default", swatch: "\u270B" },
3976
+ { tone: 2, label: "Light", swatch: "\u270B\u{1F3FB}" },
3977
+ { tone: 3, label: "Medium-light", swatch: "\u270B\u{1F3FC}" },
3978
+ { tone: 4, label: "Medium", swatch: "\u270B\u{1F3FD}" },
3979
+ { tone: 5, label: "Medium-dark", swatch: "\u270B\u{1F3FE}" },
3980
+ { tone: 6, label: "Dark", swatch: "\u270B\u{1F3FF}" }
3981
+ ];
3982
+ function EmojiPicker({
3983
+ onSelect,
3984
+ autoFocus = true
3985
+ }) {
3986
+ const { customEmojis } = useCustomEmojis();
3987
+ const [index, setIndex] = React.useState(null);
3988
+ const [loadError, setLoadError] = React.useState(false);
3989
+ const [query, setQuery] = React.useState("");
3990
+ const [skinTone, setSkinTone] = React.useState(() => readEmojiSkinTone());
3991
+ const [recents, setRecents] = React.useState(() => readEmojiRecents());
3992
+ const searchRef = React.useRef(null);
3993
+ React.useEffect(() => {
3994
+ let active = true;
3995
+ loadEmojiIndex().then((idx) => {
3996
+ if (active) {
3997
+ setIndex(idx);
3998
+ }
3999
+ }).catch(() => {
4000
+ if (active) {
4001
+ setLoadError(true);
4002
+ }
4003
+ });
4004
+ return () => {
4005
+ active = false;
4006
+ };
4007
+ }, []);
4008
+ React.useEffect(() => {
4009
+ if (autoFocus) {
4010
+ searchRef.current?.focus();
4011
+ }
4012
+ }, [autoFocus]);
4013
+ const customByName = React.useMemo(() => {
4014
+ const map = /* @__PURE__ */ new Map();
4015
+ for (const e of customEmojis) {
4016
+ map.set(e.name, e);
4017
+ }
4018
+ return map;
4019
+ }, [customEmojis]);
4020
+ const customImageEmoji = React.useMemo(() => customEmojis.filter((e) => e.url), [customEmojis]);
4021
+ const handlePick = (selection) => {
4022
+ setRecents(pushEmojiRecent(selection.name));
4023
+ onSelect(selection);
4024
+ };
4025
+ const handleSkinTone = (tone) => {
4026
+ setSkinTone(tone);
4027
+ writeEmojiSkinTone(tone);
4028
+ };
4029
+ const sections = React.useMemo(() => {
4030
+ if (!index) {
4031
+ return [];
4032
+ }
4033
+ const trimmed = query.trim().toLowerCase();
4034
+ if (trimmed) {
4035
+ const standard = index.all.filter((d) => d.shortNames.some((sn) => sn.includes(trimmed))).slice(0, 120).map((datum) => ({ datum }));
4036
+ const custom = customImageEmoji.filter((e) => e.name.toLowerCase().includes(trimmed)).map((custom2) => ({ custom: custom2 }));
4037
+ return [{ key: "search", title: "Search results", emoji: [...custom, ...standard] }];
4038
+ }
4039
+ const out = [];
4040
+ if (recents.length > 0) {
4041
+ const recentEmoji = recents.map((name) => {
4042
+ const custom = customByName.get(name);
4043
+ if (custom?.url) {
4044
+ return { custom };
4045
+ }
4046
+ const datum = index.byName.get(name);
4047
+ return datum ? { datum } : null;
4048
+ }).filter((e) => e !== null);
4049
+ if (recentEmoji.length > 0) {
4050
+ out.push({ key: "recents", title: "Frequently used", emoji: recentEmoji });
4051
+ }
4052
+ }
4053
+ if (customImageEmoji.length > 0) {
4054
+ out.push({ key: "custom", title: "Custom", emoji: customImageEmoji.map((custom) => ({ custom })) });
4055
+ }
4056
+ for (const group of index.byCategory) {
4057
+ out.push({ key: group.category, title: group.category, emoji: group.emoji.map((datum) => ({ datum })) });
4058
+ }
4059
+ return out;
4060
+ }, [index, query, recents, customByName, customImageEmoji]);
4061
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "bk-emoji-picker flex w-[320px] max-w-[90vw] flex-col gap-2", children: [
4062
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
4063
+ /* @__PURE__ */ jsxRuntime.jsx(
4064
+ Input,
4065
+ {
4066
+ ref: searchRef,
4067
+ value: query,
4068
+ placeholder: "Search emoji\u2026",
4069
+ "aria-label": "Search emoji",
4070
+ onChange: (e) => setQuery(e.target.value),
4071
+ className: "h-8"
4072
+ }
4073
+ ),
4074
+ /* @__PURE__ */ jsxRuntime.jsx(SkinToneSelector, { value: skinTone, onChange: handleSkinTone })
4075
+ ] }),
4076
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-64 overflow-y-auto pr-1", children: loadError ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "p-4 text-center text-xs text-muted-foreground", children: "Couldn't load emoji." }) : !index ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "p-4 text-center text-xs text-muted-foreground", children: "Loading emoji\u2026" }) : sections.every((s) => s.emoji.length === 0) ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "p-4 text-center text-xs text-muted-foreground", children: "No emoji found." }) : sections.map((section) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mb-2", children: [
4077
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "sticky top-0 z-10 bg-background px-1 py-1 text-[11px] font-medium text-muted-foreground", children: section.title }),
4078
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-8 gap-0.5", children: section.emoji.map(
4079
+ (entry, i) => entry.custom ? /* @__PURE__ */ jsxRuntime.jsx(
4080
+ CustomEmojiButton,
4081
+ {
4082
+ emoji: entry.custom,
4083
+ onPick: handlePick
4084
+ },
4085
+ `${section.key}-${entry.custom.name}-${i}`
4086
+ ) : entry.datum ? /* @__PURE__ */ jsxRuntime.jsx(
4087
+ StandardEmojiButton,
4088
+ {
4089
+ datum: entry.datum,
4090
+ skinTone,
4091
+ onPick: handlePick
4092
+ },
4093
+ `${section.key}-${entry.datum.name}-${i}`
4094
+ ) : null
4095
+ ) })
4096
+ ] }, section.key)) })
4097
+ ] });
4098
+ }
4099
+ function resolveStandard(datum, skinTone) {
4100
+ const tonedUnified = skinTone ? datum.skinUnified[skinTone] : void 0;
4101
+ return {
4102
+ name: datum.name,
4103
+ unicode: unifiedToUnicode(tonedUnified ?? datum.unified),
4104
+ src: null,
4105
+ skinTone: tonedUnified ? skinTone : null
4106
+ };
4107
+ }
4108
+ var emojiButtonClass = "flex h-8 w-8 cursor-pointer items-center justify-center rounded text-lg leading-none hover:bg-accent";
4109
+ function StandardEmojiButton({
4110
+ datum,
4111
+ skinTone,
4112
+ onPick
4113
+ }) {
4114
+ const tonedUnified = skinTone && datum.skinUnified[skinTone] || datum.unified;
4115
+ return /* @__PURE__ */ jsxRuntime.jsx(
4116
+ "button",
4117
+ {
4118
+ type: "button",
4119
+ className: emojiButtonClass,
4120
+ title: `:${datum.name}:`,
4121
+ "aria-label": datum.name,
4122
+ onClick: () => onPick(resolveStandard(datum, skinTone)),
4123
+ children: codepointsToGlyph(tonedUnified)
4124
+ }
4125
+ );
4126
+ }
4127
+ function CustomEmojiButton({ emoji, onPick }) {
4128
+ return /* @__PURE__ */ jsxRuntime.jsx(
4129
+ "button",
4130
+ {
4131
+ type: "button",
4132
+ className: emojiButtonClass,
4133
+ title: `:${emoji.name}:`,
4134
+ "aria-label": emoji.name,
4135
+ onClick: () => onPick({ name: emoji.name, unicode: null, src: emoji.url, skinTone: null }),
4136
+ children: emoji.url ? /* @__PURE__ */ jsxRuntime.jsx("img", { src: emoji.url, alt: `:${emoji.name}:`, className: "h-5 w-5 object-contain" }) : /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-[10px]", children: [
4137
+ ":",
4138
+ emoji.name,
4139
+ ":"
4140
+ ] })
4141
+ }
4142
+ );
4143
+ }
4144
+ function SkinToneSelector({ value, onChange }) {
4145
+ const [open, setOpen] = React.useState(false);
4146
+ const active = SKIN_TONES.find((t) => t.tone === value) ?? SKIN_TONES[0];
4147
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative shrink-0", children: [
4148
+ /* @__PURE__ */ jsxRuntime.jsx(
4149
+ "button",
4150
+ {
4151
+ type: "button",
4152
+ "aria-label": `Skin tone: ${active.label}`,
4153
+ title: `Skin tone: ${active.label}`,
4154
+ className: "flex h-8 w-8 items-center justify-center rounded border text-lg leading-none hover:bg-accent",
4155
+ onClick: () => setOpen((o) => !o),
4156
+ children: active.swatch
4157
+ }
4158
+ ),
4159
+ open && /* @__PURE__ */ jsxRuntime.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__ */ jsxRuntime.jsx(
4160
+ "button",
4161
+ {
4162
+ type: "button",
4163
+ "aria-label": t.label,
4164
+ title: t.label,
4165
+ className: cn(emojiButtonClass, value === t.tone && "bg-accent"),
4166
+ onClick: () => {
4167
+ onChange(t.tone);
4168
+ setOpen(false);
4169
+ },
4170
+ children: t.swatch
4171
+ },
4172
+ t.label
4173
+ )) })
4174
+ ] });
4175
+ }
4176
+ function EmojiPickerButton({
4177
+ onSelect,
4178
+ className,
4179
+ label = "Insert emoji",
4180
+ align = "start"
4181
+ }) {
4182
+ const [open, setOpen] = React.useState(false);
4183
+ return /* @__PURE__ */ jsxRuntime.jsxs(Popover, { open, onOpenChange: setOpen, children: [
4184
+ /* @__PURE__ */ jsxRuntime.jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsx(
4185
+ "button",
4186
+ {
4187
+ type: "button",
4188
+ "aria-label": label,
4189
+ title: label,
4190
+ className: cn(
4191
+ "flex h-7 w-7 cursor-pointer items-center justify-center rounded text-muted-foreground transition-colors hover:bg-accent hover:text-foreground",
4192
+ className
4193
+ ),
4194
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Smile, { className: "h-3.5 w-3.5" })
4195
+ }
4196
+ ) }),
4197
+ /* @__PURE__ */ jsxRuntime.jsx(PopoverContent, { align, className: "w-auto p-2", children: /* @__PURE__ */ jsxRuntime.jsx(
4198
+ EmojiPicker,
4199
+ {
4200
+ onSelect: (selection) => {
4201
+ onSelect(selection);
4202
+ setOpen(false);
4203
+ }
4204
+ }
4205
+ ) })
4206
+ ] });
4207
+ }
4208
+ function EmojiTextInsertButton({
4209
+ targetRef,
4210
+ value,
4211
+ onChange,
4212
+ className,
4213
+ align = "end"
4214
+ }) {
4215
+ return /* @__PURE__ */ jsxRuntime.jsx(
4216
+ EmojiPickerButton,
4217
+ {
4218
+ align,
4219
+ label: "Insert emoji",
4220
+ className,
4221
+ onSelect: (sel) => {
4222
+ const el = targetRef.current;
4223
+ const insert = composeTextEmoji(sel.name, sel.skinTone ?? void 0);
4224
+ const start = el?.selectionStart ?? value.length;
4225
+ const end = el?.selectionEnd ?? value.length;
4226
+ const next = value.slice(0, start) + insert + value.slice(end);
4227
+ onChange(next);
4228
+ requestAnimationFrame(() => {
4229
+ if (el) {
4230
+ const caret = start + insert.length;
4231
+ el.focus();
4232
+ el.setSelectionRange(caret, caret);
4233
+ }
4234
+ });
4235
+ }
4236
+ }
4237
+ );
4238
+ }
3748
4239
  var HEADER_MAX = 150;
3749
4240
  var HEADER_LEVELS = [1, 2, 3, 4];
3750
4241
  function HeaderEditor({ block, onChange }) {
3751
4242
  const text = block.text?.text ?? "";
3752
4243
  const remaining = HEADER_MAX - text.length;
3753
4244
  const level = block.level;
4245
+ const inputRef = React.useRef(null);
4246
+ const setText = (next) => onChange({ ...block, text: { type: "plain_text", text: next, emoji: true } });
3754
4247
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-3", children: [
3755
4248
  /* @__PURE__ */ jsxRuntime.jsx(
3756
4249
  EditorField,
@@ -3758,23 +4251,20 @@ function HeaderEditor({ block, onChange }) {
3758
4251
  label: "Heading text",
3759
4252
  help: `Plain text only. ${remaining} characters remaining.`,
3760
4253
  htmlFor: "header-text",
3761
- children: /* @__PURE__ */ jsxRuntime.jsx(
3762
- Input,
3763
- {
3764
- id: "header-text",
3765
- value: text,
3766
- maxLength: HEADER_MAX,
3767
- placeholder: "e.g. Weekly roundup",
3768
- onChange: (e) => onChange({
3769
- ...block,
3770
- text: {
3771
- type: "plain_text",
3772
- text: e.target.value,
3773
- emoji: true
3774
- }
3775
- })
3776
- }
3777
- )
4254
+ children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5", children: [
4255
+ /* @__PURE__ */ jsxRuntime.jsx(
4256
+ Input,
4257
+ {
4258
+ ref: inputRef,
4259
+ id: "header-text",
4260
+ value: text,
4261
+ maxLength: HEADER_MAX,
4262
+ placeholder: "e.g. Weekly roundup",
4263
+ onChange: (e) => setText(e.target.value)
4264
+ }
4265
+ ),
4266
+ /* @__PURE__ */ jsxRuntime.jsx(EmojiTextInsertButton, { targetRef: inputRef, value: text, onChange: setText, className: "shrink-0 border" })
4267
+ ] })
3778
4268
  }
3779
4269
  ),
3780
4270
  /* @__PURE__ */ jsxRuntime.jsx(EditorField, { label: "Level", help: "Optional. Builder-only extension; Slack's API ignores this value.", children: /* @__PURE__ */ jsxRuntime.jsxs(
@@ -4637,22 +5127,29 @@ function CheckboxField({
4637
5127
  ] });
4638
5128
  }
4639
5129
  function MarkdownEditor({ block, onChange }) {
5130
+ const textRef = React.useRef(null);
5131
+ const text = block.text ?? "";
5132
+ const setText = (next) => onChange({ ...block, text: next });
4640
5133
  return /* @__PURE__ */ jsxRuntime.jsx(
4641
5134
  EditorField,
4642
5135
  {
4643
5136
  label: "Markdown",
4644
5137
  help: "Standard markdown with GFM: **bold**, _italic_, lists, tables, ```code```, [links](url).",
4645
5138
  htmlFor: "markdown-text",
4646
- children: /* @__PURE__ */ jsxRuntime.jsx(
4647
- Textarea,
4648
- {
4649
- id: "markdown-text",
4650
- value: block.text ?? "",
4651
- rows: 8,
4652
- placeholder: "e.g. **Roadmap**\\n\\n- Item one\\n- Item two",
4653
- onChange: (e) => onChange({ ...block, text: e.target.value })
4654
- }
4655
- )
5139
+ children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start gap-1.5", children: [
5140
+ /* @__PURE__ */ jsxRuntime.jsx(
5141
+ Textarea,
5142
+ {
5143
+ ref: textRef,
5144
+ id: "markdown-text",
5145
+ value: text,
5146
+ rows: 8,
5147
+ placeholder: "e.g. **Roadmap**\\n\\n- Item one\\n- Item two",
5148
+ onChange: (e) => setText(e.target.value)
5149
+ }
5150
+ ),
5151
+ /* @__PURE__ */ jsxRuntime.jsx(EmojiTextInsertButton, { targetRef: textRef, value: text, onChange: setText, className: "mt-1 shrink-0 border" })
5152
+ ] })
4656
5153
  }
4657
5154
  );
4658
5155
  }
@@ -4747,6 +5244,26 @@ function PlanEditor({ block, onChange }) {
4747
5244
  ] });
4748
5245
  }
4749
5246
 
5247
+ // src/lib/emoji-resolve.ts
5248
+ function makeEmojiImportResolver(customByName, index = getEmojiIndex()) {
5249
+ return (el) => {
5250
+ const name = el.name ?? "";
5251
+ const skinTone = el.skin_tone ?? null;
5252
+ const custom = customByName.get(name);
5253
+ if (custom?.url) {
5254
+ return { name, src: custom.url, unicode: null, skinTone };
5255
+ }
5256
+ const lookupName = custom?.alias ?? name;
5257
+ const datum = index?.byName.get(lookupName);
5258
+ let unicode = el.unicode ?? null;
5259
+ if (!unicode && datum) {
5260
+ const toned = skinTone ? datum.skinUnified[skinTone] : void 0;
5261
+ unicode = unifiedToUnicode(toned ?? datum.unified);
5262
+ }
5263
+ return { name, src: null, unicode, skinTone };
5264
+ };
5265
+ }
5266
+
4750
5267
  // src/lib/rich-text-tiptap.ts
4751
5268
  var SUPPORTED_BLOCK_KINDS = /* @__PURE__ */ new Set([
4752
5269
  "rich_text_section",
@@ -4754,7 +5271,7 @@ var SUPPORTED_BLOCK_KINDS = /* @__PURE__ */ new Set([
4754
5271
  "rich_text_quote",
4755
5272
  "rich_text_preformatted"
4756
5273
  ]);
4757
- var SUPPORTED_INLINE_KINDS = /* @__PURE__ */ new Set(["text", "link"]);
5274
+ var SUPPORTED_INLINE_KINDS = /* @__PURE__ */ new Set(["text", "link", "emoji"]);
4758
5275
  var SUPPORTED_TEXT_STYLE_KEYS = /* @__PURE__ */ new Set(["bold", "italic", "strike", "code"]);
4759
5276
  var MAX_LIST_INDENT = 8;
4760
5277
  function detectLossy(block) {
@@ -4801,16 +5318,22 @@ function scanInlines(inlines, where, out) {
4801
5318
  }
4802
5319
  });
4803
5320
  }
4804
- function richTextToProseMirror(block) {
5321
+ var defaultEmojiResolver = (el) => ({
5322
+ name: el.name ?? "",
5323
+ src: null,
5324
+ unicode: el.unicode ?? null,
5325
+ skinTone: el.skin_tone ?? null
5326
+ });
5327
+ function richTextToProseMirror(block, resolveEmoji = defaultEmojiResolver) {
4805
5328
  const content = [];
4806
5329
  const listsByIndent = /* @__PURE__ */ new Map();
4807
5330
  for (const el of block.elements ?? []) {
4808
5331
  if (el.type === "rich_text_list") {
4809
- appendRichTextList(el, content, listsByIndent);
5332
+ appendRichTextList(el, content, listsByIndent, resolveEmoji);
4810
5333
  continue;
4811
5334
  }
4812
5335
  listsByIndent.clear();
4813
- const node = blockElementToPM(el);
5336
+ const node = blockElementToPM(el, resolveEmoji);
4814
5337
  if (node) {
4815
5338
  content.push(node);
4816
5339
  }
@@ -4820,12 +5343,12 @@ function richTextToProseMirror(block) {
4820
5343
  content: content.length > 0 ? content : [{ type: "paragraph" }]
4821
5344
  };
4822
5345
  }
4823
- function appendRichTextList(el, topLevel, listsByIndent) {
5346
+ function appendRichTextList(el, topLevel, listsByIndent, resolveEmoji) {
4824
5347
  const indent = el.indent ?? 0;
4825
5348
  const listType = el.style === "ordered" ? "orderedList" : "bulletList";
4826
5349
  const items = (el.elements ?? []).map((section) => ({
4827
5350
  type: "listItem",
4828
- content: [{ type: "paragraph", content: inlinesToPM(section.elements ?? []) }]
5351
+ content: [{ type: "paragraph", content: inlinesToPM(section.elements ?? [], resolveEmoji) }]
4829
5352
  }));
4830
5353
  for (const k of Array.from(listsByIndent.keys())) {
4831
5354
  if (k > indent) {
@@ -4851,11 +5374,11 @@ function appendRichTextList(el, topLevel, listsByIndent) {
4851
5374
  const parentLastItem = parent.content[parent.content.length - 1];
4852
5375
  parentLastItem.content = [...parentLastItem.content ?? [], list];
4853
5376
  }
4854
- function blockElementToPM(el) {
5377
+ function blockElementToPM(el, resolveEmoji) {
4855
5378
  if (el.type === "rich_text_section") {
4856
5379
  return {
4857
5380
  type: "paragraph",
4858
- content: inlinesToPM(el.elements ?? [])
5381
+ content: inlinesToPM(el.elements ?? [], resolveEmoji)
4859
5382
  };
4860
5383
  }
4861
5384
  if (el.type === "rich_text_quote") {
@@ -4864,7 +5387,7 @@ function blockElementToPM(el) {
4864
5387
  content: [
4865
5388
  {
4866
5389
  type: "paragraph",
4867
- content: inlinesToPM(el.elements ?? [])
5390
+ content: inlinesToPM(el.elements ?? [], resolveEmoji)
4868
5391
  }
4869
5392
  ]
4870
5393
  };
@@ -4879,7 +5402,7 @@ function blockElementToPM(el) {
4879
5402
  }
4880
5403
  return null;
4881
5404
  }
4882
- function inlinesToPM(inlines) {
5405
+ function inlinesToPM(inlines, resolveEmoji) {
4883
5406
  const out = [];
4884
5407
  for (const inline of inlines) {
4885
5408
  if (inline.type === "text") {
@@ -4888,6 +5411,12 @@ function inlinesToPM(inlines) {
4888
5411
  }
4889
5412
  const marks = styleToMarks(inline.style);
4890
5413
  out.push({ type: "text", text: inline.text, marks });
5414
+ } else if (inline.type === "emoji") {
5415
+ const attrs = resolveEmoji(inline);
5416
+ if (!attrs.name) {
5417
+ continue;
5418
+ }
5419
+ out.push({ type: "emoji", attrs });
4891
5420
  } else if (inline.type === "link") {
4892
5421
  const link = inline;
4893
5422
  const text = link.text || link.url || "";
@@ -5005,6 +5534,19 @@ function flattenList(listNode, indent, out) {
5005
5534
  function proseMirrorInlinesToRichTextElements(nodes) {
5006
5535
  const out = [];
5007
5536
  for (const node of nodes) {
5537
+ if (node.type === "emoji") {
5538
+ const name = String(node.attrs?.name ?? "");
5539
+ if (!name) {
5540
+ continue;
5541
+ }
5542
+ const emoji = { type: "emoji", name };
5543
+ const skinTone = node.attrs?.skinTone;
5544
+ if (typeof skinTone === "number" && skinTone >= 2 && skinTone <= 6) {
5545
+ emoji.skin_tone = skinTone;
5546
+ }
5547
+ out.push(emoji);
5548
+ continue;
5549
+ }
5008
5550
  if (node.type !== "text" || !node.text) {
5009
5551
  continue;
5010
5552
  }
@@ -5075,6 +5617,49 @@ function sameStyle(a, b) {
5075
5617
  }
5076
5618
  return ak.every((k, i) => k === bk[i]);
5077
5619
  }
5620
+ function EmojiNodeView({ node }) {
5621
+ const { name, src, unicode } = node.attrs;
5622
+ const label = `:${name}:`;
5623
+ return /* @__PURE__ */ jsxRuntime.jsx(react.NodeViewWrapper, { as: "span", className: "bk-emoji-node", "data-emoji": name, title: label, children: src ? /* @__PURE__ */ jsxRuntime.jsx(
5624
+ "img",
5625
+ {
5626
+ src,
5627
+ alt: label,
5628
+ style: {
5629
+ display: "inline-block",
5630
+ width: "1.25em",
5631
+ height: "1.25em",
5632
+ verticalAlign: "-0.25em",
5633
+ objectFit: "contain"
5634
+ }
5635
+ }
5636
+ ) : /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-label": label, role: "img", children: unicode ? codepointsToGlyph(unicode) : label }) });
5637
+ }
5638
+ var EmojiNode = react.Node.create({
5639
+ name: "emoji",
5640
+ group: "inline",
5641
+ inline: true,
5642
+ atom: true,
5643
+ selectable: true,
5644
+ addAttributes() {
5645
+ return {
5646
+ name: { default: "" },
5647
+ src: { default: null },
5648
+ unicode: { default: null },
5649
+ skinTone: { default: null }
5650
+ };
5651
+ },
5652
+ parseHTML() {
5653
+ return [{ tag: "span[data-emoji]" }];
5654
+ },
5655
+ renderHTML({ HTMLAttributes }) {
5656
+ const name = HTMLAttributes.name ?? "";
5657
+ return ["span", react.mergeAttributes({ "data-emoji": name }, HTMLAttributes), `:${name}:`];
5658
+ },
5659
+ addNodeView() {
5660
+ return react.ReactNodeViewRenderer(EmojiNodeView);
5661
+ }
5662
+ });
5078
5663
  var BLOCK_ELEMENT_LABEL = {
5079
5664
  rich_text_section: "Section",
5080
5665
  rich_text_list: "List",
@@ -5413,15 +5998,34 @@ function InlineFields({
5413
5998
  }
5414
5999
  if (kind === "emoji") {
5415
6000
  const emoji = element;
5416
- return /* @__PURE__ */ jsxRuntime.jsx(EditorField, { label: "Name", help: "Emoji shortcode without colons, e.g. wave", htmlFor: `${idPrefix}-emoji`, children: /* @__PURE__ */ jsxRuntime.jsx(
5417
- Input,
6001
+ return /* @__PURE__ */ jsxRuntime.jsx(
6002
+ EditorField,
5418
6003
  {
5419
- id: `${idPrefix}-emoji`,
5420
- value: emoji.name ?? "",
5421
- placeholder: "e.g. wave",
5422
- onChange: (e) => onChange({ ...emoji, name: e.target.value })
6004
+ label: "Name",
6005
+ help: "Pick from the emoji list, or type a shortcode (without colons).",
6006
+ htmlFor: `${idPrefix}-emoji`,
6007
+ children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5", children: [
6008
+ /* @__PURE__ */ jsxRuntime.jsx(
6009
+ Input,
6010
+ {
6011
+ id: `${idPrefix}-emoji`,
6012
+ value: emoji.name ?? "",
6013
+ placeholder: "e.g. wave",
6014
+ onChange: (e) => onChange({ ...emoji, name: e.target.value })
6015
+ }
6016
+ ),
6017
+ /* @__PURE__ */ jsxRuntime.jsx(
6018
+ EmojiPickerButton,
6019
+ {
6020
+ align: "end",
6021
+ label: "Pick emoji",
6022
+ className: "shrink-0 border",
6023
+ onSelect: (sel) => onChange({ ...emoji, name: sel.name, skin_tone: sel.skinTone ?? void 0, unicode: void 0 })
6024
+ }
6025
+ )
6026
+ ] })
5423
6027
  }
5424
- ) });
6028
+ );
5425
6029
  }
5426
6030
  if (kind === "user") {
5427
6031
  const user = element;
@@ -5632,6 +6236,20 @@ function LossyBanner({ reasons }) {
5632
6236
  }
5633
6237
  function RichTextWysiwygEditor({ block, onChange }) {
5634
6238
  const lastEmittedRef = React.useRef(null);
6239
+ const { byName: customByName } = useCustomEmojis();
6240
+ const [emojiIndex, setEmojiIndex] = React.useState(null);
6241
+ React.useEffect(() => {
6242
+ let active = true;
6243
+ loadEmojiIndex().then((idx) => active && setEmojiIndex(idx)).catch(() => {
6244
+ });
6245
+ return () => {
6246
+ active = false;
6247
+ };
6248
+ }, []);
6249
+ const resolveEmoji = React.useMemo(
6250
+ () => makeEmojiImportResolver(customByName, emojiIndex),
6251
+ [customByName, emojiIndex]
6252
+ );
5635
6253
  const handleUpdate = React.useCallback(
5636
6254
  (editor2) => {
5637
6255
  const next = proseMirrorToRichText(editor2.getJSON());
@@ -5646,6 +6264,7 @@ function RichTextWysiwygEditor({ block, onChange }) {
5646
6264
  heading: false,
5647
6265
  horizontalRule: false
5648
6266
  }),
6267
+ EmojiNode,
5649
6268
  Link__default.default.configure({
5650
6269
  openOnClick: false,
5651
6270
  autolink: true,
@@ -5662,7 +6281,7 @@ function RichTextWysiwygEditor({ block, onChange }) {
5662
6281
  HTMLAttributes: { rel: "noreferrer noopener", target: "_blank" }
5663
6282
  })
5664
6283
  ],
5665
- content: richTextToProseMirror(block),
6284
+ content: richTextToProseMirror(block, resolveEmoji),
5666
6285
  editorProps: {
5667
6286
  attributes: {
5668
6287
  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"
@@ -5677,10 +6296,10 @@ function RichTextWysiwygEditor({ block, onChange }) {
5677
6296
  if (lastEmittedRef.current && JSON.stringify(lastEmittedRef.current) === JSON.stringify(block)) {
5678
6297
  return;
5679
6298
  }
5680
- editor.commands.setContent(richTextToProseMirror(block), {
6299
+ editor.commands.setContent(richTextToProseMirror(block, resolveEmoji), {
5681
6300
  emitUpdate: false
5682
6301
  });
5683
- }, [block, editor]);
6302
+ }, [block, editor, resolveEmoji]);
5684
6303
  if (!editor) {
5685
6304
  return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "min-h-[120px] rounded-md border border-input bg-background" });
5686
6305
  }
@@ -5729,6 +6348,16 @@ function Toolbar({ editor }) {
5729
6348
  ),
5730
6349
  /* @__PURE__ */ jsxRuntime.jsx(Divider, {}),
5731
6350
  /* @__PURE__ */ jsxRuntime.jsx(LinkPopover, { editor }),
6351
+ /* @__PURE__ */ jsxRuntime.jsx(
6352
+ EmojiPickerButton,
6353
+ {
6354
+ align: "start",
6355
+ onSelect: (sel) => editor.chain().focus().insertContent({
6356
+ type: "emoji",
6357
+ attrs: { name: sel.name, src: sel.src, unicode: sel.unicode, skinTone: sel.skinTone }
6358
+ }).run()
6359
+ }
6360
+ ),
5732
6361
  /* @__PURE__ */ jsxRuntime.jsx(Divider, {}),
5733
6362
  /* @__PURE__ */ jsxRuntime.jsx(
5734
6363
  ToolbarButton,
@@ -5927,6 +6556,8 @@ function defaultImage() {
5927
6556
  function SectionEditor({ block, onChange }) {
5928
6557
  const text = block.text?.text ?? "";
5929
6558
  const accessoryKind = detectAccessory(block.accessory);
6559
+ const textRef = React.useRef(null);
6560
+ const setText = (next) => onChange({ ...block, text: { type: "mrkdwn", text: next } });
5930
6561
  const setAccessoryKind = (next) => {
5931
6562
  if (next === accessoryKind) {
5932
6563
  return;
@@ -5950,19 +6581,20 @@ function SectionEditor({ block, onChange }) {
5950
6581
  label: "Text",
5951
6582
  help: "Supports *bold*, _italic_, ~strike~, `code`, and <url|link> formatting.",
5952
6583
  htmlFor: "section-text",
5953
- children: /* @__PURE__ */ jsxRuntime.jsx(
5954
- Textarea,
5955
- {
5956
- id: "section-text",
5957
- value: text,
5958
- rows: 4,
5959
- placeholder: "e.g. Welcome to the channel! Ping <@U123> with any questions.",
5960
- onChange: (e) => onChange({
5961
- ...block,
5962
- text: { type: "mrkdwn", text: e.target.value }
5963
- })
5964
- }
5965
- )
6584
+ children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start gap-1.5", children: [
6585
+ /* @__PURE__ */ jsxRuntime.jsx(
6586
+ Textarea,
6587
+ {
6588
+ ref: textRef,
6589
+ id: "section-text",
6590
+ value: text,
6591
+ rows: 4,
6592
+ placeholder: "e.g. Welcome to the channel! Ping <@U123> with any questions.",
6593
+ onChange: (e) => setText(e.target.value)
6594
+ }
6595
+ ),
6596
+ /* @__PURE__ */ jsxRuntime.jsx(EmojiTextInsertButton, { targetRef: textRef, value: text, onChange: setText, className: "mt-1 shrink-0 border" })
6597
+ ] })
5966
6598
  }
5967
6599
  ),
5968
6600
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-3 rounded-md border bg-muted/20 p-3", children: [
@@ -6003,19 +6635,22 @@ function ButtonAccessoryFields({ button, onChange }) {
6003
6635
  const url = button.url ?? "";
6004
6636
  const value = button.value ?? "";
6005
6637
  const style = button.style === "primary" || button.style === "danger" ? button.style : "default";
6638
+ const labelRef = React.useRef(null);
6639
+ const setLabel = (next) => onChange({ ...button, text: { type: "plain_text", text: next, emoji: true } });
6006
6640
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-3", children: [
6007
- /* @__PURE__ */ jsxRuntime.jsx(EditorField, { label: "Label", htmlFor: "section-acc-btn-label", children: /* @__PURE__ */ jsxRuntime.jsx(
6008
- Input,
6009
- {
6010
- id: "section-acc-btn-label",
6011
- value: label,
6012
- placeholder: "e.g. Learn more",
6013
- onChange: (e) => onChange({
6014
- ...button,
6015
- text: { type: "plain_text", text: e.target.value, emoji: true }
6016
- })
6017
- }
6018
- ) }),
6641
+ /* @__PURE__ */ jsxRuntime.jsx(EditorField, { label: "Label", htmlFor: "section-acc-btn-label", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5", children: [
6642
+ /* @__PURE__ */ jsxRuntime.jsx(
6643
+ Input,
6644
+ {
6645
+ ref: labelRef,
6646
+ id: "section-acc-btn-label",
6647
+ value: label,
6648
+ placeholder: "e.g. Learn more",
6649
+ onChange: (e) => setLabel(e.target.value)
6650
+ }
6651
+ ),
6652
+ /* @__PURE__ */ jsxRuntime.jsx(EmojiTextInsertButton, { targetRef: labelRef, value: label, onChange: setLabel, className: "shrink-0 border" })
6653
+ ] }) }),
6019
6654
  /* @__PURE__ */ jsxRuntime.jsx(
6020
6655
  EditorField,
6021
6656
  {
@@ -7570,6 +8205,7 @@ function BlockKitchen(props) {
7570
8205
  initialBlocks,
7571
8206
  onChange,
7572
8207
  previewHooks,
8208
+ customEmojis,
7573
8209
  loadChannels,
7574
8210
  loadSendAsUserStatus,
7575
8211
  onSend,
@@ -7613,6 +8249,13 @@ function BlockKitchen(props) {
7613
8249
  const [previewSurface, setPreviewSurface] = React.useState(allowedSurfaces[0]);
7614
8250
  const [activePaletteVariantId, setActivePaletteVariantId] = React.useState(null);
7615
8251
  const isMobile = useIsMobile();
8252
+ const mergedPreviewHooks = React.useMemo(() => {
8253
+ if (!customEmojis || customEmojis.length === 0) {
8254
+ return previewHooks;
8255
+ }
8256
+ const byName = new Map(customEmojis.map((emoji) => [emoji.name, emoji]));
8257
+ return { emoji: makeEmojiHook(byName), ...previewHooks };
8258
+ }, [customEmojis, previewHooks]);
7616
8259
  const validation = useBlockKitValidation(blocks, "message");
7617
8260
  const sensors = core.useSensors(
7618
8261
  core.useSensor(core.PointerSensor, { activationConstraint: { distance: 4 } }),
@@ -7662,7 +8305,7 @@ function BlockKitchen(props) {
7662
8305
  }, []);
7663
8306
  const activePaletteVariant = activePaletteVariantId ? variantById.get(activePaletteVariantId) : null;
7664
8307
  const blockPayloads = blocks.map((b) => b.block);
7665
- return /* @__PURE__ */ jsxRuntime.jsx(BrandThemeScope, { theme, children: /* @__PURE__ */ jsxRuntime.jsx(TooltipProvider, { delayDuration: 200, children: /* @__PURE__ */ jsxRuntime.jsxs(
8308
+ return /* @__PURE__ */ jsxRuntime.jsx(BrandThemeScope, { theme, children: /* @__PURE__ */ jsxRuntime.jsx(CustomEmojiProvider, { customEmojis, children: /* @__PURE__ */ jsxRuntime.jsx(TooltipProvider, { delayDuration: 200, children: /* @__PURE__ */ jsxRuntime.jsxs(
7666
8309
  core.DndContext,
7667
8310
  {
7668
8311
  sensors,
@@ -7708,7 +8351,7 @@ function BlockKitchen(props) {
7708
8351
  {
7709
8352
  blocks,
7710
8353
  workspaceName,
7711
- previewHooks,
8354
+ previewHooks: mergedPreviewHooks,
7712
8355
  previewTheme,
7713
8356
  previewSurface,
7714
8357
  errorsByBlockId: validation.byBlockId,
@@ -7784,7 +8427,7 @@ function BlockKitchen(props) {
7784
8427
  )
7785
8428
  ]
7786
8429
  }
7787
- ) }) });
8430
+ ) }) }) });
7788
8431
  }
7789
8432
  var UNCATEGORIZED = "Other";
7790
8433
  function TemplateThumbnail({ blocks, theme }) {