@medalsocial/meda 2.0.0 → 2.2.0

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.
Files changed (237) hide show
  1. package/dist/calendar/calendar-toolbar.d.ts +2 -0
  2. package/dist/calendar/calendar-toolbar.js +22 -0
  3. package/dist/calendar/calendar.d.ts +2 -0
  4. package/dist/calendar/calendar.js +55 -0
  5. package/dist/calendar/day-view.d.ts +2 -0
  6. package/dist/calendar/day-view.js +40 -0
  7. package/dist/calendar/index.d.ts +6 -0
  8. package/dist/calendar/index.js +7 -0
  9. package/dist/calendar/internal/date.d.ts +26 -0
  10. package/dist/calendar/internal/date.js +107 -0
  11. package/dist/calendar/internal/day-cell.d.ts +15 -0
  12. package/dist/calendar/internal/day-cell.js +38 -0
  13. package/dist/calendar/internal/event-pill.d.ts +13 -0
  14. package/dist/calendar/internal/event-pill.js +30 -0
  15. package/dist/calendar/internal/time-slot.d.ts +17 -0
  16. package/dist/calendar/internal/time-slot.js +23 -0
  17. package/dist/calendar/month-view.d.ts +2 -0
  18. package/dist/calendar/month-view.js +46 -0
  19. package/dist/calendar/public.d.ts +6 -0
  20. package/dist/calendar/public.js +8 -0
  21. package/dist/calendar/types.d.ts +113 -0
  22. package/dist/calendar/types.js +15 -0
  23. package/dist/calendar/week-view.d.ts +2 -0
  24. package/dist/calendar/week-view.js +39 -0
  25. package/dist/chat/turn-card.js +10 -5
  26. package/dist/email-builder/block-error-boundary.d.ts +16 -0
  27. package/dist/email-builder/block-error-boundary.js +21 -0
  28. package/dist/email-builder/block-palette.d.ts +7 -0
  29. package/dist/email-builder/block-palette.js +22 -0
  30. package/dist/email-builder/block-registry.d.ts +15 -0
  31. package/dist/email-builder/block-registry.js +157 -0
  32. package/dist/email-builder/block-renderer.d.ts +5 -0
  33. package/dist/email-builder/block-renderer.js +40 -0
  34. package/dist/email-builder/block-renderers/button.d.ts +4 -0
  35. package/dist/email-builder/block-renderers/button.js +26 -0
  36. package/dist/email-builder/block-renderers/columns.d.ts +5 -0
  37. package/dist/email-builder/block-renderers/columns.js +27 -0
  38. package/dist/email-builder/block-renderers/divider.d.ts +4 -0
  39. package/dist/email-builder/block-renderers/divider.js +14 -0
  40. package/dist/email-builder/block-renderers/footer.d.ts +4 -0
  41. package/dist/email-builder/block-renderers/footer.js +12 -0
  42. package/dist/email-builder/block-renderers/heading.d.ts +4 -0
  43. package/dist/email-builder/block-renderers/heading.js +19 -0
  44. package/dist/email-builder/block-renderers/image.d.ts +4 -0
  45. package/dist/email-builder/block-renderers/image.js +18 -0
  46. package/dist/email-builder/block-renderers/social.d.ts +4 -0
  47. package/dist/email-builder/block-renderers/social.js +19 -0
  48. package/dist/email-builder/block-renderers/spacer.d.ts +4 -0
  49. package/dist/email-builder/block-renderers/spacer.js +5 -0
  50. package/dist/email-builder/block-renderers/text.d.ts +4 -0
  51. package/dist/email-builder/block-renderers/text.js +17 -0
  52. package/dist/email-builder/builder-canvas.d.ts +15 -0
  53. package/dist/email-builder/builder-canvas.js +28 -0
  54. package/dist/email-builder/builder-header.d.ts +9 -0
  55. package/dist/email-builder/builder-header.js +8 -0
  56. package/dist/email-builder/builder-left-tabs.d.ts +11 -0
  57. package/dist/email-builder/builder-left-tabs.js +20 -0
  58. package/dist/email-builder/email-builder.d.ts +3 -0
  59. package/dist/email-builder/email-builder.js +94 -0
  60. package/dist/email-builder/envelope-card.d.ts +7 -0
  61. package/dist/email-builder/envelope-card.js +16 -0
  62. package/dist/email-builder/floating-bar.d.ts +13 -0
  63. package/dist/email-builder/floating-bar.js +9 -0
  64. package/dist/email-builder/index.d.ts +4 -0
  65. package/dist/email-builder/index.js +12 -0
  66. package/dist/email-builder/internal/builder-state-utils.d.ts +7 -0
  67. package/dist/email-builder/internal/builder-state-utils.js +53 -0
  68. package/dist/email-builder/internal/dnd-wrapper.d.ts +10 -0
  69. package/dist/email-builder/internal/dnd-wrapper.js +9 -0
  70. package/dist/email-builder/internal/email-font-options.d.ts +5 -0
  71. package/dist/email-builder/internal/email-font-options.js +12 -0
  72. package/dist/email-builder/mobile-drawers.d.ts +10 -0
  73. package/dist/email-builder/mobile-drawers.js +7 -0
  74. package/dist/email-builder/mobile-tab-bar.d.ts +10 -0
  75. package/dist/email-builder/mobile-tab-bar.js +9 -0
  76. package/dist/email-builder/property-editors/_shared.d.ts +58 -0
  77. package/dist/email-builder/property-editors/_shared.js +54 -0
  78. package/dist/email-builder/property-editors/button-editor.d.ts +7 -0
  79. package/dist/email-builder/property-editors/button-editor.js +10 -0
  80. package/dist/email-builder/property-editors/columns-editor.d.ts +7 -0
  81. package/dist/email-builder/property-editors/columns-editor.js +15 -0
  82. package/dist/email-builder/property-editors/divider-editor.d.ts +7 -0
  83. package/dist/email-builder/property-editors/divider-editor.js +10 -0
  84. package/dist/email-builder/property-editors/footer-editor.d.ts +7 -0
  85. package/dist/email-builder/property-editors/footer-editor.js +6 -0
  86. package/dist/email-builder/property-editors/heading-editor.d.ts +7 -0
  87. package/dist/email-builder/property-editors/heading-editor.js +16 -0
  88. package/dist/email-builder/property-editors/image-editor.d.ts +8 -0
  89. package/dist/email-builder/property-editors/image-editor.js +17 -0
  90. package/dist/email-builder/property-editors/social-editor.d.ts +7 -0
  91. package/dist/email-builder/property-editors/social-editor.js +18 -0
  92. package/dist/email-builder/property-editors/spacer-editor.d.ts +7 -0
  93. package/dist/email-builder/property-editors/spacer-editor.js +6 -0
  94. package/dist/email-builder/property-editors/text-editor.d.ts +13 -0
  95. package/dist/email-builder/property-editors/text-editor.js +8 -0
  96. package/dist/email-builder/property-inspector.d.ts +11 -0
  97. package/dist/email-builder/property-inspector.js +42 -0
  98. package/dist/email-builder/public.d.ts +1 -0
  99. package/dist/email-builder/public.js +3 -0
  100. package/dist/email-builder/render-to-email-html.d.ts +11 -0
  101. package/dist/email-builder/render-to-email-html.js +220 -0
  102. package/dist/email-builder/starter-shell.d.ts +3 -0
  103. package/dist/email-builder/starter-shell.js +21 -0
  104. package/dist/email-builder/types.d.ts +182 -0
  105. package/dist/email-builder/types.js +41 -0
  106. package/dist/email-builder/view-controls.d.ts +10 -0
  107. package/dist/email-builder/view-controls.js +12 -0
  108. package/dist/index.d.ts +4 -0
  109. package/dist/index.js +4 -0
  110. package/dist/lib/format-time.js +2 -3
  111. package/dist/marketing/index.d.ts +10 -1
  112. package/dist/marketing/index.js +9 -0
  113. package/dist/marketing/marketing-bento-card.d.ts +2 -0
  114. package/dist/marketing/marketing-bento-card.js +24 -0
  115. package/dist/marketing/marketing-bento-grid.d.ts +2 -0
  116. package/dist/marketing/marketing-bento-grid.js +12 -0
  117. package/dist/marketing/marketing-faq.d.ts +2 -0
  118. package/dist/marketing/marketing-faq.js +12 -0
  119. package/dist/marketing/marketing-footer.d.ts +2 -0
  120. package/dist/marketing/marketing-footer.js +5 -0
  121. package/dist/marketing/marketing-header-logo.d.ts +10 -0
  122. package/dist/marketing/marketing-header-logo.js +6 -0
  123. package/dist/marketing/marketing-header.d.ts +2 -0
  124. package/dist/marketing/marketing-header.js +26 -0
  125. package/dist/marketing/marketing-nav-item.d.ts +4 -0
  126. package/dist/marketing/marketing-nav-item.js +10 -0
  127. package/dist/marketing/marketing-shell.d.ts +2 -0
  128. package/dist/marketing/marketing-shell.js +5 -0
  129. package/dist/marketing/marketing-trust-bar.d.ts +2 -0
  130. package/dist/marketing/marketing-trust-bar.js +5 -0
  131. package/dist/marketing/public.d.ts +1 -1
  132. package/dist/marketing/types.d.ts +99 -0
  133. package/dist/post-preview/index.d.ts +4 -0
  134. package/dist/post-preview/index.js +7 -0
  135. package/dist/post-preview/internal/avatar.d.ts +12 -0
  136. package/dist/post-preview/internal/avatar.js +21 -0
  137. package/dist/post-preview/internal/format-content.d.ts +12 -0
  138. package/dist/post-preview/internal/format-content.js +32 -0
  139. package/dist/post-preview/internal/platform-meta.d.ts +10 -0
  140. package/dist/post-preview/internal/platform-meta.js +18 -0
  141. package/dist/post-preview/internal/textarea.d.ts +8 -0
  142. package/dist/post-preview/internal/textarea.js +12 -0
  143. package/dist/post-preview/internal/use-swipe.d.ts +17 -0
  144. package/dist/post-preview/internal/use-swipe.js +29 -0
  145. package/dist/post-preview/platforms/bluesky.d.ts +15 -0
  146. package/dist/post-preview/platforms/bluesky.js +21 -0
  147. package/dist/post-preview/platforms/discord.d.ts +44 -0
  148. package/dist/post-preview/platforms/discord.js +34 -0
  149. package/dist/post-preview/platforms/facebook.d.ts +23 -0
  150. package/dist/post-preview/platforms/facebook.js +26 -0
  151. package/dist/post-preview/platforms/generic.d.ts +22 -0
  152. package/dist/post-preview/platforms/generic.js +30 -0
  153. package/dist/post-preview/platforms/google-business.d.ts +18 -0
  154. package/dist/post-preview/platforms/google-business.js +24 -0
  155. package/dist/post-preview/platforms/index.d.ts +24 -0
  156. package/dist/post-preview/platforms/index.js +13 -0
  157. package/dist/post-preview/platforms/instagram.d.ts +31 -0
  158. package/dist/post-preview/platforms/instagram.js +104 -0
  159. package/dist/post-preview/platforms/linkedin.d.ts +52 -0
  160. package/dist/post-preview/platforms/linkedin.js +140 -0
  161. package/dist/post-preview/platforms/telegram.d.ts +46 -0
  162. package/dist/post-preview/platforms/telegram.js +44 -0
  163. package/dist/post-preview/platforms/threads.d.ts +19 -0
  164. package/dist/post-preview/platforms/threads.js +26 -0
  165. package/dist/post-preview/platforms/tiktok.d.ts +18 -0
  166. package/dist/post-preview/platforms/tiktok.js +25 -0
  167. package/dist/post-preview/platforms/twitter.d.ts +40 -0
  168. package/dist/post-preview/platforms/twitter.js +137 -0
  169. package/dist/post-preview/platforms/youtube.d.ts +17 -0
  170. package/dist/post-preview/platforms/youtube.js +23 -0
  171. package/dist/post-preview/post-preview.d.ts +29 -0
  172. package/dist/post-preview/post-preview.js +70 -0
  173. package/dist/post-preview/public.d.ts +1 -0
  174. package/dist/post-preview/public.js +6 -0
  175. package/dist/post-preview/types.d.ts +82 -0
  176. package/dist/post-preview/types.js +1 -0
  177. package/dist/primitives/filter-rail.js +17 -2
  178. package/dist/shell/command-palette.js +2 -0
  179. package/dist/shell/drag-mode-banner.js +4 -0
  180. package/dist/shell/extras/shell-scrollable-content.js +2 -2
  181. package/dist/shell/extras/workbench-layout.js +24 -13
  182. package/dist/shell/internal/mobile-bottom-nav.js +1 -0
  183. package/dist/shell/internal/mobile-drawers.js +5 -0
  184. package/dist/shell/rail-drop-slot.js +20 -3
  185. package/dist/shell/rail-drop-zones.js +2 -0
  186. package/dist/shell/right-panel.js +10 -1
  187. package/dist/shell/shell-header.js +7 -1
  188. package/dist/shell/theme-next-themes.js +7 -2
  189. package/dist/shell/theme.js +7 -1
  190. package/dist/shell/use-shell-viewport.js +2 -0
  191. package/dist/timeline/lane-timeline.js +1 -1
  192. package/dist/timeline/scrub-bar.js +1 -1
  193. package/dist/timeline/timeline-tape.js +8 -5
  194. package/dist/voice/voice-orb-scene.js +4 -4
  195. package/dist/voice/voice-status-pill.js +8 -7
  196. package/dist/workflow-builder/edges/default-edge.d.ts +10 -0
  197. package/dist/workflow-builder/edges/default-edge.js +21 -0
  198. package/dist/workflow-builder/edges/index.d.ts +3 -0
  199. package/dist/workflow-builder/edges/index.js +5 -0
  200. package/dist/workflow-builder/index.d.ts +5 -0
  201. package/dist/workflow-builder/index.js +12 -0
  202. package/dist/workflow-builder/internal/trigger-icons.d.ts +4 -0
  203. package/dist/workflow-builder/internal/trigger-icons.js +43 -0
  204. package/dist/workflow-builder/internal/trigger-sentence.d.ts +14 -0
  205. package/dist/workflow-builder/internal/trigger-sentence.js +50 -0
  206. package/dist/workflow-builder/nodes/action-node.d.ts +3 -0
  207. package/dist/workflow-builder/nodes/action-node.js +7 -0
  208. package/dist/workflow-builder/nodes/base-node.d.ts +20 -0
  209. package/dist/workflow-builder/nodes/base-node.js +18 -0
  210. package/dist/workflow-builder/nodes/condition-node.d.ts +3 -0
  211. package/dist/workflow-builder/nodes/condition-node.js +11 -0
  212. package/dist/workflow-builder/nodes/delay-node.d.ts +3 -0
  213. package/dist/workflow-builder/nodes/delay-node.js +7 -0
  214. package/dist/workflow-builder/nodes/end-node.d.ts +3 -0
  215. package/dist/workflow-builder/nodes/end-node.js +7 -0
  216. package/dist/workflow-builder/nodes/index.d.ts +13 -0
  217. package/dist/workflow-builder/nodes/index.js +22 -0
  218. package/dist/workflow-builder/nodes/trigger-node.d.ts +3 -0
  219. package/dist/workflow-builder/nodes/trigger-node.js +8 -0
  220. package/dist/workflow-builder/public.d.ts +1 -0
  221. package/dist/workflow-builder/public.js +4 -0
  222. package/dist/workflow-builder/types.d.ts +140 -0
  223. package/dist/workflow-builder/types.js +18 -0
  224. package/dist/workflow-builder/workflow-builder.d.ts +10 -0
  225. package/dist/workflow-builder/workflow-builder.js +22 -0
  226. package/dist/workflow-builder/workflow-canvas.d.ts +26 -0
  227. package/dist/workflow-builder/workflow-canvas.js +29 -0
  228. package/dist/workflow-builder/workflow-card-compact.d.ts +2 -0
  229. package/dist/workflow-builder/workflow-card-compact.js +22 -0
  230. package/dist/workflow-builder/workflow-card.d.ts +2 -0
  231. package/dist/workflow-builder/workflow-card.js +44 -0
  232. package/dist/workflow-builder/workflow-header.d.ts +2 -0
  233. package/dist/workflow-builder/workflow-header.js +32 -0
  234. package/dist/workflow-builder/workflow-toolbox.d.ts +2 -0
  235. package/dist/workflow-builder/workflow-toolbox.js +19 -0
  236. package/package.json +72 -50
  237. package/dist/styles/theme.test.ts +0 -41
