@tightknitai/block-kitchen 0.8.3 → 0.8.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -0
- package/dist/helpers.cjs.map +1 -1
- package/dist/helpers.d.cts +2 -2
- package/dist/helpers.d.ts +2 -2
- package/dist/helpers.js.map +1 -1
- package/dist/index.cjs +845 -165
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -6
- package/dist/index.d.ts +5 -6
- package/dist/index.js +848 -168
- package/dist/index.js.map +1 -1
- package/dist/{palette-CTUROcUC.d.cts → palette-Dco-CVMi.d.cts} +32 -1
- package/dist/{palette-CTUROcUC.d.ts → palette-Dco-CVMi.d.ts} +32 -1
- package/dist/palette.d.cts +1 -1
- package/dist/palette.d.ts +1 -1
- package/dist/styles.css +1 -1
- package/package.json +3 -2
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
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
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
|
|
1587
|
+
return rest.replace(/^\./, "");
|
|
1536
1588
|
}
|
|
1537
|
-
function
|
|
1538
|
-
return
|
|
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
|
|
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(
|
|
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;
|
|
@@ -1598,6 +1640,12 @@ function isSafeHref(value) {
|
|
|
1598
1640
|
}
|
|
1599
1641
|
return SAFE_LINK_PROTOCOLS.test(scheme);
|
|
1600
1642
|
}
|
|
1643
|
+
function hasExplicitSafeScheme(value) {
|
|
1644
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
1645
|
+
return false;
|
|
1646
|
+
}
|
|
1647
|
+
return getScheme(value) !== null && isSafeHref(value);
|
|
1648
|
+
}
|
|
1601
1649
|
function isSafeImageSrc(value) {
|
|
1602
1650
|
if (value === null || value === void 0) {
|
|
1603
1651
|
return false;
|
|
@@ -1682,10 +1730,47 @@ function toSlackBlocks(blocks) {
|
|
|
1682
1730
|
});
|
|
1683
1731
|
}
|
|
1684
1732
|
|
|
1733
|
+
// src/lib/validation-sanitize.ts
|
|
1734
|
+
var VALIDATION_EMOJI_PLACEHOLDER = "white_check_mark";
|
|
1735
|
+
function rewriteEmojiNames(value) {
|
|
1736
|
+
if (Array.isArray(value)) {
|
|
1737
|
+
let changed = false;
|
|
1738
|
+
const next = value.map((item) => {
|
|
1739
|
+
const rewritten = rewriteEmojiNames(item);
|
|
1740
|
+
if (rewritten !== item) {
|
|
1741
|
+
changed = true;
|
|
1742
|
+
}
|
|
1743
|
+
return rewritten;
|
|
1744
|
+
});
|
|
1745
|
+
return changed ? next : value;
|
|
1746
|
+
}
|
|
1747
|
+
if (value !== null && typeof value === "object") {
|
|
1748
|
+
const source = value;
|
|
1749
|
+
let changed = false;
|
|
1750
|
+
const next = {};
|
|
1751
|
+
for (const key of Object.keys(source)) {
|
|
1752
|
+
const rewritten = rewriteEmojiNames(source[key]);
|
|
1753
|
+
next[key] = rewritten;
|
|
1754
|
+
if (rewritten !== source[key]) {
|
|
1755
|
+
changed = true;
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
if (next.type === "emoji" && typeof next.name === "string" && next.name !== VALIDATION_EMOJI_PLACEHOLDER) {
|
|
1759
|
+
next.name = VALIDATION_EMOJI_PLACEHOLDER;
|
|
1760
|
+
changed = true;
|
|
1761
|
+
}
|
|
1762
|
+
return changed ? next : value;
|
|
1763
|
+
}
|
|
1764
|
+
return value;
|
|
1765
|
+
}
|
|
1766
|
+
function stripCustomEmojiForValidation(blocks) {
|
|
1767
|
+
return blocks.map((block) => rewriteEmojiNames(block));
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1685
1770
|
// src/state/use-block-kit-validation.ts
|
|
1686
1771
|
var DEBOUNCE_MS = 150;
|
|
1687
1772
|
function computeValidation(blocks, surface) {
|
|
1688
|
-
const payload = toSlackBlocks(blocks.map((b) => b.block));
|
|
1773
|
+
const payload = stripCustomEmojiForValidation(toSlackBlocks(blocks.map((b) => b.block)));
|
|
1689
1774
|
const result = validateBlockKit(payload, {
|
|
1690
1775
|
target: "blocks",
|
|
1691
1776
|
surface: toValidatorSurface(surface)
|
|
@@ -3531,52 +3616,55 @@ function ContextEditor({ block, onChange }) {
|
|
|
3531
3616
|
{
|
|
3532
3617
|
label: "Items",
|
|
3533
3618
|
help: "Shown in a horizontal row, usually as footnote-style content. Slack supports up to 10 items.",
|
|
3534
|
-
children: /* @__PURE__ */
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3619
|
+
children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", children: [
|
|
3620
|
+
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,
|
|
3621
|
+
elements.map((el, idx) => {
|
|
3622
|
+
if (el.type === "mrkdwn" || el.type === "plain_text") {
|
|
3623
|
+
return /* @__PURE__ */ jsx(
|
|
3624
|
+
TextItemCard,
|
|
3625
|
+
{
|
|
3626
|
+
index: idx,
|
|
3627
|
+
element: el,
|
|
3628
|
+
onChange: (next) => updateAt(idx, () => next),
|
|
3629
|
+
onRemove: () => removeAt(idx)
|
|
3630
|
+
},
|
|
3631
|
+
idx
|
|
3632
|
+
);
|
|
3633
|
+
}
|
|
3634
|
+
if (el.type === "image" && "image_url" in el) {
|
|
3635
|
+
return /* @__PURE__ */ jsx(
|
|
3636
|
+
ImageItemCard,
|
|
3637
|
+
{
|
|
3638
|
+
index: idx,
|
|
3639
|
+
element: el,
|
|
3640
|
+
onChange: (next) => updateAt(idx, () => next),
|
|
3641
|
+
onRemove: () => removeAt(idx)
|
|
3642
|
+
},
|
|
3643
|
+
idx
|
|
3644
|
+
);
|
|
3645
|
+
}
|
|
3646
|
+
return /* @__PURE__ */ jsxs(
|
|
3647
|
+
"div",
|
|
3550
3648
|
{
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3649
|
+
className: "flex items-center justify-between rounded border bg-muted/30 px-2 py-1.5 text-xs text-muted-foreground",
|
|
3650
|
+
children: [
|
|
3651
|
+
/* @__PURE__ */ jsx("span", { children: "Item not editable in the visual builder" }),
|
|
3652
|
+
/* @__PURE__ */ jsx(
|
|
3653
|
+
"button",
|
|
3654
|
+
{
|
|
3655
|
+
type: "button",
|
|
3656
|
+
"aria-label": "Remove item",
|
|
3657
|
+
onClick: () => removeAt(idx),
|
|
3658
|
+
className: "text-muted-foreground hover:text-destructive",
|
|
3659
|
+
children: /* @__PURE__ */ jsx(Trash2, { className: "h-3.5 w-3.5" })
|
|
3660
|
+
}
|
|
3661
|
+
)
|
|
3662
|
+
]
|
|
3555
3663
|
},
|
|
3556
3664
|
idx
|
|
3557
3665
|
);
|
|
3558
|
-
}
|
|
3559
|
-
|
|
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
|
-
}) })
|
|
3666
|
+
})
|
|
3667
|
+
] })
|
|
3580
3668
|
}
|
|
3581
3669
|
),
|
|
3582
3670
|
/* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
|
|
@@ -3715,12 +3803,423 @@ function ImageItemCard({
|
|
|
3715
3803
|
function DividerEditor() {
|
|
3716
3804
|
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
3805
|
}
|
|
3806
|
+
|
|
3807
|
+
// src/lib/emoji-data.ts
|
|
3808
|
+
var TONE_CODEPOINT_TO_SKIN_TONE = {
|
|
3809
|
+
"1F3FB": 2,
|
|
3810
|
+
"1F3FC": 3,
|
|
3811
|
+
"1F3FD": 4,
|
|
3812
|
+
"1F3FE": 5,
|
|
3813
|
+
"1F3FF": 6
|
|
3814
|
+
};
|
|
3815
|
+
var CATEGORY_ORDER = [
|
|
3816
|
+
"Smileys & Emotion",
|
|
3817
|
+
"People & Body",
|
|
3818
|
+
"Animals & Nature",
|
|
3819
|
+
"Food & Drink",
|
|
3820
|
+
"Travel & Places",
|
|
3821
|
+
"Activities",
|
|
3822
|
+
"Objects",
|
|
3823
|
+
"Symbols",
|
|
3824
|
+
"Flags"
|
|
3825
|
+
];
|
|
3826
|
+
function codepointsToGlyph(unified) {
|
|
3827
|
+
return unified.split("-").map((cp) => {
|
|
3828
|
+
const n = Number.parseInt(cp, 16);
|
|
3829
|
+
return Number.isFinite(n) ? String.fromCodePoint(n) : "";
|
|
3830
|
+
}).join("");
|
|
3831
|
+
}
|
|
3832
|
+
function unifiedToUnicode(unified) {
|
|
3833
|
+
return unified.toLowerCase();
|
|
3834
|
+
}
|
|
3835
|
+
function composeTextEmoji(name, skinTone) {
|
|
3836
|
+
if (skinTone && skinTone >= 2 && skinTone <= 6) {
|
|
3837
|
+
return `:${name}::skin-tone-${skinTone}:`;
|
|
3838
|
+
}
|
|
3839
|
+
return `:${name}:`;
|
|
3840
|
+
}
|
|
3841
|
+
function buildEmojiIndex(raw) {
|
|
3842
|
+
const all = [];
|
|
3843
|
+
const byName = /* @__PURE__ */ new Map();
|
|
3844
|
+
const byUnified = /* @__PURE__ */ new Map();
|
|
3845
|
+
for (const r of raw) {
|
|
3846
|
+
if (!CATEGORY_ORDER.includes(r.category)) {
|
|
3847
|
+
continue;
|
|
3848
|
+
}
|
|
3849
|
+
const shortNames = r.short_names?.length ? r.short_names : [r.short_name];
|
|
3850
|
+
const skinUnified = {};
|
|
3851
|
+
if (r.skin_variations) {
|
|
3852
|
+
for (const [toneKey, variation] of Object.entries(r.skin_variations)) {
|
|
3853
|
+
const skinTone = TONE_CODEPOINT_TO_SKIN_TONE[toneKey];
|
|
3854
|
+
if (skinTone && variation?.unified) {
|
|
3855
|
+
skinUnified[skinTone] = variation.unified;
|
|
3856
|
+
}
|
|
3857
|
+
}
|
|
3858
|
+
}
|
|
3859
|
+
const datum = {
|
|
3860
|
+
name: shortNames[0],
|
|
3861
|
+
shortNames,
|
|
3862
|
+
unified: r.unified,
|
|
3863
|
+
category: r.category,
|
|
3864
|
+
sortOrder: r.sort_order,
|
|
3865
|
+
skinUnified
|
|
3866
|
+
};
|
|
3867
|
+
all.push(datum);
|
|
3868
|
+
byUnified.set(r.unified.toUpperCase(), datum);
|
|
3869
|
+
for (const sn of shortNames) {
|
|
3870
|
+
if (!byName.has(sn)) {
|
|
3871
|
+
byName.set(sn, datum);
|
|
3872
|
+
}
|
|
3873
|
+
}
|
|
3874
|
+
}
|
|
3875
|
+
all.sort((a, b) => a.sortOrder - b.sortOrder);
|
|
3876
|
+
const byCategory = CATEGORY_ORDER.map((category) => ({
|
|
3877
|
+
category,
|
|
3878
|
+
emoji: all.filter((e) => e.category === category)
|
|
3879
|
+
})).filter((group) => group.emoji.length > 0);
|
|
3880
|
+
return { all, byName, byUnified, byCategory };
|
|
3881
|
+
}
|
|
3882
|
+
var indexPromise = null;
|
|
3883
|
+
var cachedIndex = null;
|
|
3884
|
+
async function loadEmojiIndex() {
|
|
3885
|
+
if (cachedIndex) {
|
|
3886
|
+
return cachedIndex;
|
|
3887
|
+
}
|
|
3888
|
+
if (!indexPromise) {
|
|
3889
|
+
indexPromise = import('emoji-datasource/emoji.json').then((mod) => {
|
|
3890
|
+
const raw = mod.default ?? mod;
|
|
3891
|
+
cachedIndex = buildEmojiIndex(raw);
|
|
3892
|
+
return cachedIndex;
|
|
3893
|
+
}).catch((err) => {
|
|
3894
|
+
indexPromise = null;
|
|
3895
|
+
throw err;
|
|
3896
|
+
});
|
|
3897
|
+
}
|
|
3898
|
+
return indexPromise;
|
|
3899
|
+
}
|
|
3900
|
+
function getEmojiIndex() {
|
|
3901
|
+
return cachedIndex;
|
|
3902
|
+
}
|
|
3903
|
+
|
|
3904
|
+
// src/lib/emoji-recents.ts
|
|
3905
|
+
var STORAGE_KEY = "bk:emoji-recents";
|
|
3906
|
+
var SKIN_TONE_KEY = "bk:emoji-skin-tone";
|
|
3907
|
+
var MAX_RECENTS = 24;
|
|
3908
|
+
function readEmojiSkinTone() {
|
|
3909
|
+
try {
|
|
3910
|
+
const raw = globalThis.localStorage?.getItem(SKIN_TONE_KEY);
|
|
3911
|
+
if (!raw) {
|
|
3912
|
+
return null;
|
|
3913
|
+
}
|
|
3914
|
+
const n = Number.parseInt(raw, 10);
|
|
3915
|
+
return n >= 2 && n <= 6 ? n : null;
|
|
3916
|
+
} catch {
|
|
3917
|
+
return null;
|
|
3918
|
+
}
|
|
3919
|
+
}
|
|
3920
|
+
function writeEmojiSkinTone(skinTone) {
|
|
3921
|
+
try {
|
|
3922
|
+
if (skinTone && skinTone >= 2 && skinTone <= 6) {
|
|
3923
|
+
globalThis.localStorage?.setItem(SKIN_TONE_KEY, String(skinTone));
|
|
3924
|
+
} else {
|
|
3925
|
+
globalThis.localStorage?.removeItem(SKIN_TONE_KEY);
|
|
3926
|
+
}
|
|
3927
|
+
} catch {
|
|
3928
|
+
}
|
|
3929
|
+
}
|
|
3930
|
+
function readEmojiRecents() {
|
|
3931
|
+
try {
|
|
3932
|
+
const raw = globalThis.localStorage?.getItem(STORAGE_KEY);
|
|
3933
|
+
if (!raw) {
|
|
3934
|
+
return [];
|
|
3935
|
+
}
|
|
3936
|
+
const parsed = JSON.parse(raw);
|
|
3937
|
+
return Array.isArray(parsed) ? parsed.filter((n) => typeof n === "string") : [];
|
|
3938
|
+
} catch {
|
|
3939
|
+
return [];
|
|
3940
|
+
}
|
|
3941
|
+
}
|
|
3942
|
+
function pushEmojiRecent(name) {
|
|
3943
|
+
const next = [name, ...readEmojiRecents().filter((n) => n !== name)].slice(0, MAX_RECENTS);
|
|
3944
|
+
try {
|
|
3945
|
+
globalThis.localStorage?.setItem(STORAGE_KEY, JSON.stringify(next));
|
|
3946
|
+
} catch {
|
|
3947
|
+
}
|
|
3948
|
+
return next;
|
|
3949
|
+
}
|
|
3950
|
+
var SKIN_TONES = [
|
|
3951
|
+
{ tone: null, label: "Default", swatch: "\u270B" },
|
|
3952
|
+
{ tone: 2, label: "Light", swatch: "\u270B\u{1F3FB}" },
|
|
3953
|
+
{ tone: 3, label: "Medium-light", swatch: "\u270B\u{1F3FC}" },
|
|
3954
|
+
{ tone: 4, label: "Medium", swatch: "\u270B\u{1F3FD}" },
|
|
3955
|
+
{ tone: 5, label: "Medium-dark", swatch: "\u270B\u{1F3FE}" },
|
|
3956
|
+
{ tone: 6, label: "Dark", swatch: "\u270B\u{1F3FF}" }
|
|
3957
|
+
];
|
|
3958
|
+
function EmojiPicker({
|
|
3959
|
+
onSelect,
|
|
3960
|
+
autoFocus = true
|
|
3961
|
+
}) {
|
|
3962
|
+
const { customEmojis } = useCustomEmojis();
|
|
3963
|
+
const [index, setIndex] = useState(null);
|
|
3964
|
+
const [loadError, setLoadError] = useState(false);
|
|
3965
|
+
const [query, setQuery] = useState("");
|
|
3966
|
+
const [skinTone, setSkinTone] = useState(() => readEmojiSkinTone());
|
|
3967
|
+
const [recents, setRecents] = useState(() => readEmojiRecents());
|
|
3968
|
+
const searchRef = useRef(null);
|
|
3969
|
+
useEffect(() => {
|
|
3970
|
+
let active = true;
|
|
3971
|
+
loadEmojiIndex().then((idx) => {
|
|
3972
|
+
if (active) {
|
|
3973
|
+
setIndex(idx);
|
|
3974
|
+
}
|
|
3975
|
+
}).catch(() => {
|
|
3976
|
+
if (active) {
|
|
3977
|
+
setLoadError(true);
|
|
3978
|
+
}
|
|
3979
|
+
});
|
|
3980
|
+
return () => {
|
|
3981
|
+
active = false;
|
|
3982
|
+
};
|
|
3983
|
+
}, []);
|
|
3984
|
+
useEffect(() => {
|
|
3985
|
+
if (autoFocus) {
|
|
3986
|
+
searchRef.current?.focus();
|
|
3987
|
+
}
|
|
3988
|
+
}, [autoFocus]);
|
|
3989
|
+
const customByName = useMemo(() => {
|
|
3990
|
+
const map = /* @__PURE__ */ new Map();
|
|
3991
|
+
for (const e of customEmojis) {
|
|
3992
|
+
map.set(e.name, e);
|
|
3993
|
+
}
|
|
3994
|
+
return map;
|
|
3995
|
+
}, [customEmojis]);
|
|
3996
|
+
const customImageEmoji = useMemo(() => customEmojis.filter((e) => e.url), [customEmojis]);
|
|
3997
|
+
const handlePick = (selection) => {
|
|
3998
|
+
setRecents(pushEmojiRecent(selection.name));
|
|
3999
|
+
onSelect(selection);
|
|
4000
|
+
};
|
|
4001
|
+
const handleSkinTone = (tone) => {
|
|
4002
|
+
setSkinTone(tone);
|
|
4003
|
+
writeEmojiSkinTone(tone);
|
|
4004
|
+
};
|
|
4005
|
+
const sections = useMemo(() => {
|
|
4006
|
+
if (!index) {
|
|
4007
|
+
return [];
|
|
4008
|
+
}
|
|
4009
|
+
const trimmed = query.trim().toLowerCase();
|
|
4010
|
+
if (trimmed) {
|
|
4011
|
+
const standard = index.all.filter((d) => d.shortNames.some((sn) => sn.includes(trimmed))).slice(0, 120).map((datum) => ({ datum }));
|
|
4012
|
+
const custom = customImageEmoji.filter((e) => e.name.toLowerCase().includes(trimmed)).map((custom2) => ({ custom: custom2 }));
|
|
4013
|
+
return [{ key: "search", title: "Search results", emoji: [...custom, ...standard] }];
|
|
4014
|
+
}
|
|
4015
|
+
const out = [];
|
|
4016
|
+
if (recents.length > 0) {
|
|
4017
|
+
const recentEmoji = recents.map((name) => {
|
|
4018
|
+
const custom = customByName.get(name);
|
|
4019
|
+
if (custom?.url) {
|
|
4020
|
+
return { custom };
|
|
4021
|
+
}
|
|
4022
|
+
const datum = index.byName.get(name);
|
|
4023
|
+
return datum ? { datum } : null;
|
|
4024
|
+
}).filter((e) => e !== null);
|
|
4025
|
+
if (recentEmoji.length > 0) {
|
|
4026
|
+
out.push({ key: "recents", title: "Frequently used", emoji: recentEmoji });
|
|
4027
|
+
}
|
|
4028
|
+
}
|
|
4029
|
+
if (customImageEmoji.length > 0) {
|
|
4030
|
+
out.push({ key: "custom", title: "Custom", emoji: customImageEmoji.map((custom) => ({ custom })) });
|
|
4031
|
+
}
|
|
4032
|
+
for (const group of index.byCategory) {
|
|
4033
|
+
out.push({ key: group.category, title: group.category, emoji: group.emoji.map((datum) => ({ datum })) });
|
|
4034
|
+
}
|
|
4035
|
+
return out;
|
|
4036
|
+
}, [index, query, recents, customByName, customImageEmoji]);
|
|
4037
|
+
return /* @__PURE__ */ jsxs("div", { className: "bk-emoji-picker flex w-[320px] max-w-[90vw] flex-col gap-2", children: [
|
|
4038
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
4039
|
+
/* @__PURE__ */ jsx(
|
|
4040
|
+
Input,
|
|
4041
|
+
{
|
|
4042
|
+
ref: searchRef,
|
|
4043
|
+
value: query,
|
|
4044
|
+
placeholder: "Search emoji\u2026",
|
|
4045
|
+
"aria-label": "Search emoji",
|
|
4046
|
+
onChange: (e) => setQuery(e.target.value),
|
|
4047
|
+
className: "h-8"
|
|
4048
|
+
}
|
|
4049
|
+
),
|
|
4050
|
+
/* @__PURE__ */ jsx(SkinToneSelector, { value: skinTone, onChange: handleSkinTone })
|
|
4051
|
+
] }),
|
|
4052
|
+
/* @__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: [
|
|
4053
|
+
/* @__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 }),
|
|
4054
|
+
/* @__PURE__ */ jsx("div", { className: "grid grid-cols-8 gap-0.5", children: section.emoji.map(
|
|
4055
|
+
(entry, i) => entry.custom ? /* @__PURE__ */ jsx(
|
|
4056
|
+
CustomEmojiButton,
|
|
4057
|
+
{
|
|
4058
|
+
emoji: entry.custom,
|
|
4059
|
+
onPick: handlePick
|
|
4060
|
+
},
|
|
4061
|
+
`${section.key}-${entry.custom.name}-${i}`
|
|
4062
|
+
) : entry.datum ? /* @__PURE__ */ jsx(
|
|
4063
|
+
StandardEmojiButton,
|
|
4064
|
+
{
|
|
4065
|
+
datum: entry.datum,
|
|
4066
|
+
skinTone,
|
|
4067
|
+
onPick: handlePick
|
|
4068
|
+
},
|
|
4069
|
+
`${section.key}-${entry.datum.name}-${i}`
|
|
4070
|
+
) : null
|
|
4071
|
+
) })
|
|
4072
|
+
] }, section.key)) })
|
|
4073
|
+
] });
|
|
4074
|
+
}
|
|
4075
|
+
function resolveStandard(datum, skinTone) {
|
|
4076
|
+
const tonedUnified = skinTone ? datum.skinUnified[skinTone] : void 0;
|
|
4077
|
+
return {
|
|
4078
|
+
name: datum.name,
|
|
4079
|
+
unicode: unifiedToUnicode(tonedUnified ?? datum.unified),
|
|
4080
|
+
src: null,
|
|
4081
|
+
skinTone: tonedUnified ? skinTone : null
|
|
4082
|
+
};
|
|
4083
|
+
}
|
|
4084
|
+
var emojiButtonClass = "flex h-8 w-8 cursor-pointer items-center justify-center rounded text-lg leading-none hover:bg-accent";
|
|
4085
|
+
function StandardEmojiButton({
|
|
4086
|
+
datum,
|
|
4087
|
+
skinTone,
|
|
4088
|
+
onPick
|
|
4089
|
+
}) {
|
|
4090
|
+
const tonedUnified = skinTone && datum.skinUnified[skinTone] || datum.unified;
|
|
4091
|
+
return /* @__PURE__ */ jsx(
|
|
4092
|
+
"button",
|
|
4093
|
+
{
|
|
4094
|
+
type: "button",
|
|
4095
|
+
className: emojiButtonClass,
|
|
4096
|
+
title: `:${datum.name}:`,
|
|
4097
|
+
"aria-label": datum.name,
|
|
4098
|
+
onClick: () => onPick(resolveStandard(datum, skinTone)),
|
|
4099
|
+
children: codepointsToGlyph(tonedUnified)
|
|
4100
|
+
}
|
|
4101
|
+
);
|
|
4102
|
+
}
|
|
4103
|
+
function CustomEmojiButton({ emoji, onPick }) {
|
|
4104
|
+
return /* @__PURE__ */ jsx(
|
|
4105
|
+
"button",
|
|
4106
|
+
{
|
|
4107
|
+
type: "button",
|
|
4108
|
+
className: emojiButtonClass,
|
|
4109
|
+
title: `:${emoji.name}:`,
|
|
4110
|
+
"aria-label": emoji.name,
|
|
4111
|
+
onClick: () => onPick({ name: emoji.name, unicode: null, src: emoji.url, skinTone: null }),
|
|
4112
|
+
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: [
|
|
4113
|
+
":",
|
|
4114
|
+
emoji.name,
|
|
4115
|
+
":"
|
|
4116
|
+
] })
|
|
4117
|
+
}
|
|
4118
|
+
);
|
|
4119
|
+
}
|
|
4120
|
+
function SkinToneSelector({ value, onChange }) {
|
|
4121
|
+
const [open, setOpen] = useState(false);
|
|
4122
|
+
const active = SKIN_TONES.find((t) => t.tone === value) ?? SKIN_TONES[0];
|
|
4123
|
+
return /* @__PURE__ */ jsxs("div", { className: "relative shrink-0", children: [
|
|
4124
|
+
/* @__PURE__ */ jsx(
|
|
4125
|
+
"button",
|
|
4126
|
+
{
|
|
4127
|
+
type: "button",
|
|
4128
|
+
"aria-label": `Skin tone: ${active.label}`,
|
|
4129
|
+
title: `Skin tone: ${active.label}`,
|
|
4130
|
+
className: "flex h-8 w-8 items-center justify-center rounded border text-lg leading-none hover:bg-accent",
|
|
4131
|
+
onClick: () => setOpen((o) => !o),
|
|
4132
|
+
children: active.swatch
|
|
4133
|
+
}
|
|
4134
|
+
),
|
|
4135
|
+
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(
|
|
4136
|
+
"button",
|
|
4137
|
+
{
|
|
4138
|
+
type: "button",
|
|
4139
|
+
"aria-label": t.label,
|
|
4140
|
+
title: t.label,
|
|
4141
|
+
className: cn(emojiButtonClass, value === t.tone && "bg-accent"),
|
|
4142
|
+
onClick: () => {
|
|
4143
|
+
onChange(t.tone);
|
|
4144
|
+
setOpen(false);
|
|
4145
|
+
},
|
|
4146
|
+
children: t.swatch
|
|
4147
|
+
},
|
|
4148
|
+
t.label
|
|
4149
|
+
)) })
|
|
4150
|
+
] });
|
|
4151
|
+
}
|
|
4152
|
+
function EmojiPickerButton({
|
|
4153
|
+
onSelect,
|
|
4154
|
+
className,
|
|
4155
|
+
label = "Insert emoji",
|
|
4156
|
+
align = "start"
|
|
4157
|
+
}) {
|
|
4158
|
+
const [open, setOpen] = useState(false);
|
|
4159
|
+
return /* @__PURE__ */ jsxs(Popover, { open, onOpenChange: setOpen, children: [
|
|
4160
|
+
/* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
|
|
4161
|
+
"button",
|
|
4162
|
+
{
|
|
4163
|
+
type: "button",
|
|
4164
|
+
"aria-label": label,
|
|
4165
|
+
title: label,
|
|
4166
|
+
className: cn(
|
|
4167
|
+
"flex h-7 w-7 cursor-pointer items-center justify-center rounded text-muted-foreground transition-colors hover:bg-accent hover:text-foreground",
|
|
4168
|
+
className
|
|
4169
|
+
),
|
|
4170
|
+
children: /* @__PURE__ */ jsx(Smile, { className: "h-3.5 w-3.5" })
|
|
4171
|
+
}
|
|
4172
|
+
) }),
|
|
4173
|
+
/* @__PURE__ */ jsx(PopoverContent, { align, className: "w-auto p-2", children: /* @__PURE__ */ jsx(
|
|
4174
|
+
EmojiPicker,
|
|
4175
|
+
{
|
|
4176
|
+
onSelect: (selection) => {
|
|
4177
|
+
onSelect(selection);
|
|
4178
|
+
setOpen(false);
|
|
4179
|
+
}
|
|
4180
|
+
}
|
|
4181
|
+
) })
|
|
4182
|
+
] });
|
|
4183
|
+
}
|
|
4184
|
+
function EmojiTextInsertButton({
|
|
4185
|
+
targetRef,
|
|
4186
|
+
value,
|
|
4187
|
+
onChange,
|
|
4188
|
+
className,
|
|
4189
|
+
align = "end"
|
|
4190
|
+
}) {
|
|
4191
|
+
return /* @__PURE__ */ jsx(
|
|
4192
|
+
EmojiPickerButton,
|
|
4193
|
+
{
|
|
4194
|
+
align,
|
|
4195
|
+
label: "Insert emoji",
|
|
4196
|
+
className,
|
|
4197
|
+
onSelect: (sel) => {
|
|
4198
|
+
const el = targetRef.current;
|
|
4199
|
+
const insert = composeTextEmoji(sel.name, sel.skinTone ?? void 0);
|
|
4200
|
+
const start = el?.selectionStart ?? value.length;
|
|
4201
|
+
const end = el?.selectionEnd ?? value.length;
|
|
4202
|
+
const next = value.slice(0, start) + insert + value.slice(end);
|
|
4203
|
+
onChange(next);
|
|
4204
|
+
requestAnimationFrame(() => {
|
|
4205
|
+
if (el) {
|
|
4206
|
+
const caret = start + insert.length;
|
|
4207
|
+
el.focus();
|
|
4208
|
+
el.setSelectionRange(caret, caret);
|
|
4209
|
+
}
|
|
4210
|
+
});
|
|
4211
|
+
}
|
|
4212
|
+
}
|
|
4213
|
+
);
|
|
4214
|
+
}
|
|
3718
4215
|
var HEADER_MAX = 150;
|
|
3719
4216
|
var HEADER_LEVELS = [1, 2, 3, 4];
|
|
3720
4217
|
function HeaderEditor({ block, onChange }) {
|
|
3721
4218
|
const text = block.text?.text ?? "";
|
|
3722
4219
|
const remaining = HEADER_MAX - text.length;
|
|
3723
4220
|
const level = block.level;
|
|
4221
|
+
const inputRef = useRef(null);
|
|
4222
|
+
const setText = (next) => onChange({ ...block, text: { type: "plain_text", text: next, emoji: true } });
|
|
3724
4223
|
return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3", children: [
|
|
3725
4224
|
/* @__PURE__ */ jsx(
|
|
3726
4225
|
EditorField,
|
|
@@ -3728,23 +4227,20 @@ function HeaderEditor({ block, onChange }) {
|
|
|
3728
4227
|
label: "Heading text",
|
|
3729
4228
|
help: `Plain text only. ${remaining} characters remaining.`,
|
|
3730
4229
|
htmlFor: "header-text",
|
|
3731
|
-
children: /* @__PURE__ */
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
})
|
|
3746
|
-
}
|
|
3747
|
-
)
|
|
4230
|
+
children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5", children: [
|
|
4231
|
+
/* @__PURE__ */ jsx(
|
|
4232
|
+
Input,
|
|
4233
|
+
{
|
|
4234
|
+
ref: inputRef,
|
|
4235
|
+
id: "header-text",
|
|
4236
|
+
value: text,
|
|
4237
|
+
maxLength: HEADER_MAX,
|
|
4238
|
+
placeholder: "e.g. Weekly roundup",
|
|
4239
|
+
onChange: (e) => setText(e.target.value)
|
|
4240
|
+
}
|
|
4241
|
+
),
|
|
4242
|
+
/* @__PURE__ */ jsx(EmojiTextInsertButton, { targetRef: inputRef, value: text, onChange: setText, className: "shrink-0 border" })
|
|
4243
|
+
] })
|
|
3748
4244
|
}
|
|
3749
4245
|
),
|
|
3750
4246
|
/* @__PURE__ */ jsx(EditorField, { label: "Level", help: "Optional. Builder-only extension; Slack's API ignores this value.", children: /* @__PURE__ */ jsxs(
|
|
@@ -4607,22 +5103,29 @@ function CheckboxField({
|
|
|
4607
5103
|
] });
|
|
4608
5104
|
}
|
|
4609
5105
|
function MarkdownEditor({ block, onChange }) {
|
|
5106
|
+
const textRef = useRef(null);
|
|
5107
|
+
const text = block.text ?? "";
|
|
5108
|
+
const setText = (next) => onChange({ ...block, text: next });
|
|
4610
5109
|
return /* @__PURE__ */ jsx(
|
|
4611
5110
|
EditorField,
|
|
4612
5111
|
{
|
|
4613
5112
|
label: "Markdown",
|
|
4614
5113
|
help: "Standard markdown with GFM: **bold**, _italic_, lists, tables, ```code```, [links](url).",
|
|
4615
5114
|
htmlFor: "markdown-text",
|
|
4616
|
-
children: /* @__PURE__ */
|
|
4617
|
-
|
|
4618
|
-
|
|
4619
|
-
|
|
4620
|
-
|
|
4621
|
-
|
|
4622
|
-
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
|
|
5115
|
+
children: /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-1.5", children: [
|
|
5116
|
+
/* @__PURE__ */ jsx(
|
|
5117
|
+
Textarea,
|
|
5118
|
+
{
|
|
5119
|
+
ref: textRef,
|
|
5120
|
+
id: "markdown-text",
|
|
5121
|
+
value: text,
|
|
5122
|
+
rows: 8,
|
|
5123
|
+
placeholder: "e.g. **Roadmap**\\n\\n- Item one\\n- Item two",
|
|
5124
|
+
onChange: (e) => setText(e.target.value)
|
|
5125
|
+
}
|
|
5126
|
+
),
|
|
5127
|
+
/* @__PURE__ */ jsx(EmojiTextInsertButton, { targetRef: textRef, value: text, onChange: setText, className: "mt-1 shrink-0 border" })
|
|
5128
|
+
] })
|
|
4626
5129
|
}
|
|
4627
5130
|
);
|
|
4628
5131
|
}
|
|
@@ -4717,6 +5220,26 @@ function PlanEditor({ block, onChange }) {
|
|
|
4717
5220
|
] });
|
|
4718
5221
|
}
|
|
4719
5222
|
|
|
5223
|
+
// src/lib/emoji-resolve.ts
|
|
5224
|
+
function makeEmojiImportResolver(customByName, index = getEmojiIndex()) {
|
|
5225
|
+
return (el) => {
|
|
5226
|
+
const name = el.name ?? "";
|
|
5227
|
+
const skinTone = el.skin_tone ?? null;
|
|
5228
|
+
const custom = customByName.get(name);
|
|
5229
|
+
if (custom?.url) {
|
|
5230
|
+
return { name, src: custom.url, unicode: null, skinTone };
|
|
5231
|
+
}
|
|
5232
|
+
const lookupName = custom?.alias ?? name;
|
|
5233
|
+
const datum = index?.byName.get(lookupName);
|
|
5234
|
+
let unicode = el.unicode ?? null;
|
|
5235
|
+
if (!unicode && datum) {
|
|
5236
|
+
const toned = skinTone ? datum.skinUnified[skinTone] : void 0;
|
|
5237
|
+
unicode = unifiedToUnicode(toned ?? datum.unified);
|
|
5238
|
+
}
|
|
5239
|
+
return { name, src: null, unicode, skinTone };
|
|
5240
|
+
};
|
|
5241
|
+
}
|
|
5242
|
+
|
|
4720
5243
|
// src/lib/rich-text-tiptap.ts
|
|
4721
5244
|
var SUPPORTED_BLOCK_KINDS = /* @__PURE__ */ new Set([
|
|
4722
5245
|
"rich_text_section",
|
|
@@ -4724,7 +5247,7 @@ var SUPPORTED_BLOCK_KINDS = /* @__PURE__ */ new Set([
|
|
|
4724
5247
|
"rich_text_quote",
|
|
4725
5248
|
"rich_text_preformatted"
|
|
4726
5249
|
]);
|
|
4727
|
-
var SUPPORTED_INLINE_KINDS = /* @__PURE__ */ new Set(["text", "link"]);
|
|
5250
|
+
var SUPPORTED_INLINE_KINDS = /* @__PURE__ */ new Set(["text", "link", "emoji"]);
|
|
4728
5251
|
var SUPPORTED_TEXT_STYLE_KEYS = /* @__PURE__ */ new Set(["bold", "italic", "strike", "code"]);
|
|
4729
5252
|
var MAX_LIST_INDENT = 8;
|
|
4730
5253
|
function detectLossy(block) {
|
|
@@ -4771,16 +5294,22 @@ function scanInlines(inlines, where, out) {
|
|
|
4771
5294
|
}
|
|
4772
5295
|
});
|
|
4773
5296
|
}
|
|
4774
|
-
|
|
5297
|
+
var defaultEmojiResolver = (el) => ({
|
|
5298
|
+
name: el.name ?? "",
|
|
5299
|
+
src: null,
|
|
5300
|
+
unicode: el.unicode ?? null,
|
|
5301
|
+
skinTone: el.skin_tone ?? null
|
|
5302
|
+
});
|
|
5303
|
+
function richTextToProseMirror(block, resolveEmoji = defaultEmojiResolver) {
|
|
4775
5304
|
const content = [];
|
|
4776
5305
|
const listsByIndent = /* @__PURE__ */ new Map();
|
|
4777
5306
|
for (const el of block.elements ?? []) {
|
|
4778
5307
|
if (el.type === "rich_text_list") {
|
|
4779
|
-
appendRichTextList(el, content, listsByIndent);
|
|
5308
|
+
appendRichTextList(el, content, listsByIndent, resolveEmoji);
|
|
4780
5309
|
continue;
|
|
4781
5310
|
}
|
|
4782
5311
|
listsByIndent.clear();
|
|
4783
|
-
const node = blockElementToPM(el);
|
|
5312
|
+
const node = blockElementToPM(el, resolveEmoji);
|
|
4784
5313
|
if (node) {
|
|
4785
5314
|
content.push(node);
|
|
4786
5315
|
}
|
|
@@ -4790,12 +5319,12 @@ function richTextToProseMirror(block) {
|
|
|
4790
5319
|
content: content.length > 0 ? content : [{ type: "paragraph" }]
|
|
4791
5320
|
};
|
|
4792
5321
|
}
|
|
4793
|
-
function appendRichTextList(el, topLevel, listsByIndent) {
|
|
5322
|
+
function appendRichTextList(el, topLevel, listsByIndent, resolveEmoji) {
|
|
4794
5323
|
const indent = el.indent ?? 0;
|
|
4795
5324
|
const listType = el.style === "ordered" ? "orderedList" : "bulletList";
|
|
4796
5325
|
const items = (el.elements ?? []).map((section) => ({
|
|
4797
5326
|
type: "listItem",
|
|
4798
|
-
content: [{ type: "paragraph", content: inlinesToPM(section.elements ?? []) }]
|
|
5327
|
+
content: [{ type: "paragraph", content: inlinesToPM(section.elements ?? [], resolveEmoji) }]
|
|
4799
5328
|
}));
|
|
4800
5329
|
for (const k of Array.from(listsByIndent.keys())) {
|
|
4801
5330
|
if (k > indent) {
|
|
@@ -4821,11 +5350,11 @@ function appendRichTextList(el, topLevel, listsByIndent) {
|
|
|
4821
5350
|
const parentLastItem = parent.content[parent.content.length - 1];
|
|
4822
5351
|
parentLastItem.content = [...parentLastItem.content ?? [], list];
|
|
4823
5352
|
}
|
|
4824
|
-
function blockElementToPM(el) {
|
|
5353
|
+
function blockElementToPM(el, resolveEmoji) {
|
|
4825
5354
|
if (el.type === "rich_text_section") {
|
|
4826
5355
|
return {
|
|
4827
5356
|
type: "paragraph",
|
|
4828
|
-
content: inlinesToPM(el.elements ?? [])
|
|
5357
|
+
content: inlinesToPM(el.elements ?? [], resolveEmoji)
|
|
4829
5358
|
};
|
|
4830
5359
|
}
|
|
4831
5360
|
if (el.type === "rich_text_quote") {
|
|
@@ -4834,7 +5363,7 @@ function blockElementToPM(el) {
|
|
|
4834
5363
|
content: [
|
|
4835
5364
|
{
|
|
4836
5365
|
type: "paragraph",
|
|
4837
|
-
content: inlinesToPM(el.elements ?? [])
|
|
5366
|
+
content: inlinesToPM(el.elements ?? [], resolveEmoji)
|
|
4838
5367
|
}
|
|
4839
5368
|
]
|
|
4840
5369
|
};
|
|
@@ -4849,15 +5378,31 @@ function blockElementToPM(el) {
|
|
|
4849
5378
|
}
|
|
4850
5379
|
return null;
|
|
4851
5380
|
}
|
|
4852
|
-
function
|
|
5381
|
+
function pushTextWithHardBreaks(out, text, marks) {
|
|
5382
|
+
const segments = text.split("\n");
|
|
5383
|
+
segments.forEach((segment, i) => {
|
|
5384
|
+
if (i > 0) {
|
|
5385
|
+
out.push({ type: "hardBreak" });
|
|
5386
|
+
}
|
|
5387
|
+
if (segment) {
|
|
5388
|
+
out.push({ type: "text", text: segment, marks });
|
|
5389
|
+
}
|
|
5390
|
+
});
|
|
5391
|
+
}
|
|
5392
|
+
function inlinesToPM(inlines, resolveEmoji) {
|
|
4853
5393
|
const out = [];
|
|
4854
5394
|
for (const inline of inlines) {
|
|
4855
5395
|
if (inline.type === "text") {
|
|
4856
5396
|
if (!inline.text) {
|
|
4857
5397
|
continue;
|
|
4858
5398
|
}
|
|
4859
|
-
|
|
4860
|
-
|
|
5399
|
+
pushTextWithHardBreaks(out, inline.text, styleToMarks(inline.style));
|
|
5400
|
+
} else if (inline.type === "emoji") {
|
|
5401
|
+
const attrs = resolveEmoji(inline);
|
|
5402
|
+
if (!attrs.name) {
|
|
5403
|
+
continue;
|
|
5404
|
+
}
|
|
5405
|
+
out.push({ type: "emoji", attrs });
|
|
4861
5406
|
} else if (inline.type === "link") {
|
|
4862
5407
|
const link = inline;
|
|
4863
5408
|
const text = link.text || link.url || "";
|
|
@@ -4975,6 +5520,23 @@ function flattenList(listNode, indent, out) {
|
|
|
4975
5520
|
function proseMirrorInlinesToRichTextElements(nodes) {
|
|
4976
5521
|
const out = [];
|
|
4977
5522
|
for (const node of nodes) {
|
|
5523
|
+
if (node.type === "emoji") {
|
|
5524
|
+
const name = String(node.attrs?.name ?? "");
|
|
5525
|
+
if (!name) {
|
|
5526
|
+
continue;
|
|
5527
|
+
}
|
|
5528
|
+
const emoji = { type: "emoji", name };
|
|
5529
|
+
const skinTone = node.attrs?.skinTone;
|
|
5530
|
+
if (typeof skinTone === "number" && skinTone >= 2 && skinTone <= 6) {
|
|
5531
|
+
emoji.skin_tone = skinTone;
|
|
5532
|
+
}
|
|
5533
|
+
out.push(emoji);
|
|
5534
|
+
continue;
|
|
5535
|
+
}
|
|
5536
|
+
if (node.type === "hardBreak") {
|
|
5537
|
+
out.push({ type: "text", text: "\n" });
|
|
5538
|
+
continue;
|
|
5539
|
+
}
|
|
4978
5540
|
if (node.type !== "text" || !node.text) {
|
|
4979
5541
|
continue;
|
|
4980
5542
|
}
|
|
@@ -5045,6 +5607,49 @@ function sameStyle(a, b) {
|
|
|
5045
5607
|
}
|
|
5046
5608
|
return ak.every((k, i) => k === bk[i]);
|
|
5047
5609
|
}
|
|
5610
|
+
function EmojiNodeView({ node }) {
|
|
5611
|
+
const { name, src, unicode } = node.attrs;
|
|
5612
|
+
const label = `:${name}:`;
|
|
5613
|
+
return /* @__PURE__ */ jsx(NodeViewWrapper, { as: "span", className: "bk-emoji-node", "data-emoji": name, title: label, children: src ? /* @__PURE__ */ jsx(
|
|
5614
|
+
"img",
|
|
5615
|
+
{
|
|
5616
|
+
src,
|
|
5617
|
+
alt: label,
|
|
5618
|
+
style: {
|
|
5619
|
+
display: "inline-block",
|
|
5620
|
+
width: "1.25em",
|
|
5621
|
+
height: "1.25em",
|
|
5622
|
+
verticalAlign: "-0.25em",
|
|
5623
|
+
objectFit: "contain"
|
|
5624
|
+
}
|
|
5625
|
+
}
|
|
5626
|
+
) : /* @__PURE__ */ jsx("span", { "aria-label": label, role: "img", children: unicode ? codepointsToGlyph(unicode) : label }) });
|
|
5627
|
+
}
|
|
5628
|
+
var EmojiNode = Node.create({
|
|
5629
|
+
name: "emoji",
|
|
5630
|
+
group: "inline",
|
|
5631
|
+
inline: true,
|
|
5632
|
+
atom: true,
|
|
5633
|
+
selectable: true,
|
|
5634
|
+
addAttributes() {
|
|
5635
|
+
return {
|
|
5636
|
+
name: { default: "" },
|
|
5637
|
+
src: { default: null },
|
|
5638
|
+
unicode: { default: null },
|
|
5639
|
+
skinTone: { default: null }
|
|
5640
|
+
};
|
|
5641
|
+
},
|
|
5642
|
+
parseHTML() {
|
|
5643
|
+
return [{ tag: "span[data-emoji]" }];
|
|
5644
|
+
},
|
|
5645
|
+
renderHTML({ HTMLAttributes }) {
|
|
5646
|
+
const name = HTMLAttributes.name ?? "";
|
|
5647
|
+
return ["span", mergeAttributes({ "data-emoji": name }, HTMLAttributes), `:${name}:`];
|
|
5648
|
+
},
|
|
5649
|
+
addNodeView() {
|
|
5650
|
+
return ReactNodeViewRenderer(EmojiNodeView);
|
|
5651
|
+
}
|
|
5652
|
+
});
|
|
5048
5653
|
var BLOCK_ELEMENT_LABEL = {
|
|
5049
5654
|
rich_text_section: "Section",
|
|
5050
5655
|
rich_text_list: "List",
|
|
@@ -5383,15 +5988,34 @@ function InlineFields({
|
|
|
5383
5988
|
}
|
|
5384
5989
|
if (kind === "emoji") {
|
|
5385
5990
|
const emoji = element;
|
|
5386
|
-
return /* @__PURE__ */ jsx(
|
|
5387
|
-
|
|
5991
|
+
return /* @__PURE__ */ jsx(
|
|
5992
|
+
EditorField,
|
|
5388
5993
|
{
|
|
5389
|
-
|
|
5390
|
-
|
|
5391
|
-
|
|
5392
|
-
|
|
5994
|
+
label: "Name",
|
|
5995
|
+
help: "Pick from the emoji list, or type a shortcode (without colons).",
|
|
5996
|
+
htmlFor: `${idPrefix}-emoji`,
|
|
5997
|
+
children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5", children: [
|
|
5998
|
+
/* @__PURE__ */ jsx(
|
|
5999
|
+
Input,
|
|
6000
|
+
{
|
|
6001
|
+
id: `${idPrefix}-emoji`,
|
|
6002
|
+
value: emoji.name ?? "",
|
|
6003
|
+
placeholder: "e.g. wave",
|
|
6004
|
+
onChange: (e) => onChange({ ...emoji, name: e.target.value })
|
|
6005
|
+
}
|
|
6006
|
+
),
|
|
6007
|
+
/* @__PURE__ */ jsx(
|
|
6008
|
+
EmojiPickerButton,
|
|
6009
|
+
{
|
|
6010
|
+
align: "end",
|
|
6011
|
+
label: "Pick emoji",
|
|
6012
|
+
className: "shrink-0 border",
|
|
6013
|
+
onSelect: (sel) => onChange({ ...emoji, name: sel.name, skin_tone: sel.skinTone ?? void 0, unicode: void 0 })
|
|
6014
|
+
}
|
|
6015
|
+
)
|
|
6016
|
+
] })
|
|
5393
6017
|
}
|
|
5394
|
-
)
|
|
6018
|
+
);
|
|
5395
6019
|
}
|
|
5396
6020
|
if (kind === "user") {
|
|
5397
6021
|
const user = element;
|
|
@@ -5600,8 +6224,59 @@ function LossyBanner({ reasons }) {
|
|
|
5600
6224
|
] })
|
|
5601
6225
|
] });
|
|
5602
6226
|
}
|
|
6227
|
+
var RICH_TEXT_INPUT_RULE_ALLOWLIST = ["bold", "italic", "strike", "code"];
|
|
6228
|
+
function buildRichTextExtensions() {
|
|
6229
|
+
return [
|
|
6230
|
+
StarterKit.configure({
|
|
6231
|
+
heading: false,
|
|
6232
|
+
horizontalRule: false,
|
|
6233
|
+
// StarterKit bundles its own Link extension (enabled by default),
|
|
6234
|
+
// which would register a second, unconfigured `link` mark and a
|
|
6235
|
+
// duplicate autolinker alongside the hardened one we add below.
|
|
6236
|
+
// Disable it so our single, security-configured Link is the only
|
|
6237
|
+
// link extension in the schema.
|
|
6238
|
+
link: false
|
|
6239
|
+
}),
|
|
6240
|
+
EmojiNode,
|
|
6241
|
+
Link.configure({
|
|
6242
|
+
openOnClick: false,
|
|
6243
|
+
autolink: true,
|
|
6244
|
+
defaultProtocol: "https",
|
|
6245
|
+
// Pin TipTap's link allowlist to the same set as our shared
|
|
6246
|
+
// `isSafeHref` helper. The upstream default also includes
|
|
6247
|
+
// ftp/cid/callto which we don't expect inside Slack content.
|
|
6248
|
+
protocols: ["http", "https", "mailto", "tel", "sms", "xmpp"],
|
|
6249
|
+
// Belt-and-suspenders: even if a downstream contributor widens
|
|
6250
|
+
// `protocols` later, our isSafeHref guard rejects everything
|
|
6251
|
+
// outside the safe-link set. setLink and toggleLink both gate
|
|
6252
|
+
// on this hook before applying the mark.
|
|
6253
|
+
isAllowedUri: (url) => isSafeHref(url),
|
|
6254
|
+
// Only auto-link tokens that carry an explicit, safe scheme. TipTap's
|
|
6255
|
+
// default would link any `host.tld` (e.g. `2.xyz`, `report.zip`,
|
|
6256
|
+
// `logo.png`) because the suffix is a real gTLD, which is wrong for
|
|
6257
|
+
// free-form Slack message text. Deliberate links can still be added
|
|
6258
|
+
// via the link button.
|
|
6259
|
+
shouldAutoLink: hasExplicitSafeScheme,
|
|
6260
|
+
HTMLAttributes: { rel: "noreferrer noopener", target: "_blank" }
|
|
6261
|
+
})
|
|
6262
|
+
];
|
|
6263
|
+
}
|
|
5603
6264
|
function RichTextWysiwygEditor({ block, onChange }) {
|
|
5604
6265
|
const lastEmittedRef = useRef(null);
|
|
6266
|
+
const { byName: customByName } = useCustomEmojis();
|
|
6267
|
+
const [emojiIndex, setEmojiIndex] = useState(null);
|
|
6268
|
+
useEffect(() => {
|
|
6269
|
+
let active = true;
|
|
6270
|
+
loadEmojiIndex().then((idx) => active && setEmojiIndex(idx)).catch(() => {
|
|
6271
|
+
});
|
|
6272
|
+
return () => {
|
|
6273
|
+
active = false;
|
|
6274
|
+
};
|
|
6275
|
+
}, []);
|
|
6276
|
+
const resolveEmoji = useMemo(
|
|
6277
|
+
() => makeEmojiImportResolver(customByName, emojiIndex),
|
|
6278
|
+
[customByName, emojiIndex]
|
|
6279
|
+
);
|
|
5605
6280
|
const handleUpdate = useCallback(
|
|
5606
6281
|
(editor2) => {
|
|
5607
6282
|
const next = proseMirrorToRichText(editor2.getJSON());
|
|
@@ -5611,28 +6286,9 @@ function RichTextWysiwygEditor({ block, onChange }) {
|
|
|
5611
6286
|
[onChange]
|
|
5612
6287
|
);
|
|
5613
6288
|
const editor = useEditor({
|
|
5614
|
-
extensions:
|
|
5615
|
-
|
|
5616
|
-
|
|
5617
|
-
horizontalRule: false
|
|
5618
|
-
}),
|
|
5619
|
-
Link.configure({
|
|
5620
|
-
openOnClick: false,
|
|
5621
|
-
autolink: true,
|
|
5622
|
-
defaultProtocol: "https",
|
|
5623
|
-
// Pin TipTap's link allowlist to the same set as our shared
|
|
5624
|
-
// `isSafeHref` helper. The upstream default also includes
|
|
5625
|
-
// ftp/cid/callto which we don't expect inside Slack content.
|
|
5626
|
-
protocols: ["http", "https", "mailto", "tel", "sms", "xmpp"],
|
|
5627
|
-
// Belt-and-suspenders: even if a downstream contributor widens
|
|
5628
|
-
// `protocols` later, our isSafeHref guard rejects everything
|
|
5629
|
-
// outside the safe-link set. setLink and toggleLink both gate
|
|
5630
|
-
// on this hook before applying the mark.
|
|
5631
|
-
isAllowedUri: (url) => isSafeHref(url),
|
|
5632
|
-
HTMLAttributes: { rel: "noreferrer noopener", target: "_blank" }
|
|
5633
|
-
})
|
|
5634
|
-
],
|
|
5635
|
-
content: richTextToProseMirror(block),
|
|
6289
|
+
extensions: buildRichTextExtensions(),
|
|
6290
|
+
enableInputRules: RICH_TEXT_INPUT_RULE_ALLOWLIST,
|
|
6291
|
+
content: richTextToProseMirror(block, resolveEmoji),
|
|
5636
6292
|
editorProps: {
|
|
5637
6293
|
attributes: {
|
|
5638
6294
|
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 +6303,10 @@ function RichTextWysiwygEditor({ block, onChange }) {
|
|
|
5647
6303
|
if (lastEmittedRef.current && JSON.stringify(lastEmittedRef.current) === JSON.stringify(block)) {
|
|
5648
6304
|
return;
|
|
5649
6305
|
}
|
|
5650
|
-
editor.commands.setContent(richTextToProseMirror(block), {
|
|
6306
|
+
editor.commands.setContent(richTextToProseMirror(block, resolveEmoji), {
|
|
5651
6307
|
emitUpdate: false
|
|
5652
6308
|
});
|
|
5653
|
-
}, [block, editor]);
|
|
6309
|
+
}, [block, editor, resolveEmoji]);
|
|
5654
6310
|
if (!editor) {
|
|
5655
6311
|
return /* @__PURE__ */ jsx("div", { className: "min-h-[120px] rounded-md border border-input bg-background" });
|
|
5656
6312
|
}
|
|
@@ -5699,6 +6355,16 @@ function Toolbar({ editor }) {
|
|
|
5699
6355
|
),
|
|
5700
6356
|
/* @__PURE__ */ jsx(Divider, {}),
|
|
5701
6357
|
/* @__PURE__ */ jsx(LinkPopover, { editor }),
|
|
6358
|
+
/* @__PURE__ */ jsx(
|
|
6359
|
+
EmojiPickerButton,
|
|
6360
|
+
{
|
|
6361
|
+
align: "start",
|
|
6362
|
+
onSelect: (sel) => editor.chain().focus().insertContent({
|
|
6363
|
+
type: "emoji",
|
|
6364
|
+
attrs: { name: sel.name, src: sel.src, unicode: sel.unicode, skinTone: sel.skinTone }
|
|
6365
|
+
}).run()
|
|
6366
|
+
}
|
|
6367
|
+
),
|
|
5702
6368
|
/* @__PURE__ */ jsx(Divider, {}),
|
|
5703
6369
|
/* @__PURE__ */ jsx(
|
|
5704
6370
|
ToolbarButton,
|
|
@@ -5897,6 +6563,8 @@ function defaultImage() {
|
|
|
5897
6563
|
function SectionEditor({ block, onChange }) {
|
|
5898
6564
|
const text = block.text?.text ?? "";
|
|
5899
6565
|
const accessoryKind = detectAccessory(block.accessory);
|
|
6566
|
+
const textRef = useRef(null);
|
|
6567
|
+
const setText = (next) => onChange({ ...block, text: { type: "mrkdwn", text: next } });
|
|
5900
6568
|
const setAccessoryKind = (next) => {
|
|
5901
6569
|
if (next === accessoryKind) {
|
|
5902
6570
|
return;
|
|
@@ -5920,19 +6588,20 @@ function SectionEditor({ block, onChange }) {
|
|
|
5920
6588
|
label: "Text",
|
|
5921
6589
|
help: "Supports *bold*, _italic_, ~strike~, `code`, and <url|link> formatting.",
|
|
5922
6590
|
htmlFor: "section-text",
|
|
5923
|
-
children: /* @__PURE__ */
|
|
5924
|
-
|
|
5925
|
-
|
|
5926
|
-
|
|
5927
|
-
|
|
5928
|
-
|
|
5929
|
-
|
|
5930
|
-
|
|
5931
|
-
|
|
5932
|
-
|
|
5933
|
-
}
|
|
5934
|
-
|
|
5935
|
-
|
|
6591
|
+
children: /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-1.5", children: [
|
|
6592
|
+
/* @__PURE__ */ jsx(
|
|
6593
|
+
Textarea,
|
|
6594
|
+
{
|
|
6595
|
+
ref: textRef,
|
|
6596
|
+
id: "section-text",
|
|
6597
|
+
value: text,
|
|
6598
|
+
rows: 4,
|
|
6599
|
+
placeholder: "e.g. Welcome to the channel! Ping <@U123> with any questions.",
|
|
6600
|
+
onChange: (e) => setText(e.target.value)
|
|
6601
|
+
}
|
|
6602
|
+
),
|
|
6603
|
+
/* @__PURE__ */ jsx(EmojiTextInsertButton, { targetRef: textRef, value: text, onChange: setText, className: "mt-1 shrink-0 border" })
|
|
6604
|
+
] })
|
|
5936
6605
|
}
|
|
5937
6606
|
),
|
|
5938
6607
|
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3 rounded-md border bg-muted/20 p-3", children: [
|
|
@@ -5973,19 +6642,22 @@ function ButtonAccessoryFields({ button, onChange }) {
|
|
|
5973
6642
|
const url = button.url ?? "";
|
|
5974
6643
|
const value = button.value ?? "";
|
|
5975
6644
|
const style = button.style === "primary" || button.style === "danger" ? button.style : "default";
|
|
6645
|
+
const labelRef = useRef(null);
|
|
6646
|
+
const setLabel = (next) => onChange({ ...button, text: { type: "plain_text", text: next, emoji: true } });
|
|
5976
6647
|
return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3", children: [
|
|
5977
|
-
/* @__PURE__ */ jsx(EditorField, { label: "Label", htmlFor: "section-acc-btn-label", children: /* @__PURE__ */
|
|
5978
|
-
|
|
5979
|
-
|
|
5980
|
-
|
|
5981
|
-
|
|
5982
|
-
|
|
5983
|
-
|
|
5984
|
-
|
|
5985
|
-
|
|
5986
|
-
}
|
|
5987
|
-
|
|
5988
|
-
|
|
6648
|
+
/* @__PURE__ */ jsx(EditorField, { label: "Label", htmlFor: "section-acc-btn-label", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5", children: [
|
|
6649
|
+
/* @__PURE__ */ jsx(
|
|
6650
|
+
Input,
|
|
6651
|
+
{
|
|
6652
|
+
ref: labelRef,
|
|
6653
|
+
id: "section-acc-btn-label",
|
|
6654
|
+
value: label,
|
|
6655
|
+
placeholder: "e.g. Learn more",
|
|
6656
|
+
onChange: (e) => setLabel(e.target.value)
|
|
6657
|
+
}
|
|
6658
|
+
),
|
|
6659
|
+
/* @__PURE__ */ jsx(EmojiTextInsertButton, { targetRef: labelRef, value: label, onChange: setLabel, className: "shrink-0 border" })
|
|
6660
|
+
] }) }),
|
|
5989
6661
|
/* @__PURE__ */ jsx(
|
|
5990
6662
|
EditorField,
|
|
5991
6663
|
{
|
|
@@ -7540,6 +8212,7 @@ function BlockKitchen(props) {
|
|
|
7540
8212
|
initialBlocks,
|
|
7541
8213
|
onChange,
|
|
7542
8214
|
previewHooks,
|
|
8215
|
+
customEmojis,
|
|
7543
8216
|
loadChannels,
|
|
7544
8217
|
loadSendAsUserStatus,
|
|
7545
8218
|
onSend,
|
|
@@ -7583,6 +8256,13 @@ function BlockKitchen(props) {
|
|
|
7583
8256
|
const [previewSurface, setPreviewSurface] = useState(allowedSurfaces[0]);
|
|
7584
8257
|
const [activePaletteVariantId, setActivePaletteVariantId] = useState(null);
|
|
7585
8258
|
const isMobile = useIsMobile();
|
|
8259
|
+
const mergedPreviewHooks = useMemo(() => {
|
|
8260
|
+
if (!customEmojis || customEmojis.length === 0) {
|
|
8261
|
+
return previewHooks;
|
|
8262
|
+
}
|
|
8263
|
+
const byName = new Map(customEmojis.map((emoji) => [emoji.name, emoji]));
|
|
8264
|
+
return { emoji: makeEmojiHook(byName), ...previewHooks };
|
|
8265
|
+
}, [customEmojis, previewHooks]);
|
|
7586
8266
|
const validation = useBlockKitValidation(blocks, "message");
|
|
7587
8267
|
const sensors = useSensors(
|
|
7588
8268
|
useSensor(PointerSensor, { activationConstraint: { distance: 4 } }),
|
|
@@ -7632,7 +8312,7 @@ function BlockKitchen(props) {
|
|
|
7632
8312
|
}, []);
|
|
7633
8313
|
const activePaletteVariant = activePaletteVariantId ? variantById.get(activePaletteVariantId) : null;
|
|
7634
8314
|
const blockPayloads = blocks.map((b) => b.block);
|
|
7635
|
-
return /* @__PURE__ */ jsx(BrandThemeScope, { theme, children: /* @__PURE__ */ jsx(TooltipProvider, { delayDuration: 200, children: /* @__PURE__ */ jsxs(
|
|
8315
|
+
return /* @__PURE__ */ jsx(BrandThemeScope, { theme, children: /* @__PURE__ */ jsx(CustomEmojiProvider, { customEmojis, children: /* @__PURE__ */ jsx(TooltipProvider, { delayDuration: 200, children: /* @__PURE__ */ jsxs(
|
|
7636
8316
|
DndContext,
|
|
7637
8317
|
{
|
|
7638
8318
|
sensors,
|
|
@@ -7678,7 +8358,7 @@ function BlockKitchen(props) {
|
|
|
7678
8358
|
{
|
|
7679
8359
|
blocks,
|
|
7680
8360
|
workspaceName,
|
|
7681
|
-
previewHooks,
|
|
8361
|
+
previewHooks: mergedPreviewHooks,
|
|
7682
8362
|
previewTheme,
|
|
7683
8363
|
previewSurface,
|
|
7684
8364
|
errorsByBlockId: validation.byBlockId,
|
|
@@ -7754,7 +8434,7 @@ function BlockKitchen(props) {
|
|
|
7754
8434
|
)
|
|
7755
8435
|
]
|
|
7756
8436
|
}
|
|
7757
|
-
) }) });
|
|
8437
|
+
) }) }) });
|
|
7758
8438
|
}
|
|
7759
8439
|
var UNCATEGORIZED = "Other";
|
|
7760
8440
|
function TemplateThumbnail({ blocks, theme }) {
|