slack-blocks-to-jsx 0.0.3

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.mjs ADDED
@@ -0,0 +1,434 @@
1
+ // src/utils/date.ts
2
+ var dateToTime = (timestamp) => {
3
+ const date = new Date(timestamp);
4
+ return date.toLocaleTimeString("en-US", {
5
+ hour: "numeric",
6
+ minute: "numeric",
7
+ hour12: false
8
+ });
9
+ };
10
+
11
+ // src/utils/emojis.ts
12
+ import { emojify } from "node-emoji";
13
+ var missing_emojis = {
14
+ "heart_hands": "\u{1FAF6}",
15
+ "white_frowning_face": "\u{1F641}"
16
+ };
17
+ var parseEmojis = (text) => {
18
+ return emojify(text, {
19
+ fallback: (name) => {
20
+ var _a;
21
+ return (_a = missing_emojis[name]) != null ? _a : `:${name}:`;
22
+ }
23
+ });
24
+ };
25
+
26
+ // src/utils/markdown.tsx
27
+ import parse from "html-react-parser";
28
+ var options = {
29
+ trim: false
30
+ };
31
+ var parseMrkdwn = (text) => {
32
+ if (!text)
33
+ return null;
34
+ const pre = /```(.*?)```/g;
35
+ const code = /`(?!(?:[^<]*>|[^<>]*<\/pre>))([^`]*)`/g;
36
+ const bold = /\*(?!(?:[^<]*>|[^<>]*<\/pre>))([^*]*)\*/g;
37
+ const italic = /_(?!(?:[^<]*>|[^<>]*<\/pre>))([^_]*)_/g;
38
+ const strikethrough = /~(?!(?:[^<]*>|[^<>]*<\/pre>))([^~]*)~/g;
39
+ const blockquote = new RegExp("(?<!<pre>)&gt;(.*)", "g");
40
+ const single_newline = /(\n)/g;
41
+ const consecutive_brs = /(<br \/>){2,}/g;
42
+ let parsed = text;
43
+ parsed = parsed.replace(pre, "<pre>$1</pre>");
44
+ parsed = parsed.replace(code, "<code>$1</code>");
45
+ parsed = parsed.replace(blockquote, "<blockquote>$1</blockquote>");
46
+ parsed = parsed.replace(bold, "<b>$1</b>");
47
+ parsed = parsed.replace(italic, "<i>$1</i>");
48
+ parsed = parsed.replace(strikethrough, "<s>$1</s>");
49
+ parsed = parsed.replace(single_newline, "<br />");
50
+ parsed = parsed.replace(consecutive_brs, (match) => {
51
+ var _a, _b;
52
+ const count = ((_b = (_a = match.match(/<br \/>/g)) == null ? void 0 : _a.length) != null ? _b : 0) - 1;
53
+ let spans = "<br />";
54
+ for (let i = 0; i < count; i++) {
55
+ spans += `<br style="display:block;content:'';margin-top:8px" />`;
56
+ }
57
+ return spans;
58
+ });
59
+ return parse(parsed, options);
60
+ };
61
+
62
+ // src/header.tsx
63
+ import { jsx, jsxs } from "react/jsx-runtime";
64
+ var Header = (props) => {
65
+ const { name, time } = props;
66
+ return /* @__PURE__ */ jsxs("div", { className: "flex gap-[5px] items-center w-full", children: [
67
+ /* @__PURE__ */ jsx("h3", { className: "font-black text-[15px]", children: name }),
68
+ /* @__PURE__ */ jsx("div", { className: "h-[14px] w-[25.31px] leading-[12.5px] py-[1px] px-[3px] uppercase text-[10px] text-black-primary/[0.7] bg-black-primary/[0.13] font-semibold rounded-[2px]", children: "APP" }),
69
+ /* @__PURE__ */ jsx("div", { className: "text-xs text-black-secondary uppercase leading-[17.6px]", children: dateToTime(time) })
70
+ ] });
71
+ };
72
+
73
+ // src/components/blocks/Divider.tsx
74
+ import { jsx as jsx2 } from "react/jsx-runtime";
75
+ var Divider = (props) => {
76
+ const { data } = props;
77
+ const { block_id } = data;
78
+ return /* @__PURE__ */ jsx2(
79
+ "div",
80
+ {
81
+ id: block_id,
82
+ className: "border-b border-gray-primary border-solid w-full mt-1 mb-2"
83
+ }
84
+ );
85
+ };
86
+
87
+ // src/components/elements/TextObject.tsx
88
+ import { jsx as jsx3 } from "react/jsx-runtime";
89
+ var TextObject = (props) => {
90
+ const { type, text, emoji } = props.data;
91
+ const emoji_parsed = type == "mrkdwn" ? parseEmojis(text) : emoji === false ? text : parseEmojis(text);
92
+ if (type === "plain_text")
93
+ return /* @__PURE__ */ jsx3("div", { children: emoji_parsed });
94
+ return /* @__PURE__ */ jsx3("div", { children: parseMrkdwn(emoji_parsed) });
95
+ };
96
+
97
+ // src/components/elements/ImageElement.tsx
98
+ import { jsx as jsx4 } from "react/jsx-runtime";
99
+ var ImageElement = (props) => {
100
+ const { alt_text, image_url } = props.data;
101
+ return /* @__PURE__ */ jsx4("div", { className: "relative w-6 h-[25.25px]", children: /* @__PURE__ */ jsx4(
102
+ "img",
103
+ {
104
+ src: image_url,
105
+ alt: alt_text,
106
+ className: "w-5 h-5 rounded-[2px] overflow-hidden"
107
+ }
108
+ ) });
109
+ };
110
+
111
+ // src/components/elements/ButtonElement.tsx
112
+ import { jsx as jsx5 } from "react/jsx-runtime";
113
+ var ButtonElement = (props) => {
114
+ const { text, action_id, url, style, accessibility_label } = props.data;
115
+ return /* @__PURE__ */ jsx5(
116
+ "button",
117
+ {
118
+ id: action_id,
119
+ "aria-label": accessibility_label,
120
+ className: `px-2 pt-0 pb-[1px] text-small h-[28px] min-w-[56px] border rounded whitespace-nowrap font-semibold ${style === "primary" ? "border-green-primary.3 text-green-primary" : style === "danger" ? "border-red-primary.3 text-red-primary" : "border-black-primary.3 text-black-primary"}`,
121
+ onClick: () => {
122
+ if (url) {
123
+ window.open(url, "_blank");
124
+ }
125
+ },
126
+ children: /* @__PURE__ */ jsx5(TextObject, { data: text })
127
+ }
128
+ );
129
+ };
130
+
131
+ // src/components/elements/StaticSelectElement.tsx
132
+ import { useEffect, useState } from "react";
133
+ import { jsx as jsx6, jsxs as jsxs2 } from "react/jsx-runtime";
134
+ var StaticSelectElement = (props) => {
135
+ const {
136
+ action_id,
137
+ options: options2,
138
+ // TODO: ADD CONFIRM DIALOG
139
+ // confirm
140
+ focus_on_load,
141
+ // TODO: ADD INITIAL OPTION
142
+ // initial_option,
143
+ // TODO: ADD OPTION GROUPS
144
+ // option_groups,
145
+ placeholder
146
+ } = props.data;
147
+ const [visible, setVisible] = useState(focus_on_load != null ? focus_on_load : false);
148
+ const [search, setSearch] = useState("");
149
+ const [selected, setSelected] = useState("");
150
+ useEffect(() => {
151
+ const handleClickOutside = (e) => {
152
+ var _a, _b;
153
+ const target = e.target;
154
+ console.log({ target });
155
+ console.log(target.id);
156
+ if (target.id !== "static_select_popup" && target.id !== "static_select_dropdown_button" && ((_a = target.parentElement) == null ? void 0 : _a.id) !== "static_select_popup" && ((_b = target.parentElement) == null ? void 0 : _b.id) !== "static_select_dropdown_button")
157
+ setVisible(false);
158
+ };
159
+ document.addEventListener("mousedown", handleClickOutside);
160
+ return () => {
161
+ document.removeEventListener("mousedown", handleClickOutside);
162
+ };
163
+ }, []);
164
+ const filteredOptions = options2.filter((option) => {
165
+ const results = option.text.text.toLowerCase().includes(search.toLowerCase());
166
+ return results;
167
+ }, []);
168
+ return /* @__PURE__ */ jsxs2(
169
+ "div",
170
+ {
171
+ id: action_id,
172
+ className: "py-1 px-2 h-7 min-h-[28px] relative flex items-center justify-between rounded text-small w-[190px] border border-black-primary.3",
173
+ tabIndex: 0,
174
+ children: [
175
+ visible && /* @__PURE__ */ jsx6(
176
+ "div",
177
+ {
178
+ className: "absolute top-[24px] -left-3 bg-white-secondary border border-gray-primary rounded-[6px] shadow-custom_shadow-1 w-[322px] max-w-[322px] max-h-[240px] overflow-auto py-3 z-20",
179
+ id: "static_select_popup",
180
+ children: /* @__PURE__ */ jsx6("div", { className: "flex flex-col", children: filteredOptions.map((option, i) => {
181
+ return /* @__PURE__ */ jsx6(
182
+ "div",
183
+ {
184
+ className: "h-7 px-6 flex items-center select-none cursor-pointer hover:bg-blue-primary hover:text-white-primary",
185
+ tabIndex: 0,
186
+ onClick: () => {
187
+ setVisible(false);
188
+ setSelected(option.value);
189
+ },
190
+ children: /* @__PURE__ */ jsx6(TextObject, { data: option.text })
191
+ },
192
+ i
193
+ );
194
+ }) })
195
+ }
196
+ ),
197
+ /* @__PURE__ */ jsx6(
198
+ "input",
199
+ {
200
+ type: "text",
201
+ value: search,
202
+ onChange: (e) => setSearch(e.currentTarget.value),
203
+ className: "w-full py-1 pr-7 focus:outline-none",
204
+ tabIndex: -1,
205
+ "aria-label": "Select an item"
206
+ }
207
+ ),
208
+ !search && /* @__PURE__ */ jsx6("div", { className: "absolute w-full h-full pointer-events-none flex items-center text-black-primary.3", children: /* @__PURE__ */ jsx6(
209
+ TextObject,
210
+ {
211
+ data: placeholder != null ? placeholder : {
212
+ type: "plain_text",
213
+ text: "Pick an option"
214
+ }
215
+ }
216
+ ) }),
217
+ /* @__PURE__ */ jsx6(
218
+ "button",
219
+ {
220
+ className: "absolute right-1 top-1/2 flex justify-center items-center w-5 -translate-y-1/2 text-black-secondary z-10 h-full",
221
+ id: "static_select_dropdown_button",
222
+ onClick: () => {
223
+ setVisible((prev) => !prev);
224
+ },
225
+ children: /* @__PURE__ */ jsx6(
226
+ "svg",
227
+ {
228
+ xmlns: "http://www.w3.org/2000/svg",
229
+ fill: "none",
230
+ viewBox: "0 0 24 24",
231
+ strokeWidth: "3",
232
+ stroke: "currentColor",
233
+ className: "w-3 h-3",
234
+ children: /* @__PURE__ */ jsx6(
235
+ "path",
236
+ {
237
+ strokeLinecap: "round",
238
+ strokeLinejoin: "round",
239
+ d: "M19.5 8.25l-7.5 7.5-7.5-7.5"
240
+ }
241
+ )
242
+ }
243
+ )
244
+ }
245
+ )
246
+ ]
247
+ }
248
+ );
249
+ };
250
+
251
+ // src/components/blocks/Section.tsx
252
+ import { jsx as jsx7, jsxs as jsxs3 } from "react/jsx-runtime";
253
+ var Section = (props) => {
254
+ const { accessory, fields, text, block_id } = props.data;
255
+ return /* @__PURE__ */ jsxs3(
256
+ "div",
257
+ {
258
+ id: block_id,
259
+ className: "mt-2 mb-1 text-primary flex w-full text-black-primary",
260
+ children: [
261
+ /* @__PURE__ */ jsx7("div", { className: "grow", children: /* @__PURE__ */ jsxs3("div", { className: "flex flex-col gap-3", children: [
262
+ text && /* @__PURE__ */ jsx7(TextObject, { data: text }),
263
+ fields && /* @__PURE__ */ jsx7("div", { className: "grid grid-col-2 w-full", children: fields.map((field, i) => {
264
+ return /* @__PURE__ */ jsx7(TextObject, { data: field }, i);
265
+ }) })
266
+ ] }) }),
267
+ accessory && accessory.type === "image" && /* @__PURE__ */ jsx7("div", { className: "ml-2 mb-1 relative shrink-0", children: /* @__PURE__ */ jsx7(
268
+ "img",
269
+ {
270
+ src: accessory.image_url,
271
+ alt: accessory.alt_text,
272
+ className: "w-[88px] h-[88px] object-cover ml-1 rounded-[8px] overflow-hidden"
273
+ }
274
+ ) })
275
+ ]
276
+ }
277
+ );
278
+ };
279
+
280
+ // src/components/blocks/Image.tsx
281
+ import { useState as useState2 } from "react";
282
+ import { jsx as jsx8, jsxs as jsxs4 } from "react/jsx-runtime";
283
+ var Image = (props) => {
284
+ const {
285
+ block_id,
286
+ alt_text,
287
+ image_url,
288
+ image_bytes,
289
+ image_height,
290
+ image_width
291
+ } = props.data;
292
+ const [showImage, setShowImage] = useState2(true);
293
+ return /* @__PURE__ */ jsxs4("div", { id: block_id, className: "my-2 flex flex-col gap-2", children: [
294
+ /* @__PURE__ */ jsxs4("div", { className: "text-black-secondary text-small flex gap-1 items-center", children: [
295
+ image_bytes && `(${Math.round(image_bytes / 1e3)} kB)`,
296
+ /* @__PURE__ */ jsx8(
297
+ "button",
298
+ {
299
+ className: "w-[15px] h-[15px]",
300
+ onClick: () => setShowImage((prev) => !prev),
301
+ children: /* @__PURE__ */ jsx8(
302
+ "svg",
303
+ {
304
+ xmlns: "http://www.w3.org/2000/svg",
305
+ fillRule: "evenodd",
306
+ clipRule: "evenodd",
307
+ className: "text-blue-primary",
308
+ imageRendering: "optimizeQuality",
309
+ shapeRendering: "geometricPrecision",
310
+ textRendering: "geometricPrecision",
311
+ viewBox: "0 0 512 336.36",
312
+ width: 6,
313
+ height: 6,
314
+ style: {
315
+ transform: showImage ? "rotate(0deg)" : "rotate(-90deg)"
316
+ },
317
+ children: /* @__PURE__ */ jsx8(
318
+ "path",
319
+ {
320
+ fill: "currentColor",
321
+ fillRule: "nonzero",
322
+ d: "M42.47.01L469.5 0C492.96 0 512 19.04 512 42.5c0 11.07-4.23 21.15-11.17 28.72L294.18 320.97c-14.93 18.06-41.7 20.58-59.76 5.65-1.8-1.49-3.46-3.12-4.97-4.83L10.43 70.39C-4.97 52.71-3.1 25.86 14.58 10.47 22.63 3.46 32.57.02 42.47.01z"
323
+ }
324
+ )
325
+ }
326
+ )
327
+ }
328
+ )
329
+ ] }),
330
+ showImage && /* @__PURE__ */ jsx8("div", { className: "rounded-md overflow-hidden relative max-w-max", children: /* @__PURE__ */ jsx8(
331
+ "img",
332
+ {
333
+ src: image_url,
334
+ style: {
335
+ width: image_width || 400,
336
+ height: image_height || 300
337
+ },
338
+ alt: alt_text
339
+ }
340
+ ) })
341
+ ] });
342
+ };
343
+
344
+ // src/components/blocks/Context.tsx
345
+ import { jsx as jsx9 } from "react/jsx-runtime";
346
+ var Context = (props) => {
347
+ const { elements, block_id } = props.data;
348
+ return /* @__PURE__ */ jsx9(
349
+ "div",
350
+ {
351
+ id: block_id,
352
+ className: "my-1 text-primary flex w-full text-black-primary flex-wrap items-center",
353
+ children: elements.map((element, i) => {
354
+ if (element.type !== "image") {
355
+ return /* @__PURE__ */ jsx9(
356
+ "span",
357
+ {
358
+ className: "text-black-secondary text-small h-[25.25px] pr-3 flex items-center",
359
+ children: /* @__PURE__ */ jsx9(TextObject, { data: element })
360
+ },
361
+ i
362
+ );
363
+ }
364
+ return /* @__PURE__ */ jsx9("span", { children: /* @__PURE__ */ jsx9(ImageElement, { data: element }) }, i);
365
+ })
366
+ }
367
+ );
368
+ };
369
+
370
+ // src/components/blocks/Actions.tsx
371
+ import { jsx as jsx10 } from "react/jsx-runtime";
372
+ var Actions = (props) => {
373
+ const { elements, block_id } = props.data;
374
+ return /* @__PURE__ */ jsx10(
375
+ "div",
376
+ {
377
+ id: block_id,
378
+ className: "mb-2 text-primary flex w-full text-black-primary items-center",
379
+ children: /* @__PURE__ */ jsx10("div", { className: "flex flex-wrap", children: elements.map((element, i) => {
380
+ if (element.type === "button") {
381
+ return /* @__PURE__ */ jsx10("div", { className: "mt-2 mr-2", children: /* @__PURE__ */ jsx10(ButtonElement, { data: element }) }, i);
382
+ }
383
+ if (element.type === "static_select") {
384
+ return /* @__PURE__ */ jsx10("div", { className: "mt-2 mr-2", children: /* @__PURE__ */ jsx10(StaticSelectElement, { data: element }) }, i);
385
+ }
386
+ return /* @__PURE__ */ jsx10("div", { className: "mt-2 mr-2" }, i);
387
+ }) })
388
+ }
389
+ );
390
+ };
391
+
392
+ // src/components/index.tsx
393
+ import { jsx as jsx11 } from "react/jsx-runtime";
394
+ var getBlockComponent = (block) => {
395
+ if (block.type === "divider")
396
+ return /* @__PURE__ */ jsx11(Divider, { data: block });
397
+ if (block.type === "section")
398
+ return /* @__PURE__ */ jsx11(Section, { data: block });
399
+ if (block.type === "image")
400
+ return /* @__PURE__ */ jsx11(Image, { data: block });
401
+ if (block.type === "context")
402
+ return /* @__PURE__ */ jsx11(Context, { data: block });
403
+ if (block.type === "actions")
404
+ return /* @__PURE__ */ jsx11(Actions, { data: block });
405
+ return null;
406
+ };
407
+
408
+ // src/block-wrapper.tsx
409
+ import { jsx as jsx12 } from "react/jsx-runtime";
410
+ var BlockWrapper = (props) => {
411
+ const { children } = props;
412
+ return /* @__PURE__ */ jsx12("div", { className: "text-base break-words font-normal", children });
413
+ };
414
+
415
+ // src/message.tsx
416
+ import { jsx as jsx13, jsxs as jsxs5 } from "react/jsx-runtime";
417
+ var Message = (props) => {
418
+ const { theme = "light", blocks, logo, name, time = /* @__PURE__ */ new Date() } = props;
419
+ return /* @__PURE__ */ jsxs5("section", { className: "flex gap-2 w-full max-w-[600px] slack-message", children: [
420
+ /* @__PURE__ */ jsx13("div", { className: "shrink-0", children: /* @__PURE__ */ jsx13("img", { src: logo, className: "w-9 h-9", alt: name }) }),
421
+ /* @__PURE__ */ jsxs5("div", { className: "flex flex-col w-full", children: [
422
+ /* @__PURE__ */ jsx13(Header, { name, time }),
423
+ blocks.map((block, i) => {
424
+ const element = getBlockComponent(block);
425
+ if (!element)
426
+ return null;
427
+ return /* @__PURE__ */ jsx13(BlockWrapper, { children: element }, i);
428
+ })
429
+ ] })
430
+ ] });
431
+ };
432
+ export {
433
+ Message
434
+ };
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "slack-blocks-to-jsx",
3
+ "author": "Manish Panwar <codeemash@gmail.com>",
4
+ "description": "A library to convert Slack blocks to JSX components to be used with React",
5
+ "keywords": [
6
+ "slack",
7
+ "slack-blocks",
8
+ "slack-blocks-to-jsx",
9
+ "slack-blocks-to-react",
10
+ "slack-blocks-to-react-comp"
11
+ ],
12
+ "repository": "https://github.com/themashcodee/slack-blocks-to-jsx.git",
13
+ "license": "MIT",
14
+ "version": "0.0.3",
15
+ "main": "dist/index.js",
16
+ "module": "dist/index.mjs",
17
+ "types": "dist/index.d.ts",
18
+ "bugs": {
19
+ "url": "https://github.com/themashcodee/slack-blocks-to-jsx/issues"
20
+ },
21
+ "homepage": "https://github.com/themashcodee/slack-blocks-to-jsx#readme",
22
+ "scripts": {
23
+ "build": "tsup ./src/index.ts --format cjs,esm --dts",
24
+ "release": "pnpm run build && changeset publish",
25
+ "lint": "tsc"
26
+ },
27
+ "dependencies": {
28
+ "html-react-parser": "^4.2.2",
29
+ "node-emoji": "^2.1.0"
30
+ },
31
+ "peerDependencies": {
32
+ "react": "^17.0.0",
33
+ "react-dom": "^17.0.0"
34
+ },
35
+ "devDependencies": {
36
+ "@changesets/cli": "^2.26.2",
37
+ "@types/react": "17.0.0",
38
+ "@types/react-dom": "17.0.0",
39
+ "tsup": "^7.2.0",
40
+ "typescript": "^5.2.2"
41
+ }
42
+ }
package/readme.md ADDED
@@ -0,0 +1 @@
1
+ # Slack Blocks to JSX