@@ -0,0 +1,140 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Globe, MessageSquare, MoreHorizontal, Repeat2, Send, ThumbsUp } from 'lucide-react';
4
+ import { useCallback, useRef, useState } from 'react';
5
+ import { cn } from '../../lib/utils.js';
6
+ import { Avatar } from '../internal/avatar.js';
7
+ import { Textarea } from '../internal/textarea.js';
8
+ // skipcq: SCT-1000 — `LinkedInLabels` is a TypeScript label-strings interface,
9
+ // not a client credential. DeepSource's secrets regex misclassifies the name.
10
+ export const DEFAULT_LINKEDIN_LABELS = {
11
+ placeholder: 'Share your thoughts...',
12
+ now: 'now',
13
+ moreOptions: 'More options',
14
+ comments: (count) => `${count} comments`,
15
+ reposts: (count) => `${count} reposts`,
16
+ like: 'Like',
17
+ comment: 'Comment',
18
+ repost: 'Repost',
19
+ send: 'Send',
20
+ };
21
+ function useLinkedInMentionTrigger(textareaRef, value, onChange, mentions, onMentionsChange) {
22
+ const [isOpen, setIsOpen] = useState(false);
23
+ const [query, setQuery] = useState('');
24
+ const triggerStartRef = useRef(null);
25
+ const close = useCallback(() => {
26
+ setIsOpen(false);
27
+ setQuery('');
28
+ triggerStartRef.current = null;
29
+ }, []);
30
+ const handleInput = useCallback(() => {
31
+ const ta = textareaRef.current;
32
+ /* v8 ignore next — ref is always attached when the textarea event handler fires */
33
+ if (!ta)
34
+ return;
35
+ /* v8 ignore next — selectionStart is always a number in a real browser/jsdom */
36
+ const cursor = ta.selectionStart ?? 0;
37
+ const text = ta.value;
38
+ let i = cursor - 1;
39
+ let foundAt = -1;
40
+ while (i >= 0) {
41
+ const ch = text[i];
42
+ if (ch === '@') {
43
+ if (i === 0 || /\s/.test(text[i - 1]))
44
+ foundAt = i;
45
+ break;
46
+ }
47
+ if (/\s/.test(ch))
48
+ break;
49
+ i -= 1;
50
+ }
51
+ if (foundAt === -1) {
52
+ if (isOpen)
53
+ close();
54
+ return;
55
+ }
56
+ triggerStartRef.current = foundAt;
57
+ setQuery(text.slice(foundAt + 1, cursor));
58
+ setIsOpen(true);
59
+ }, [textareaRef, isOpen, close]);
60
+ const insertMention = useCallback((picked) => {
61
+ const ta = textareaRef.current;
62
+ /* v8 ignore next — ref is always attached when insertMention is callable */
63
+ if (!ta)
64
+ return;
65
+ const start = triggerStartRef.current;
66
+ /* v8 ignore next — triggerStartRef is set before insertMention becomes accessible */
67
+ if (start === null)
68
+ return;
69
+ /* v8 ignore next — selectionStart is always a number in a real browser/jsdom */
70
+ const cursor = ta.selectionStart ?? 0;
71
+ const before = value.slice(0, start);
72
+ const after = value.slice(cursor);
73
+ const next = `${before}${picked.name} ${after}`;
74
+ onChange?.(next);
75
+ const span = {
76
+ ...picked,
77
+ offset: start,
78
+ length: picked.name.length,
79
+ };
80
+ onMentionsChange?.([...mentions, span]);
81
+ close();
82
+ }, [textareaRef, value, onChange, mentions, onMentionsChange, close]);
83
+ return { isOpen, query, close, insertMention, handleInput };
84
+ }
85
+ function MentionedContent({ text, mentions }) {
86
+ if (!mentions || mentions.length === 0) {
87
+ return _jsx("span", { className: "whitespace-pre-wrap break-words text-[14px]", children: text });
88
+ }
89
+ // Sort spans by offset and render alternating plain/highlight segments.
90
+ const sorted = [...mentions].sort((a, b) => a.offset - b.offset);
91
+ const segments = [];
92
+ let cursor = 0;
93
+ sorted.forEach((mention, idx) => {
94
+ if (mention.offset > cursor) {
95
+ segments.push({
96
+ key: `t-${cursor}`,
97
+ isMention: false,
98
+ value: text.slice(cursor, mention.offset),
99
+ });
100
+ }
101
+ const end = mention.offset + mention.length;
102
+ segments.push({
103
+ key: `m-${idx}-${mention.urn}`,
104
+ isMention: true,
105
+ value: text.slice(mention.offset, end),
106
+ });
107
+ cursor = end;
108
+ });
109
+ if (cursor < text.length) {
110
+ segments.push({ key: `t-${cursor}`, isMention: false, value: text.slice(cursor) });
111
+ }
112
+ return (_jsx("span", { className: "whitespace-pre-wrap break-words text-[14px]", children: segments.map((seg) => seg.isMention ? (_jsx("span", { "data-slot": "linkedin-mention", className: "font-semibold text-[#0A66C2]", children: seg.value }, seg.key)) : (_jsx("span", { children: seg.value }, seg.key))) }));
113
+ }
114
+ /**
115
+ * LinkedIn feed-card preview. Renders avatar, name, headline (`username`),
116
+ * content with optional `@mention` highlight spans, media grid, and the
117
+ * standard Like/Comment/Repost/Send footer.
118
+ */
119
+ /* v8 ignore next 14 — v8 phantom duplicate function record */
120
+ export function LinkedInPreview({ displayName, username, avatarUrl, content, mediaUrls, editable = false, onContentChange, className, labels, mentions = [], onMentionsChange, renderMentionPicker, }) {
121
+ const l = { ...DEFAULT_LINKEDIN_LABELS, ...labels };
122
+ const textareaRef = useRef(null);
123
+ const mention = useLinkedInMentionTrigger(textareaRef, content, onContentChange, mentions, onMentionsChange);
124
+ /* v8 ignore next — v8 phantom duplicate function body record */
125
+ return (_jsxs("div", { "data-slot": "post-preview", "data-platform": "linkedin", className: cn('@container bg-white text-gray-900 @[420px]:text-[15px] dark:bg-[#1B1F23] dark:text-[#E7E9EA]', className), children: [_jsxs("div", { className: "p-4 pb-0", children: [_jsxs("div", { className: "flex gap-3", children: [_jsx(Avatar, { src: avatarUrl, displayName: displayName, className: "h-12 w-12 flex-shrink-0" }), _jsx("div", { className: "min-w-0 flex-1", children: _jsxs("div", { className: "flex items-start justify-between", children: [_jsxs("div", { children: [_jsx("p", { className: "font-semibold text-[14px] text-gray-900", children: displayName }), _jsx("p", { className: "line-clamp-1 text-[12px] text-gray-500", children: username }), _jsxs("div", { className: "flex items-center gap-1 text-[12px] text-gray-500", children: [_jsx("span", { children: l.now }), _jsx("span", { children: "\u00B7" }), _jsx(Globe, { className: "h-3 w-3", "aria-hidden": "true" })] })] }), _jsx("button", { type: "button", "aria-label": l.moreOptions, className: "-m-1.5 inline-flex min-h-11 min-w-11 items-center justify-center rounded-full p-1.5 text-gray-500 hover:bg-gray-100", children: _jsx(MoreHorizontal, { className: "h-5 w-5" }) })] }) })] }), editable ? (_jsxs("div", { className: "relative", children: [_jsx(Textarea, { ref: textareaRef, value: content, onChange: (e) => onContentChange?.(e.target.value), onInput: mention.handleInput, onKeyUp: mention.handleInput, placeholder: l.placeholder, className: cn('mt-3 min-h-[80px] text-[14px] text-gray-900 placeholder:text-gray-500') }), renderMentionPicker && mention.isOpen
126
+ ? renderMentionPicker({
127
+ query: mention.query,
128
+ onPick: mention.insertMention,
129
+ onCancel: mention.close,
130
+ })
131
+ : null] })) : (_jsx("p", { className: "mt-3", children: _jsx(MentionedContent, { text: content, mentions: mentions }) }))] }), mediaUrls && mediaUrls.length > 0 && (_jsx("div", { className: "mt-3", children: mediaUrls.length === 1 ? (_jsx("div", { className: "aspect-video bg-gray-100", children: _jsx("img", { src: mediaUrls[0], alt: "Post media", loading: "lazy", decoding: "async", className: "h-full w-full object-cover" }) })) : (_jsx(LinkedInMultiImageGrid, { mediaUrls: mediaUrls })) })), _jsxs("div", { className: "flex items-center justify-between border-gray-200 border-b px-4 py-2 text-[12px] text-gray-500", children: [_jsxs("div", { className: "flex items-center gap-1", children: [_jsxs("div", { className: "flex -space-x-1", children: [_jsx("span", { role: "img", "aria-label": "Like", className: "inline-flex h-4 w-4 items-center justify-center rounded-full bg-blue-600 text-[10px] text-white", children: _jsx(ThumbsUp, { className: "h-2.5 w-2.5", fill: "currentColor" }) }), _jsx("span", { role: "img", "aria-label": "Love", className: "inline-flex h-4 w-4 items-center justify-center rounded-full bg-red-500 text-[10px] text-white", children: "\u2665" })] }), _jsx("span", { className: "ml-1", children: "0" })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { children: l.comments(0) }), _jsx("span", { children: "\u00B7" }), _jsx("span", { children: l.reposts(0) })] })] }), _jsxs("div", { className: "flex items-center justify-around px-2 py-1", children: [_jsxs("button", { type: "button", className: "flex min-h-11 flex-1 items-center justify-center gap-2 rounded-lg py-3 text-gray-600 hover:bg-gray-100", children: [_jsx(ThumbsUp, { className: "h-5 w-5" }), _jsx("span", { className: "font-medium text-[14px]", children: l.like })] }), _jsxs("button", { type: "button", className: "flex min-h-11 flex-1 items-center justify-center gap-2 rounded-lg py-3 text-gray-600 hover:bg-gray-100", children: [_jsx(MessageSquare, { className: "h-5 w-5" }), _jsx("span", { className: "font-medium text-[14px]", children: l.comment })] }), _jsxs("button", { type: "button", className: "flex min-h-11 flex-1 items-center justify-center gap-2 rounded-lg py-3 text-gray-600 hover:bg-gray-100", children: [_jsx(Repeat2, { className: "h-5 w-5" }), _jsx("span", { className: "font-medium text-[14px]", children: l.repost })] }), _jsxs("button", { type: "button", className: "flex min-h-11 flex-1 items-center justify-center gap-2 rounded-lg py-3 text-gray-600 hover:bg-gray-100", children: [_jsx(Send, { className: "h-5 w-5" }), _jsx("span", { className: "font-medium text-[14px]", children: l.send })] })] })] }));
132
+ }
133
+ /* v8 ignore next — v8 phantom duplicate function record */
134
+ function LinkedInMultiImageGrid({ mediaUrls }) {
135
+ /* v8 ignore next 2 — v8 phantom duplicate if-statement and its consequent */
136
+ if (mediaUrls.length === 3) {
137
+ return (_jsxs("div", { className: "grid gap-0.5", children: [_jsx("div", { className: "aspect-video bg-gray-100", children: _jsx("img", { src: mediaUrls[0], alt: "Media 1", loading: "lazy", decoding: "async", className: "h-full w-full object-cover" }) }), _jsx("div", { className: "grid grid-cols-2 gap-0.5", children: mediaUrls.slice(1, 3).map((url, i) => (_jsx("div", { className: "relative aspect-square bg-gray-100", children: _jsx("img", { src: url, alt: `Media ${i + 2}`, loading: "lazy", decoding: "async", className: "h-full w-full object-cover" }) }, url))) })] }));
138
+ }
139
+ return (_jsx("div", { className: "grid grid-cols-2 gap-0.5", children: mediaUrls.slice(0, 4).map((url, i) => (_jsxs("div", { className: "relative aspect-square bg-gray-100", children: [_jsx("img", { src: url, alt: `Media ${i + 1}`, loading: "lazy", decoding: "async", className: "h-full w-full object-cover" }), i === 3 && mediaUrls.length > 4 && (_jsx("div", { className: "absolute inset-0 flex items-center justify-center bg-black/60", children: _jsxs("span", { className: "font-semibold text-2xl text-white", children: ["+", mediaUrls.length - 4] }) }))] }, url))) }));
140
+ }
@@ -0,0 +1,46 @@
1
+ import type { PostPreviewBaseProps } from '../types.js';
2
+ /**
3
+ * Telegram poll attached to a channel post.
4
+ *
5
+ * Field names mirror Telegram's bot API wire shape so callers can pass
6
+ * the same value to `sendPoll`.
7
+ */
8
+ export interface TelegramPollState {
9
+ question: string;
10
+ options: string[];
11
+ multiple?: boolean;
12
+ quiz?: boolean;
13
+ /** Required when `quiz` is true; index of the correct option. */
14
+ correctOptionId?: number;
15
+ }
16
+ export interface TelegramInlineKeyboardButton {
17
+ text: string;
18
+ url?: string;
19
+ callbackData?: string;
20
+ }
21
+ /**
22
+ * Inline keyboard markup. Field name `inline_keyboard` matches the
23
+ * Telegram bot API so consumers can serialise this object directly.
24
+ */
25
+ export interface TelegramInlineKeyboardMarkup {
26
+ inline_keyboard: TelegramInlineKeyboardButton[][];
27
+ }
28
+ export interface TelegramLabels {
29
+ placeholder: string;
30
+ pinnedMessage: string;
31
+ share: string;
32
+ buttonFallback: string;
33
+ pollOptionPlaceholder: (number: number) => string;
34
+ }
35
+ export declare const DEFAULT_TELEGRAM_LABELS: TelegramLabels;
36
+ export interface TelegramPreviewProps extends PostPreviewBaseProps {
37
+ labels?: Partial<TelegramLabels>;
38
+ poll?: TelegramPollState;
39
+ pinMessage?: boolean;
40
+ replyMarkup?: TelegramInlineKeyboardMarkup;
41
+ }
42
+ /**
43
+ * Telegram channel post preview. Renders a chat bubble with optional
44
+ * poll, pinned indicator, and inline keyboard.
45
+ */
46
+ export declare function TelegramPreview({ displayName, username: _username, avatarUrl, content, mediaUrls, editable, onContentChange, className, labels, poll, pinMessage, replyMarkup, }: TelegramPreviewProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,44 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Eye, Pin, Share2 } from 'lucide-react';
4
+ import { cn } from '../../lib/utils.js';
5
+ import { Avatar } from '../internal/avatar.js';
6
+ import { Textarea } from '../internal/textarea.js';
7
+ export const DEFAULT_TELEGRAM_LABELS = {
8
+ placeholder: 'Type a message...',
9
+ pinnedMessage: 'Pinned message',
10
+ share: 'Share',
11
+ buttonFallback: 'Button',
12
+ pollOptionPlaceholder: (number) => `Option ${number}`,
13
+ };
14
+ /**
15
+ * Telegram channel post preview. Renders a chat bubble with optional
16
+ * poll, pinned indicator, and inline keyboard.
17
+ */
18
+ export function TelegramPreview({ displayName, username: _username, avatarUrl, content, mediaUrls, editable = false, onContentChange, className, labels, poll, pinMessage, replyMarkup, }) {
19
+ const l = { ...DEFAULT_TELEGRAM_LABELS, ...labels };
20
+ return (_jsx("div", { "data-slot": "post-preview", "data-platform": "telegram", className: cn('@container min-h-[200px] bg-[#E7EBF0] p-4 text-gray-900 @[420px]:p-5', className), children: _jsxs("div", { className: "max-w-md", children: [_jsxs("div", { className: "mb-2 flex items-center gap-2", children: [_jsx(Avatar, { src: avatarUrl, displayName: displayName, className: "h-8 w-8" }), _jsx("span", { className: "font-medium text-[14px] text-[#168ACD]", children: displayName })] }), _jsxs("div", { className: "overflow-hidden rounded-xl rounded-tl-sm bg-white shadow-sm", children: [pinMessage ? (_jsxs("div", { "data-slot": "telegram-pinned", className: "flex items-center gap-1 border-gray-200 border-b px-3 py-2 text-[11px] text-gray-500", children: [_jsx(Pin, { className: "h-3 w-3", "aria-hidden": "true" }), _jsx("span", { children: l.pinnedMessage })] })) : null, mediaUrls && mediaUrls.length > 0 && (_jsx("div", { className: cn(mediaUrls.length === 1 && 'aspect-video', mediaUrls.length >= 2 && 'grid grid-cols-2 gap-0.5'), children: mediaUrls.slice(0, 4).map((url, i) => (_jsx("div", { className: cn('bg-gray-100', mediaUrls.length === 1 && 'aspect-video', mediaUrls.length >= 2 && 'aspect-square'), children: _jsx("img", { src: url, alt: `Media ${i + 1}`, loading: "lazy", decoding: "async", className: "h-full w-full object-cover" }) }, url))) })), _jsxs("div", { className: "p-3", children: [renderBody({ poll, editable, content, onContentChange, labels: l }), replyMarkup?.inline_keyboard?.length ? (_jsx("div", { className: "mt-3 space-y-1", children: replyMarkup.inline_keyboard.map((row) => {
21
+ const rowKey = row
22
+ .map((b) => `${b.text}|${b.url ?? b.callbackData ?? ''}`)
23
+ .join('::');
24
+ return (_jsx("div", { className: "flex gap-1", children: row.map((button) => (_jsx("a", { href: button.url ?? '#', target: "_blank", rel: "noreferrer", className: "flex-1 rounded bg-[#168ACD] px-3 py-1.5 text-center text-white text-xs", children: button.text || l.buttonFallback }, `${button.text}-${button.url ?? button.callbackData ?? ''}`))) }, rowKey));
25
+ }) })) : null, _jsxs("div", { className: "mt-2 flex items-center justify-end gap-1 text-[12px] text-gray-500", children: [_jsx(Eye, { className: "h-3 w-3", "aria-hidden": "true" }), _jsx("span", { children: "0" }), _jsx("span", { className: "ml-2", children: "12:00" })] })] })] }), _jsxs("div", { className: "mt-2 flex items-center gap-2", children: [_jsxs("button", { type: "button", className: "inline-flex min-h-11 items-center gap-1 rounded-full bg-white px-3 py-1 text-[13px] shadow-sm", children: [_jsx("span", { "aria-hidden": "true", children: "\uD83D\uDC4D" }), _jsx("span", { className: "text-gray-500", children: "0" })] }), _jsxs("button", { type: "button", className: "inline-flex min-h-11 items-center gap-1 rounded-full bg-white px-3 py-1 text-[13px] shadow-sm", children: [_jsx("span", { "aria-hidden": "true", children: "\u2764\uFE0F" }), _jsx("span", { className: "text-gray-500", children: "0" })] }), _jsx("button", { type: "button", "aria-label": l.share, className: "inline-flex min-h-11 min-w-11 items-center justify-center rounded-full bg-white p-1.5 shadow-sm", children: _jsx(Share2, { className: "h-4 w-4 text-gray-500" }) })] })] }) }));
26
+ }
27
+ /* v8 ignore next 13 — v8 phantom duplicate function record */
28
+ function renderBody({ poll, editable, content, onContentChange, labels, }) {
29
+ /* v8 ignore next 2 — v8 phantom duplicate if-statement and its consequent */
30
+ if (poll) {
31
+ return (_jsxs("div", { "data-slot": "telegram-poll", className: "space-y-2", children: [_jsx("p", { className: "font-medium", children: poll.question }), poll.options.map((option, index) => {
32
+ // Options can repeat / be empty during composing; combine with stable index in a non-positional way
33
+ // by counting prior empties so the key remains unique without leaking positional intent.
34
+ const key = option
35
+ ? `opt-${option}`
36
+ : `placeholder-${labels.pollOptionPlaceholder(index + 1)}`;
37
+ return (_jsx("div", { className: "rounded bg-gray-50 px-3 py-2 text-sm", children: option || labels.pollOptionPlaceholder(index + 1) }, key));
38
+ })] }));
39
+ }
40
+ if (editable) {
41
+ return (_jsx(Textarea, { value: content, onChange: (e) => onContentChange?.(e.target.value), placeholder: labels.placeholder, className: "min-h-[60px] text-[15px] placeholder:text-gray-500" }));
42
+ }
43
+ return _jsx("p", { className: "whitespace-pre-wrap break-words text-[15px]", children: content });
44
+ }
@@ -0,0 +1,19 @@
1
+ import type { PostPreviewBaseProps } from '../types.js';
2
+ export interface ThreadsLabels {
3
+ placeholder: string;
4
+ replies: (count: number) => string;
5
+ like: string;
6
+ comment: string;
7
+ repost: string;
8
+ share: string;
9
+ moreOptions: string;
10
+ }
11
+ export declare const DEFAULT_THREADS_LABELS: ThreadsLabels;
12
+ export interface ThreadsPreviewProps extends PostPreviewBaseProps {
13
+ labels?: Partial<ThreadsLabels>;
14
+ }
15
+ /**
16
+ * Threads feed-card preview. Honors a parent `.dark` class for the dark
17
+ * variant the platform itself uses.
18
+ */
19
+ export declare function ThreadsPreview({ displayName, username, avatarUrl, content, mediaUrls, editable, onContentChange, className, labels, }: ThreadsPreviewProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,26 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Heart, MessageCircle, MoreHorizontal, Repeat2, Send } from 'lucide-react';
4
+ import { cn } from '../../lib/utils.js';
5
+ import { Avatar } from '../internal/avatar.js';
6
+ import { Textarea } from '../internal/textarea.js';
7
+ export const DEFAULT_THREADS_LABELS = {
8
+ placeholder: 'Start a thread...',
9
+ replies: (count) => `${count} replies`,
10
+ like: 'Like',
11
+ comment: 'Comment',
12
+ repost: 'Repost',
13
+ share: 'Share',
14
+ moreOptions: 'More options',
15
+ };
16
+ /**
17
+ * Threads feed-card preview. Honors a parent `.dark` class for the dark
18
+ * variant the platform itself uses.
19
+ */
20
+ /* v8 ignore next 11 — v8 phantom duplicate function record */
21
+ export function ThreadsPreview({ displayName, username, avatarUrl, content, mediaUrls, editable = false, onContentChange, className, labels, }) {
22
+ const l = { ...DEFAULT_THREADS_LABELS, ...labels };
23
+ const formattedUsername = username.startsWith('@') ? username.slice(1) : username;
24
+ /* v8 ignore next — v8 phantom duplicate function body record */
25
+ return (_jsxs("div", { "data-slot": "post-preview", "data-platform": "threads", className: cn('@container bg-white p-4 text-gray-900 @[420px]:text-[15px] dark:bg-[#101010] dark:text-white', className), children: [_jsxs("div", { className: "flex gap-3", children: [_jsxs("div", { className: "flex flex-col items-center", children: [_jsx(Avatar, { src: avatarUrl, displayName: displayName, className: "h-9 w-9" }), _jsx("div", { className: "mt-2 min-h-[20px] w-0.5 flex-1 bg-gray-200 dark:bg-gray-700" })] }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { className: "flex items-center gap-1", children: [_jsx("span", { className: "font-semibold text-[15px]", children: formattedUsername }), _jsx("span", { className: "text-[15px] text-gray-500", children: "\u00B7 now" })] }), _jsx("button", { type: "button", "aria-label": l.moreOptions, className: "-m-1.5 inline-flex min-h-11 min-w-11 items-center justify-center rounded-full p-1.5 text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800", children: _jsx(MoreHorizontal, { className: "h-4 w-4" }) })] }), editable ? (_jsx(Textarea, { value: content, onChange: (e) => onContentChange?.(e.target.value), placeholder: l.placeholder, className: "mt-1 min-h-[60px] text-[15px] placeholder:text-gray-500" })) : (_jsx("p", { className: "mt-1 whitespace-pre-wrap break-words text-[15px]", children: content })), mediaUrls && mediaUrls.length > 0 && (_jsx("div", { className: cn('mt-3 overflow-hidden rounded-xl', mediaUrls.length === 1 && 'aspect-video', mediaUrls.length === 2 && 'grid grid-cols-2 gap-1', mediaUrls.length >= 3 && 'grid grid-cols-2 gap-1'), children: mediaUrls.slice(0, 4).map((url, i) => (_jsx("div", { className: cn('bg-gray-100 dark:bg-gray-800', mediaUrls.length === 1 && 'aspect-video', mediaUrls.length >= 2 && 'aspect-square'), children: _jsx("img", { src: url, alt: `Media ${i + 1}`, loading: "lazy", decoding: "async", className: "h-full w-full rounded-lg object-cover" }) }, url))) })), _jsxs("div", { className: "mt-3 flex items-center gap-4 text-gray-500", children: [_jsx("button", { type: "button", "aria-label": l.like, className: "-m-2 inline-flex min-h-11 min-w-11 items-center justify-center rounded-full p-2 hover:bg-gray-100 dark:hover:bg-gray-800", children: _jsx(Heart, { className: "h-5 w-5" }) }), _jsx("button", { type: "button", "aria-label": l.comment, className: "-m-2 inline-flex min-h-11 min-w-11 items-center justify-center rounded-full p-2 hover:bg-gray-100 dark:hover:bg-gray-800", children: _jsx(MessageCircle, { className: "h-5 w-5" }) }), _jsx("button", { type: "button", "aria-label": l.repost, className: "-m-2 inline-flex min-h-11 min-w-11 items-center justify-center rounded-full p-2 hover:bg-gray-100 dark:hover:bg-gray-800", children: _jsx(Repeat2, { className: "h-5 w-5" }) }), _jsx("button", { type: "button", "aria-label": l.share, className: "-m-2 inline-flex min-h-11 min-w-11 items-center justify-center rounded-full p-2 hover:bg-gray-100 dark:hover:bg-gray-800", children: _jsx(Send, { className: "h-5 w-5" }) })] })] })] }), _jsxs("div", { className: "mt-3 ml-12 flex items-center gap-3", children: [_jsxs("div", { className: "flex -space-x-1.5", children: [_jsx("div", { className: "h-5 w-5 rounded-full border-2 border-white bg-gray-300 dark:border-[#101010] dark:bg-gray-600" }), _jsx("div", { className: "h-5 w-5 rounded-full border-2 border-white bg-gray-400 dark:border-[#101010] dark:bg-gray-500" })] }), _jsx("span", { className: "text-[15px] text-gray-500", children: l.replies(0) })] })] }));
26
+ }
@@ -0,0 +1,18 @@
1
+ import type { PostPreviewBaseProps } from '../types.js';
2
+ export interface TikTokLabels {
3
+ placeholder: string;
4
+ share: string;
5
+ originalSound: (name: string) => string;
6
+ like: string;
7
+ comment: string;
8
+ bookmark: string;
9
+ }
10
+ export declare const DEFAULT_TIKTOK_LABELS: TikTokLabels;
11
+ export interface TikTokPreviewProps extends PostPreviewBaseProps {
12
+ labels?: Partial<TikTokLabels>;
13
+ }
14
+ /**
15
+ * TikTok video-overlay preview. Always renders against a dark background to
16
+ * match the platform's video-first UI.
17
+ */
18
+ export declare function TikTokPreview({ displayName, username, avatarUrl, content, editable, onContentChange, className, labels, }: TikTokPreviewProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,25 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Bookmark, Heart, MessageCircle, Share2 } from 'lucide-react';
4
+ import { cn } from '../../lib/utils.js';
5
+ import { Avatar } from '../internal/avatar.js';
6
+ import { Textarea } from '../internal/textarea.js';
7
+ export const DEFAULT_TIKTOK_LABELS = {
8
+ placeholder: 'Add a caption...',
9
+ share: 'Share',
10
+ originalSound: (name) => `Original sound - ${name}`,
11
+ like: 'Like',
12
+ comment: 'Comment',
13
+ bookmark: 'Bookmark',
14
+ };
15
+ /**
16
+ * TikTok video-overlay preview. Always renders against a dark background to
17
+ * match the platform's video-first UI.
18
+ */
19
+ /* v8 ignore next 10 — v8 phantom duplicate function record */
20
+ export function TikTokPreview({ displayName, username, avatarUrl, content, editable = false, onContentChange, className, labels, }) {
21
+ const l = { ...DEFAULT_TIKTOK_LABELS, ...labels };
22
+ const formattedUsername = username.startsWith('@') ? username : `@${username}`;
23
+ /* v8 ignore next — v8 phantom duplicate function body record */
24
+ return (_jsxs("div", { "data-slot": "post-preview", "data-platform": "tiktok", className: cn('@container relative min-h-[300px] bg-black text-white @[420px]:min-h-[420px]', className), children: [_jsx("div", { className: "absolute inset-0 bg-gradient-to-t from-black/80 via-transparent to-transparent" }), _jsxs("div", { className: "absolute right-3 bottom-20 flex flex-col items-center gap-4", children: [_jsxs("div", { className: "flex flex-col items-center gap-1", children: [_jsx(Avatar, { src: avatarUrl, displayName: displayName, className: "h-11 w-11 ring-2 ring-white" }), _jsx("div", { className: "-mt-2.5 flex h-5 w-5 items-center justify-center rounded-full bg-[#FE2C55]", children: _jsx("span", { className: "font-bold text-white text-xs", "aria-hidden": "true", children: "+" }) })] }), _jsxs("button", { type: "button", "aria-label": l.like, className: "inline-flex min-h-11 min-w-11 flex-col items-center gap-1", children: [_jsx("div", { className: "rounded-full bg-gray-800/50 p-2", children: _jsx(Heart, { className: "h-7 w-7", fill: "white" }) }), _jsx("span", { className: "font-semibold text-xs", children: "0" })] }), _jsxs("button", { type: "button", "aria-label": l.comment, className: "inline-flex min-h-11 min-w-11 flex-col items-center gap-1", children: [_jsx("div", { className: "rounded-full bg-gray-800/50 p-2", children: _jsx(MessageCircle, { className: "h-7 w-7", fill: "white" }) }), _jsx("span", { className: "font-semibold text-xs", children: "0" })] }), _jsxs("button", { type: "button", "aria-label": l.bookmark, className: "inline-flex min-h-11 min-w-11 flex-col items-center gap-1", children: [_jsx("div", { className: "rounded-full bg-gray-800/50 p-2", children: _jsx(Bookmark, { className: "h-7 w-7" }) }), _jsx("span", { className: "font-semibold text-xs", children: "0" })] }), _jsxs("button", { type: "button", "aria-label": l.share, className: "inline-flex min-h-11 min-w-11 flex-col items-center gap-1", children: [_jsx("div", { className: "rounded-full bg-gray-800/50 p-2", children: _jsx(Share2, { className: "h-7 w-7" }) }), _jsx("span", { className: "font-semibold text-xs", children: l.share })] }), _jsx("div", { className: "mt-2 flex h-11 w-11 animate-spin items-center justify-center rounded-full border-4 border-gray-700 bg-gray-800", children: _jsx("div", { className: "h-4 w-4 rounded-full bg-gray-600" }) })] }), _jsxs("div", { className: "absolute right-16 bottom-0 left-0 p-4", children: [_jsx("p", { className: "mb-1 font-semibold text-[16px]", children: formattedUsername }), editable ? (_jsx(Textarea, { value: content, onChange: (e) => onContentChange?.(e.target.value), placeholder: l.placeholder, className: "min-h-[40px] text-[14px] text-white placeholder:text-gray-400" })) : (_jsx("p", { className: "whitespace-pre-wrap break-words text-[14px]", children: content })), _jsxs("div", { className: "mt-3 flex items-center gap-2", children: [_jsx("svg", { className: "h-3 w-3", viewBox: "0 0 24 24", fill: "currentColor", "aria-hidden": "true", children: _jsx("path", { d: "M12 3v10.55c-.59-.34-1.27-.55-2-.55-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4V7h4V3h-6z" }) }), _jsx("span", { className: "text-[13px]", children: l.originalSound(displayName) })] })] })] }));
25
+ }
@@ -0,0 +1,40 @@
1
+ import { type ReactNode } from 'react';
2
+ import type { PostPreviewBaseProps } from '../types.js';
3
+ /**
4
+ * Visual theme variants for Twitter / X.
5
+ * - `light`: white background, dark text.
6
+ * - `dim`: navy-blue dark variant.
7
+ * - `lights-out`: pure black variant.
8
+ */
9
+ export type TwitterTheme = 'light' | 'dim' | 'lights-out';
10
+ export interface TwitterMentionPickerContext {
11
+ query: string;
12
+ onPick: (mention: string) => void;
13
+ onCancel: () => void;
14
+ }
15
+ export interface TwitterLabels {
16
+ placeholder: string;
17
+ now: string;
18
+ moreOptions: string;
19
+ reply: string;
20
+ retweet: string;
21
+ like: string;
22
+ save: string;
23
+ share: string;
24
+ viewAnalytics: string;
25
+ }
26
+ export declare const DEFAULT_TWITTER_LABELS: TwitterLabels;
27
+ export interface TwitterPreviewProps extends PostPreviewBaseProps {
28
+ labels?: Partial<TwitterLabels>;
29
+ /** Visual theme. Defaults to `lights-out` to match X's default. */
30
+ theme?: TwitterTheme;
31
+ /** Optional render slot for an `@mention` picker. Called with the
32
+ * current query (text after `@`) when the user types `@xxx` in the
33
+ * editable textarea. */
34
+ renderMentionPicker?: (ctx: TwitterMentionPickerContext) => ReactNode;
35
+ }
36
+ /**
37
+ * Twitter / X feed-card preview. Supports light, dim, and lights-out
38
+ * themes plus an optional `@mention` picker render slot.
39
+ */
40
+ export declare function TwitterPreview({ displayName, username, avatarUrl, content, mediaUrls, characterLimit, editable, onContentChange, className, labels, theme, renderMentionPicker, }: TwitterPreviewProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,137 @@
1
+ 'use client';
2
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Bookmark, Heart, MessageCircle, MoreHorizontal, Repeat2, Share } from 'lucide-react';
4
+ import { useCallback, useRef, useState } from 'react';
5
+ import { cn } from '../../lib/utils.js';
6
+ import { Avatar } from '../internal/avatar.js';
7
+ import { Textarea } from '../internal/textarea.js';
8
+ export const DEFAULT_TWITTER_LABELS = {
9
+ placeholder: "What's happening?",
10
+ now: 'now',
11
+ moreOptions: 'More options',
12
+ reply: 'Reply',
13
+ retweet: 'Retweet',
14
+ like: 'Like',
15
+ save: 'Save',
16
+ share: 'Share',
17
+ viewAnalytics: 'View post analytics',
18
+ };
19
+ function useMentionTrigger(textareaRef, value, onChange) {
20
+ const [isOpen, setIsOpen] = useState(false);
21
+ const [query, setQuery] = useState('');
22
+ const triggerStartRef = useRef(null);
23
+ const close = useCallback(() => {
24
+ setIsOpen(false);
25
+ setQuery('');
26
+ triggerStartRef.current = null;
27
+ }, []);
28
+ const handleInput = useCallback(() => {
29
+ const ta = textareaRef.current;
30
+ /* v8 ignore next — ref is always attached when the textarea event handler fires */
31
+ if (!ta)
32
+ return;
33
+ /* v8 ignore next — selectionStart is always a number in a real browser/jsdom */
34
+ const cursor = ta.selectionStart ?? 0;
35
+ const text = ta.value;
36
+ // Walk back from cursor to find `@` until whitespace or start.
37
+ let i = cursor - 1;
38
+ let foundAt = -1;
39
+ while (i >= 0) {
40
+ const ch = text[i];
41
+ if (ch === '@') {
42
+ // Must be at start or preceded by whitespace
43
+ if (i === 0 || /\s/.test(text[i - 1])) {
44
+ foundAt = i;
45
+ }
46
+ break;
47
+ }
48
+ if (/\s/.test(ch))
49
+ break;
50
+ i -= 1;
51
+ }
52
+ if (foundAt === -1) {
53
+ if (isOpen)
54
+ close();
55
+ return;
56
+ }
57
+ triggerStartRef.current = foundAt;
58
+ setQuery(text.slice(foundAt + 1, cursor));
59
+ setIsOpen(true);
60
+ }, [textareaRef, isOpen, close]);
61
+ const insertMention = useCallback((mention) => {
62
+ const ta = textareaRef.current;
63
+ /* v8 ignore next — ref is always attached when insertMention is callable */
64
+ if (!ta)
65
+ return;
66
+ const start = triggerStartRef.current;
67
+ /* v8 ignore next — triggerStartRef is set before insertMention becomes accessible */
68
+ if (start === null)
69
+ return;
70
+ /* v8 ignore next — selectionStart is always a number in a real browser/jsdom */
71
+ const cursor = ta.selectionStart ?? 0;
72
+ const before = value.slice(0, start);
73
+ const after = value.slice(cursor);
74
+ const formatted = mention.startsWith('@') ? mention : `@${mention}`;
75
+ const next = `${before}${formatted} ${after}`;
76
+ onChange?.(next);
77
+ close();
78
+ }, [textareaRef, value, onChange, close]);
79
+ return { isOpen, query, close, insertMention, handleInput };
80
+ }
81
+ function ContentWithMentions({ text, accent }) {
82
+ // Build typed parts with stable offsets so keys don't rely on positional index.
83
+ const segments = [];
84
+ let offset = 0;
85
+ for (const part of text.split(/(@[A-Za-z0-9_]+)/g)) {
86
+ if (!part)
87
+ continue;
88
+ segments.push({
89
+ key: `${offset}-${part}`,
90
+ isMention: /^@[A-Za-z0-9_]+$/.test(part),
91
+ value: part,
92
+ });
93
+ offset += part.length;
94
+ }
95
+ return (_jsx(_Fragment, { children: segments.map((seg) => seg.isMention ? (_jsx("span", { className: accent, children: seg.value }, seg.key)) : (_jsx("span", { children: seg.value }, seg.key))) }));
96
+ }
97
+ const THEME_CLASSES = {
98
+ light: {
99
+ surface: 'bg-white',
100
+ text: 'text-gray-900',
101
+ muted: 'text-gray-500',
102
+ border: 'border-gray-200',
103
+ mention: 'text-blue-500',
104
+ },
105
+ dim: {
106
+ surface: 'bg-[#15202B]',
107
+ text: 'text-white',
108
+ muted: 'text-gray-400',
109
+ border: 'border-[#38444D]',
110
+ mention: 'text-[#1D9BF0]',
111
+ },
112
+ 'lights-out': {
113
+ surface: 'bg-black',
114
+ text: 'text-white',
115
+ muted: 'text-gray-500',
116
+ border: 'border-gray-800',
117
+ mention: 'text-blue-400',
118
+ },
119
+ };
120
+ /**
121
+ * Twitter / X feed-card preview. Supports light, dim, and lights-out
122
+ * themes plus an optional `@mention` picker render slot.
123
+ */
124
+ export function TwitterPreview({ displayName, username, avatarUrl, content, mediaUrls, characterLimit, editable = false, onContentChange, className, labels, theme = 'lights-out', renderMentionPicker, }) {
125
+ const l = { ...DEFAULT_TWITTER_LABELS, ...labels };
126
+ const t = THEME_CLASSES[theme];
127
+ const formattedUsername = username.startsWith('@') ? username : `@${username}`;
128
+ const textareaRef = useRef(null);
129
+ const mention = useMentionTrigger(textareaRef, content, onContentChange);
130
+ return (_jsx("div", { "data-slot": "post-preview", "data-platform": "twitter", "data-theme": theme, className: cn('@container p-4 @[420px]:p-5', t.surface, t.text, className), children: _jsxs("div", { className: "flex gap-3", children: [_jsx(Avatar, { src: avatarUrl, displayName: displayName, className: "h-10 w-10 flex-shrink-0" }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { className: "flex items-center gap-1 text-[15px]", children: [_jsx("span", { className: cn('truncate font-bold', t.text), children: displayName }), _jsx("span", { className: cn('truncate', t.muted), children: formattedUsername }), _jsx("span", { className: t.muted, children: "\u00B7" }), _jsx("span", { className: t.muted, children: l.now })] }), _jsx("button", { type: "button", "aria-label": l.moreOptions, className: cn('-m-1.5 inline-flex min-h-11 min-w-11 items-center justify-center rounded-full p-1.5', t.muted, theme === 'light' ? 'hover:bg-gray-100' : 'hover:bg-gray-800'), children: _jsx(MoreHorizontal, { className: "h-4 w-4" }) })] }), editable ? (_jsxs("div", { className: "relative", children: [_jsx(Textarea, { ref: textareaRef, value: content, onChange: (e) => onContentChange?.(e.target.value), onInput: mention.handleInput, onKeyUp: mention.handleInput, placeholder: l.placeholder, className: cn('mt-1 min-h-[60px] text-[15px]', t.text, theme === 'light' ? 'placeholder:text-gray-500' : 'placeholder:text-gray-500') }), renderMentionPicker && mention.isOpen
131
+ ? renderMentionPicker({
132
+ query: mention.query,
133
+ onPick: mention.insertMention,
134
+ onCancel: mention.close,
135
+ })
136
+ : null, characterLimit !== undefined && (_jsxs("div", { "data-slot": "twitter-character-counter", className: cn('mt-1 text-right text-xs', content.length > characterLimit ? 'text-red-500' : t.muted), children: [_jsx("span", { children: characterLimit - content.length }), _jsx("span", { className: "mx-1", children: "/" }), _jsx("span", { children: characterLimit })] }))] })) : (_jsx("p", { className: "mt-1 whitespace-pre-wrap break-words text-[15px]", children: _jsx(ContentWithMentions, { text: content, accent: t.mention }) })), mediaUrls && mediaUrls.length > 0 && (_jsx("div", { className: cn('mt-3 overflow-hidden rounded-2xl border', t.border, mediaUrls.length === 1 && 'aspect-video', mediaUrls.length >= 2 && 'grid aspect-video grid-cols-2 gap-0.5'), children: mediaUrls.slice(0, 4).map((url, i) => (_jsx("div", { className: cn('overflow-hidden bg-gray-800', mediaUrls.length === 1 && 'h-full', mediaUrls.length === 3 && i === 0 && 'row-span-2'), children: _jsx("img", { src: url, alt: `Media ${i + 1}`, loading: "lazy", decoding: "async", className: "h-full w-full object-cover" }) }, url))) })), _jsxs("div", { className: cn('mt-3 flex max-w-[425px] items-center justify-between @[420px]:max-w-[480px]', t.muted), children: [_jsxs("button", { type: "button", "aria-label": l.reply, className: "-m-2 inline-flex min-h-11 min-w-11 items-center gap-1 rounded-full p-2 hover:bg-blue-500/10 hover:text-blue-400", children: [_jsx(MessageCircle, { className: "h-[18px] w-[18px]" }), _jsx("span", { className: "text-[13px]", children: "0" })] }), _jsxs("button", { type: "button", "aria-label": l.retweet, className: "-m-2 inline-flex min-h-11 min-w-11 items-center gap-1 rounded-full p-2 hover:bg-green-500/10 hover:text-green-400", children: [_jsx(Repeat2, { className: "h-[18px] w-[18px]" }), _jsx("span", { className: "text-[13px]", children: "0" })] }), _jsxs("button", { type: "button", "aria-label": l.like, className: "-m-2 inline-flex min-h-11 min-w-11 items-center gap-1 rounded-full p-2 hover:bg-pink-500/10 hover:text-pink-400", children: [_jsx(Heart, { className: "h-[18px] w-[18px]" }), _jsx("span", { className: "text-[13px]", children: "0" })] }), _jsx("button", { type: "button", "aria-label": l.viewAnalytics, className: "-m-2 inline-flex min-h-11 min-w-11 items-center gap-1 rounded-full p-2 hover:bg-blue-500/10 hover:text-blue-400", children: _jsx("svg", { className: "h-[18px] w-[18px]", viewBox: "0 0 24 24", fill: "currentColor", "aria-hidden": "true", children: _jsx("path", { d: "M8.75 21V3h2v18h-2zM18 21V8.5h2V21h-2zM4 21l.004-10h2L6 21H4zm9.248 0v-7h2v7h-2z" }) }) }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsx("button", { type: "button", "aria-label": l.save, className: "-m-2 inline-flex min-h-11 min-w-11 items-center justify-center rounded-full p-2 hover:bg-blue-500/10 hover:text-blue-400", children: _jsx(Bookmark, { className: "h-[18px] w-[18px]" }) }), _jsx("button", { type: "button", "aria-label": l.share, className: "-m-2 inline-flex min-h-11 min-w-11 items-center justify-center rounded-full p-2 hover:bg-blue-500/10 hover:text-blue-400", children: _jsx(Share, { className: "h-[18px] w-[18px]" }) })] })] })] })] }) }));
137
+ }
@@ -0,0 +1,17 @@
1
+ import type { PostPreviewBaseProps } from '../types.js';
2
+ export interface YouTubeLabels {
3
+ placeholder: string;
4
+ justNow: string;
5
+ like: string;
6
+ dislike: string;
7
+ share: string;
8
+ moreOptions: string;
9
+ }
10
+ export declare const DEFAULT_YOUTUBE_LABELS: YouTubeLabels;
11
+ export interface YouTubePreviewProps extends PostPreviewBaseProps {
12
+ labels?: Partial<YouTubeLabels>;
13
+ }
14
+ /**
15
+ * YouTube community-post preview. Honors `.dark` for the dark variant.
16
+ */
17
+ export declare function YouTubePreview({ displayName, username: _username, avatarUrl, content, mediaUrls, editable, onContentChange, className, labels, }: YouTubePreviewProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,23 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { MoreVertical, ThumbsDown, ThumbsUp } from 'lucide-react';
4
+ import { cn } from '../../lib/utils.js';
5
+ import { Avatar } from '../internal/avatar.js';
6
+ import { Textarea } from '../internal/textarea.js';
7
+ export const DEFAULT_YOUTUBE_LABELS = {
8
+ placeholder: 'Create a post...',
9
+ justNow: 'just now',
10
+ like: 'Like',
11
+ dislike: 'Dislike',
12
+ share: 'Share',
13
+ moreOptions: 'More options',
14
+ };
15
+ /**
16
+ * YouTube community-post preview. Honors `.dark` for the dark variant.
17
+ */
18
+ /* v8 ignore next 11 — v8 phantom duplicate function record */
19
+ export function YouTubePreview({ displayName, username: _username, avatarUrl, content, mediaUrls, editable = false, onContentChange, className, labels, }) {
20
+ const l = { ...DEFAULT_YOUTUBE_LABELS, ...labels };
21
+ /* v8 ignore next — v8 phantom duplicate function body record */
22
+ return (_jsxs("div", { "data-slot": "post-preview", "data-platform": "youtube", className: cn('@container bg-white p-4 text-gray-900 @[420px]:text-[14px] dark:bg-[#0f0f0f] dark:text-white', className), children: [_jsxs("div", { className: "flex items-start justify-between", children: [_jsxs("div", { className: "flex gap-3", children: [_jsx(Avatar, { src: avatarUrl, displayName: displayName, className: "h-10 w-10" }), _jsx("div", { children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "font-medium text-[14px]", children: displayName }), _jsx("span", { className: "text-[12px] text-gray-500 dark:text-gray-400", children: l.justNow })] }) })] }), _jsx("button", { type: "button", "aria-label": l.moreOptions, className: "-m-2 inline-flex min-h-11 min-w-11 items-center justify-center rounded-full p-2 text-gray-600 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800", children: _jsx(MoreVertical, { className: "h-5 w-5" }) })] }), editable ? (_jsx(Textarea, { value: content, onChange: (e) => onContentChange?.(e.target.value), placeholder: l.placeholder, className: "mt-3 min-h-[60px] text-[14px] placeholder:text-gray-500" })) : (_jsx("p", { className: "mt-3 whitespace-pre-wrap break-words text-[14px]", children: content })), mediaUrls && mediaUrls.length > 0 && (_jsx("div", { className: "mt-3 overflow-hidden rounded-xl", children: _jsx("div", { className: "aspect-video bg-gray-100 dark:bg-gray-800", children: _jsx("img", { src: mediaUrls[0], alt: "Media 1", loading: "lazy", decoding: "async", className: "h-full w-full object-cover" }) }) })), _jsxs("div", { className: "mt-4 flex items-center gap-2", children: [_jsxs("button", { type: "button", className: "inline-flex min-h-11 items-center gap-1 rounded-full bg-gray-100 px-3 py-1.5 hover:bg-gray-200 dark:bg-gray-800 dark:hover:bg-gray-700", "aria-label": l.like, children: [_jsx(ThumbsUp, { className: "h-4 w-4" }), _jsx("span", { className: "font-medium text-[13px]", children: "0" })] }), _jsx("button", { type: "button", "aria-label": l.dislike, className: "inline-flex min-h-11 min-w-11 items-center justify-center rounded-full bg-gray-100 p-2 hover:bg-gray-200 dark:bg-gray-800 dark:hover:bg-gray-700", children: _jsx(ThumbsDown, { className: "h-4 w-4" }) }), _jsxs("button", { type: "button", "aria-label": l.share, className: "inline-flex min-h-11 items-center gap-1 rounded-full bg-gray-100 px-3 py-1.5 hover:bg-gray-200 dark:bg-gray-800 dark:hover:bg-gray-700", children: [_jsx("svg", { className: "h-4 w-4", viewBox: "0 0 24 24", fill: "currentColor", "aria-hidden": "true", children: _jsx("path", { d: "M21 6h-2v9H6v2c0 .55.45 1 1 1h11l4 4V7c0-.55-.45-1-1-1zm-4 6V3c0-.55-.45-1-1-1H3c-.55 0-1 .45-1 1v14l4-4h10c.55 0 1-.45 1-1z" }) }), _jsx("span", { className: "font-medium text-[13px]", children: "0" })] })] })] }));
23
+ }
@@ -0,0 +1,29 @@
1
+ import type * as React from 'react';
2
+ import type { BlueSkyPreviewProps, DiscordPreviewProps, FacebookPreviewProps, GenericPreviewProps, GoogleBusinessPreviewProps, InstagramPreviewProps, LinkedInPreviewProps, TelegramPreviewProps, ThreadsPreviewProps, TikTokPreviewProps, TwitterPreviewProps, YouTubePreviewProps } from './platforms/index.js';
3
+ import type { PostPreviewSlots } from './types.js';
4
+ export type PostPreviewProps = PostPreviewSlots & (({
5
+ platform: 'instagram';
6
+ } & InstagramPreviewProps) | ({
7
+ platform: 'twitter';
8
+ } & TwitterPreviewProps) | ({
9
+ platform: 'facebook';
10
+ } & FacebookPreviewProps) | ({
11
+ platform: 'linkedin';
12
+ } & LinkedInPreviewProps) | ({
13
+ platform: 'tiktok';
14
+ } & TikTokPreviewProps) | ({
15
+ platform: 'youtube';
16
+ } & YouTubePreviewProps) | ({
17
+ platform: 'threads';
18
+ } & ThreadsPreviewProps) | ({
19
+ platform: 'bluesky';
20
+ } & BlueSkyPreviewProps) | ({
21
+ platform: 'discord';
22
+ } & DiscordPreviewProps) | ({
23
+ platform: 'telegram';
24
+ } & TelegramPreviewProps) | ({
25
+ platform: 'google_business';
26
+ } & GoogleBusinessPreviewProps) | ({
27
+ platform: 'generic';
28
+ } & GenericPreviewProps));
29
+ export declare function PostPreview(props: PostPreviewProps): React.ReactElement;