@linktr.ee/messaging-react 2.1.0 → 2.2.0-rc-1778753733

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 (58) hide show
  1. package/dist/{Card-CsJvUF_b.js → Card-BdTueeyk.js} +2 -2
  2. package/dist/{Card-CsJvUF_b.js.map → Card-BdTueeyk.js.map} +1 -1
  3. package/dist/{Card-DlMSDSdm.js → Card-ChR37pLZ.js} +2 -2
  4. package/dist/{Card-DlMSDSdm.js.map → Card-ChR37pLZ.js.map} +1 -1
  5. package/dist/{Card-CFFNq49v.js → Card-EKxCn56j.js} +3 -3
  6. package/dist/{Card-CFFNq49v.js.map → Card-EKxCn56j.js.map} +1 -1
  7. package/dist/{LockedThumbnail-DpJx169C.js → LockedThumbnail-B16qP3eH.js} +2 -2
  8. package/dist/{LockedThumbnail-DpJx169C.js.map → LockedThumbnail-B16qP3eH.js.map} +1 -1
  9. package/dist/index-Dn7BC9xK.js +4748 -0
  10. package/dist/index-Dn7BC9xK.js.map +1 -0
  11. package/dist/index.d.ts +591 -25
  12. package/dist/index.js +24 -19
  13. package/package.json +1 -1
  14. package/src/components/CustomMessage/MessageAttachmentConversations.stories.tsx +841 -0
  15. package/src/components/LinkAttachment/LinkAttachment.stories.tsx +7 -92
  16. package/src/components/LinkAttachment/LinkAttachment.test.tsx +69 -0
  17. package/src/components/LinkAttachment/components/Received/Card.tsx +10 -30
  18. package/src/components/LinkAttachment/components/_shared/CardShell.tsx +5 -1
  19. package/src/components/LinkAttachment/index.tsx +24 -50
  20. package/src/components/LinkAttachment/types.ts +12 -5
  21. package/src/components/MessageAttachment/Audio/AudioAttachment.stories.tsx +203 -0
  22. package/src/components/MessageAttachment/Audio/index.tsx +189 -0
  23. package/src/components/MessageAttachment/File/FileAttachment.stories.tsx +352 -0
  24. package/src/components/MessageAttachment/File/index.tsx +240 -0
  25. package/src/components/MessageAttachment/Image/ImageAttachment.stories.tsx +288 -0
  26. package/src/components/MessageAttachment/Image/index.tsx +257 -0
  27. package/src/components/MessageAttachment/MessageAttachment.test.tsx +783 -0
  28. package/src/components/MessageAttachment/Pdf/PdfAttachment.stories.tsx +292 -0
  29. package/src/components/MessageAttachment/Pdf/index.tsx +228 -0
  30. package/src/components/MessageAttachment/Video/VideoAttachment.stories.tsx +272 -0
  31. package/src/components/MessageAttachment/Video/index.tsx +281 -0
  32. package/src/components/MessageAttachment/_shared/Bubble.tsx +173 -0
  33. package/src/components/MessageAttachment/_shared/CompactDocumentRow.tsx +152 -0
  34. package/src/components/MessageAttachment/_shared/DismissButton.tsx +39 -0
  35. package/src/components/MessageAttachment/_shared/DownloadAction.tsx +175 -0
  36. package/src/components/MessageAttachment/_shared/ImageViewer.tsx +314 -0
  37. package/src/components/MessageAttachment/_shared/MediaStackGrid.tsx +139 -0
  38. package/src/components/MessageAttachment/_shared/PdfViewer.tsx +100 -0
  39. package/src/components/MessageAttachment/_shared/VideoViewer.tsx +171 -0
  40. package/src/components/MessageAttachment/_shared/ViewerShell.tsx +159 -0
  41. package/src/components/MessageAttachment/_shared/fileMeta.test.ts +82 -0
  42. package/src/components/MessageAttachment/_shared/fileMeta.ts +95 -0
  43. package/src/components/MessageAttachment/_shared/triggerDownload.ts +54 -0
  44. package/src/components/MessageAttachment/_shared/useViewer.ts +53 -0
  45. package/src/components/MessageAttachment/index.tsx +149 -0
  46. package/src/components/MessageAttachment/stories/StoryTable.tsx +72 -0
  47. package/src/components/MessageAttachment/types.ts +178 -0
  48. package/src/index.ts +32 -0
  49. package/dist/Card-D32U6KfZ.js +0 -85
  50. package/dist/Card-D32U6KfZ.js.map +0 -1
  51. package/dist/Card-DlSSJPip.js +0 -60
  52. package/dist/Card-DlSSJPip.js.map +0 -1
  53. package/dist/Card-zGbhRBwv.js +0 -48
  54. package/dist/Card-zGbhRBwv.js.map +0 -1
  55. package/dist/CardThumbnail-DTBuRQHF.js +0 -239
  56. package/dist/CardThumbnail-DTBuRQHF.js.map +0 -1
  57. package/dist/index-DfcRe-Hj.js +0 -3103
  58. package/dist/index-DfcRe-Hj.js.map +0 -1
@@ -1,239 +0,0 @@
1
- import { jsx as l, jsxs as c } from "react/jsx-runtime";
2
- import d from "classnames";
3
- import { g as p, r as v } from "./index-DfcRe-Hj.js";
4
- import "react";
5
- import "@phosphor-icons/react";
6
- const w = /^([a-z][a-z0-9+.-]*):/i, k = /* @__PURE__ */ new Set(["http", "https", "mailto", "tel", "sms"]);
7
- function C(t) {
8
- if (typeof t != "string") return;
9
- const e = t.trim();
10
- if (e === "") return;
11
- const s = w.exec(e);
12
- if (s) {
13
- const n = s[1].toLowerCase();
14
- return k.has(n) ? e : void 0;
15
- }
16
- return e.startsWith("//") || e.startsWith("/") ? e : `https://${e}`;
17
- }
18
- const S = {
19
- dark: "bg-white text-[#121110] hover:bg-white/90",
20
- light: "bg-[#121110] text-white hover:bg-[#2a2928]"
21
- }, A = ({ variant: t, cta: e }) => {
22
- const s = d(
23
- "mt-2 inline-flex h-10 w-full items-center justify-center rounded-full px-4 text-sm font-medium leading-none transition-colors",
24
- S[t]
25
- ), n = C(e.href);
26
- return n ? /* @__PURE__ */ l(
27
- "a",
28
- {
29
- href: n,
30
- target: "_blank",
31
- rel: "noopener noreferrer",
32
- onClick: (a) => {
33
- var r;
34
- a.stopPropagation(), (r = e.onClick) == null || r.call(e);
35
- },
36
- className: `${s} no-underline`,
37
- children: e.label
38
- }
39
- ) : /* @__PURE__ */ l(
40
- "button",
41
- {
42
- type: "button",
43
- onClick: (a) => {
44
- var r;
45
- a.stopPropagation(), (r = e.onClick) == null || r.call(e);
46
- },
47
- className: s,
48
- children: e.label
49
- }
50
- );
51
- }, E = {
52
- dark: "text-white",
53
- light: "text-black/90"
54
- }, L = "text-white/30", y = {
55
- dark: "text-white/55",
56
- light: "text-black/55"
57
- }, F = ({
58
- variant: t,
59
- title: e,
60
- placeholderTitle: s,
61
- description: n,
62
- url: a,
63
- appIcon: r,
64
- cta: i,
65
- trailingAction: u
66
- }) => {
67
- const o = t === "dark", f = e ?? (o ? s : void 0) ?? "", h = f.trim() !== "", m = n != null && n.trim() !== "", b = typeof a == "string" ? a.trim() : "", g = b !== "", x = i != null;
68
- if (!h && !m && !g && !x) return null;
69
- const _ = d(
70
- "truncate text-base font-medium leading-6",
71
- o && !e ? L : E[t]
72
- ), N = d(
73
- "truncate text-xs leading-4",
74
- y[t]
75
- );
76
- return /* @__PURE__ */ c("div", { className: "px-4 py-3", children: [
77
- /* @__PURE__ */ c("div", { className: "flex items-end gap-3", children: [
78
- /* @__PURE__ */ c("div", { className: "flex min-w-0 flex-1 flex-col gap-2", children: [
79
- /* @__PURE__ */ c("div", { className: "flex min-w-0 flex-col gap-1", children: [
80
- h && /* @__PURE__ */ c("div", { className: "flex min-w-0 items-center gap-2", children: [
81
- r ? /* @__PURE__ */ l("span", { className: "shrink-0", children: r }) : null,
82
- /* @__PURE__ */ l("p", { className: d("min-w-0", _), children: f })
83
- ] }),
84
- m && /* @__PURE__ */ l("p", { className: N, children: n })
85
- ] }),
86
- !x && g && /* @__PURE__ */ l("p", { className: N, children: b })
87
- ] }),
88
- u && /* @__PURE__ */ l("div", { className: "shrink-0", children: u })
89
- ] }),
90
- i && /* @__PURE__ */ l(A, { variant: t, cta: i })
91
- ] });
92
- }, T = d(
93
- "relative block w-[280px] select-none overflow-hidden rounded-md",
94
- "shadow-[0_0_0_1px_rgba(0,0,0,0.04),0_1px_2px_rgba(0,0,0,0.04),0_8px_32px_rgba(0,0,0,0.1)]"
95
- ), V = ({
96
- variant: t,
97
- children: e,
98
- href: s,
99
- onClick: n,
100
- ariaLabel: a,
101
- rootRef: r,
102
- topRight: i,
103
- bgClassName: u,
104
- "data-testid": o
105
- }) => {
106
- const f = s != null || n != null, h = d(
107
- T,
108
- u ?? (t === "dark" ? "bg-[#121110]" : "bg-white"),
109
- // `focus-ring` is a design-system utility from the component-library
110
- // tailwind preset — outline-none + a black 2px focus-visible ring
111
- // with offset, so keyboard users can see the focused card.
112
- f ? "cursor-pointer no-underline focus-ring" : null
113
- ), m = i ? /* @__PURE__ */ l("div", { className: "pointer-events-auto absolute right-3 top-3 z-10", children: i }) : null;
114
- return s ? /* @__PURE__ */ c(
115
- "a",
116
- {
117
- ref: r,
118
- href: s,
119
- target: "_blank",
120
- rel: "noopener noreferrer",
121
- onClick: n,
122
- "data-testid": o,
123
- className: h,
124
- children: [
125
- e,
126
- m
127
- ]
128
- }
129
- ) : n ? /* @__PURE__ */ c(
130
- "button",
131
- {
132
- ref: r,
133
- type: "button",
134
- onClick: n,
135
- "aria-label": a,
136
- "data-testid": o,
137
- className: d(h, "text-left"),
138
- children: [
139
- e,
140
- m
141
- ]
142
- }
143
- ) : /* @__PURE__ */ c(
144
- "div",
145
- {
146
- ref: r,
147
- "data-testid": o,
148
- className: h,
149
- children: [
150
- e,
151
- m
152
- ]
153
- }
154
- );
155
- }, D = {
156
- dark: "bg-white/10",
157
- light: "bg-black/5"
158
- }, z = {
159
- dark: "size-16 text-white/25",
160
- light: "size-16 text-black/25"
161
- }, I = (t, e) => !!e && !!t && p(t) === "audio", Y = (t, e) => {
162
- if (!e || !t) return !1;
163
- const s = p(t);
164
- return s === "video" || s === "audio";
165
- }, R = "bg-[#F2F3F4]", G = ({
166
- variant: t,
167
- thumbnailUrl: e,
168
- sourceUrl: s,
169
- title: n,
170
- mimeType: a = "image/*",
171
- topLeft: r,
172
- topRight: i
173
- }) => {
174
- const u = p(a), o = !!s && u === "video";
175
- return I(a, s) ? /* @__PURE__ */ l("div", { className: "p-3", children: /* @__PURE__ */ l(
176
- "audio",
177
- {
178
- src: s,
179
- controls: !0,
180
- preload: "metadata",
181
- className: "block w-full",
182
- children: /* @__PURE__ */ l("track", { kind: "captions" })
183
- }
184
- ) }) : /* @__PURE__ */ c(
185
- "div",
186
- {
187
- className: d(
188
- "relative h-[180px] w-full overflow-hidden",
189
- o && "bg-black"
190
- ),
191
- children: [
192
- o ? /* @__PURE__ */ l(
193
- "video",
194
- {
195
- src: s,
196
- poster: e,
197
- controls: !0,
198
- playsInline: !0,
199
- preload: "metadata",
200
- className: "absolute inset-0 h-full w-full object-contain",
201
- children: /* @__PURE__ */ l("track", { kind: "captions" })
202
- }
203
- ) : e ? /* @__PURE__ */ l(
204
- "img",
205
- {
206
- src: e,
207
- alt: n ?? "",
208
- draggable: !1,
209
- className: "absolute inset-0 h-full w-full object-cover"
210
- }
211
- ) : /* @__PURE__ */ l(
212
- "div",
213
- {
214
- className: d(
215
- "flex h-full w-full items-center justify-center",
216
- D[t]
217
- ),
218
- children: v(a, {
219
- className: z[t],
220
- weight: "regular"
221
- })
222
- }
223
- ),
224
- r ? /* @__PURE__ */ l("div", { className: "pointer-events-auto absolute left-3 top-3 z-10", children: r }) : null,
225
- i ? /* @__PURE__ */ l("div", { className: "pointer-events-auto absolute right-3 top-3 z-10", children: i }) : null
226
- ]
227
- }
228
- );
229
- };
230
- export {
231
- R as A,
232
- V as C,
233
- G as a,
234
- F as b,
235
- Y as c,
236
- I as i,
237
- C as n
238
- };
239
- //# sourceMappingURL=CardThumbnail-DTBuRQHF.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"CardThumbnail-DTBuRQHF.js","sources":["../src/components/LinkAttachment/components/_shared/normalizeExternalHref.ts","../src/components/LinkAttachment/components/_shared/CardCta.tsx","../src/components/LinkAttachment/components/_shared/CardBody.tsx","../src/components/LinkAttachment/components/_shared/CardShell.tsx","../src/components/LinkAttachment/components/_shared/CardThumbnail.tsx"],"sourcesContent":["/**\n * Scheme detector: `protocol:` per RFC 3986 — a letter followed by any\n * combination of letters, digits, `+`, `.`, or `-` then `:`.\n */\nconst SCHEME_PATTERN = /^([a-z][a-z0-9+.-]*):/i\n\n/**\n * Allowlist of schemes that are safe to forward into `<a href>` for\n * external navigation. `javascript:` / `data:` / `vbscript:` etc. are\n * intentionally **not** on this list — link-attachment data is\n * effectively user-controlled, so passing them through would let a\n * recipient click execute attacker-supplied code or markup.\n */\nconst SAFE_SCHEMES = new Set(['http', 'https', 'mailto', 'tel', 'sms'])\n\n/**\n * Normalize a user-supplied URL into something safe to assign to\n * `<a href>` for external navigation.\n *\n * Link attachments / link apps always point at external destinations\n * (Spotify, TikTok, FAQ links, bare-hostname Linktree URLs like\n * `tr.ee/briemix`, etc.). Without normalization, a bare hostname is\n * treated as a relative path by the browser and clicks navigate within\n * the host site (e.g. `https://linktr.ee/admin/tr.ee/briemix`) instead\n * of opening the intended destination.\n *\n * Rules:\n * - Empty / whitespace-only → returns `undefined` (no href).\n * - Explicit scheme in the safe allowlist (`http`, `https`, `mailto`,\n * `tel`, `sms`) → returned trimmed.\n * - Explicit scheme **not** on the allowlist (`javascript:`, `data:`,\n * `vbscript:`, custom protocols, …) → returns `undefined` so the\n * shell falls back to a non-navigational chrome instead of letting\n * an attacker-controlled URL execute on click.\n * - Protocol-relative (`//example.com/…`) → returned as-is; browsers\n * resolve these against the current page's scheme.\n * - Site-relative path (`/admin/…`) → returned as-is so consumers can\n * still opt into in-app navigation if they really want to.\n * - Bare hostname or anything else → `https://` is prepended so the\n * browser treats it as an external URL.\n */\nexport function normalizeExternalHref(value?: string): string | undefined {\n if (typeof value !== 'string') return undefined\n const trimmed = value.trim()\n if (trimmed === '') return undefined\n\n const schemeMatch = SCHEME_PATTERN.exec(trimmed)\n if (schemeMatch) {\n const scheme = schemeMatch[1].toLowerCase()\n return SAFE_SCHEMES.has(scheme) ? trimmed : undefined\n }\n\n if (trimmed.startsWith('//')) return trimmed\n if (trimmed.startsWith('/')) return trimmed\n return `https://${trimmed}`\n}\n","import classNames from 'classnames'\nimport React from 'react'\n\nimport type { LinkAttachmentCta } from '../../types'\n\nimport type { LinkAttachmentVariant } from './CardShell'\nimport { normalizeExternalHref } from './normalizeExternalHref'\n\nexport interface CardCtaProps {\n variant: LinkAttachmentVariant\n cta: LinkAttachmentCta\n}\n\nconst BUTTON_CLASS_BY_VARIANT: Record<LinkAttachmentVariant, string> = {\n dark: 'bg-white text-[#121110] hover:bg-white/90',\n light: 'bg-[#121110] text-white hover:bg-[#2a2928]',\n}\n\n/**\n * Pill-shaped CTA rendered below the description on Link App cards that\n * surface an action instead of a URL (e.g. FAQ \"View FAQs\", Form \"Complete form\").\n * Renders as `<a target=\"_blank\">` when `cta.href` is set, otherwise as a\n * plain `<button>`.\n */\nconst CardCta: React.FC<CardCtaProps> = ({ variant, cta }) => {\n const className = classNames(\n 'mt-2 inline-flex h-10 w-full items-center justify-center rounded-full px-4 text-sm font-medium leading-none transition-colors',\n BUTTON_CLASS_BY_VARIANT[variant]\n )\n\n // Mirror the URL normalization used by the shell anchor so bare\n // hostnames (e.g. `tr.ee/foo`) open as external links rather than\n // resolving against the current host.\n const normalizedHref = normalizeExternalHref(cta.href)\n\n if (normalizedHref) {\n return (\n <a\n href={normalizedHref}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n onClick={(e) => {\n // Stop the click from bubbling up to the card's anchor wrapper\n // (Received variant) so we don't navigate twice.\n e.stopPropagation()\n cta.onClick?.()\n }}\n className={`${className} no-underline`}\n >\n {cta.label}\n </a>\n )\n }\n\n return (\n <button\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation()\n cta.onClick?.()\n }}\n className={className}\n >\n {cta.label}\n </button>\n )\n}\n\nexport default CardCta\n","import classNames from 'classnames'\nimport React from 'react'\n\nimport type { LinkAttachmentCta } from '../../types'\n\nimport CardCta from './CardCta'\nimport type { LinkAttachmentVariant } from './CardShell'\n\nexport interface CardBodyProps {\n variant: LinkAttachmentVariant\n title?: string\n /** Placeholder shown in the title slot when no title is set (dark variants only). */\n placeholderTitle?: string\n description?: string\n /** Footer URL shown below the description. Ignored when `cta` is set. */\n url?: string\n /**\n * Optional 16x16 brand badge rendered before the title (used by Link Apps:\n * Spotify, TikTok, FAQ, Form, etc.).\n */\n appIcon?: React.ReactNode\n /** Optional CTA rendered in place of the URL footer. */\n cta?: LinkAttachmentCta\n /** Trailing action rendered on the right of the title/description block. */\n trailingAction?: React.ReactNode\n}\n\nconst TITLE_CLASS_BY_VARIANT: Record<LinkAttachmentVariant, string> = {\n dark: 'text-white',\n light: 'text-black/90',\n}\n\nconst TITLE_DIMMED_CLASS = 'text-white/30'\n\nconst SECONDARY_CLASS_BY_VARIANT: Record<LinkAttachmentVariant, string> = {\n dark: 'text-white/55',\n light: 'text-black/55',\n}\n\n/**\n * Body of a `LinkAttachment.*` card. Matches the Figma `Container > Labels`\n * group: 16px horizontal padding, 12px vertical padding, 8px gap between\n * the title/description group and the URL/CTA footer, 4px gap within the\n * title group.\n *\n * Returns `null` when there's nothing to render so plain image / file\n * attachments collapse to a thumbnail-only card.\n */\nconst CardBody: React.FC<CardBodyProps> = ({\n variant,\n title,\n placeholderTitle,\n description,\n url,\n appIcon,\n cta,\n trailingAction,\n}) => {\n const isDark = variant === 'dark'\n const displayTitle = title ?? (isDark ? placeholderTitle : undefined) ?? ''\n const hasTitle = displayTitle.trim() !== ''\n const hasDescription =\n description != null && description.trim() !== ''\n // Mirror the trimming applied by `ReceivedCard` so a whitespace-only\n // `url` collapses the body footer (and the whole body, for media-only\n // cards) instead of rendering an empty line.\n const trimmedUrl = typeof url === 'string' ? url.trim() : ''\n const hasUrl = trimmedUrl !== ''\n const hasCta = cta != null\n\n if (!hasTitle && !hasDescription && !hasUrl && !hasCta) return null\n\n const titleDimmed = isDark && !title\n\n const titleClass = classNames(\n 'truncate text-base font-medium leading-6',\n titleDimmed ? TITLE_DIMMED_CLASS : TITLE_CLASS_BY_VARIANT[variant]\n )\n\n const secondaryClass = classNames(\n 'truncate text-xs leading-4',\n SECONDARY_CLASS_BY_VARIANT[variant]\n )\n\n return (\n <div className=\"px-4 py-3\">\n <div className=\"flex items-end gap-3\">\n <div className=\"flex min-w-0 flex-1 flex-col gap-2\">\n <div className=\"flex min-w-0 flex-col gap-1\">\n {hasTitle && (\n <div className=\"flex min-w-0 items-center gap-2\">\n {appIcon ? <span className=\"shrink-0\">{appIcon}</span> : null}\n <p className={classNames('min-w-0', titleClass)}>\n {displayTitle}\n </p>\n </div>\n )}\n\n {hasDescription && (\n <p className={secondaryClass}>{description}</p>\n )}\n </div>\n\n {!hasCta && hasUrl && (\n <p className={secondaryClass}>{trimmedUrl}</p>\n )}\n </div>\n\n {trailingAction && <div className=\"shrink-0\">{trailingAction}</div>}\n </div>\n\n {cta && <CardCta variant={variant} cta={cta} />}\n </div>\n )\n}\n\nexport default CardBody\n","import classNames from 'classnames'\nimport React from 'react'\n\nexport type LinkAttachmentVariant = 'dark' | 'light'\n\nexport interface CardShellProps {\n variant: LinkAttachmentVariant\n children: React.ReactNode\n /**\n * When provided, the entire card chrome is rendered as an anchor (used by\n * the Received card to open the link target on click). Falls back to a\n * `<div>` when omitted so Composer / Sent cards stay non-navigational.\n */\n href?: string\n /**\n * Click handler for the card chrome. When `href` is set the shell is an\n * anchor and `onClick` is invoked in addition to navigation. When `href`\n * is omitted but `onClick` is set, the shell renders as a clickable\n * button (used by media-only Received cards to open an image preview).\n */\n onClick?: () => void\n /** Accessible label for the clickable variant (when `onClick` is set without `href`). */\n ariaLabel?: string\n rootRef?: React.Ref<HTMLElement>\n /**\n * Absolutely-positioned slot rendered in the top-right corner of the\n * shell. Used by the Composer card to surface its dismiss affordance\n * when there's no hero thumbnail to anchor it to.\n */\n topRight?: React.ReactNode\n /**\n * Overrides the variant-derived background colour (e.g. audio cards\n * use `bg-[#F2F3F4]` regardless of the dark/light variant).\n */\n bgClassName?: string\n 'data-testid'?: string\n}\n\nconst SHELL_CLASS = classNames(\n 'relative block w-[280px] select-none overflow-hidden rounded-md',\n 'shadow-[0_0_0_1px_rgba(0,0,0,0.04),0_1px_2px_rgba(0,0,0,0.04),0_8px_32px_rgba(0,0,0,0.1)]'\n)\n\n/**\n * Outer chrome for every `LinkAttachment.*` card. Matches the 280px width,\n * 16px corner radius, and shadow-400 treatment from the Figma design system.\n */\nconst CardShell: React.FC<CardShellProps> = ({\n variant,\n children,\n href,\n onClick,\n ariaLabel,\n rootRef,\n topRight,\n bgClassName,\n 'data-testid': dataTestId,\n}) => {\n const isInteractive = href != null || onClick != null\n const className = classNames(\n SHELL_CLASS,\n bgClassName ?? (variant === 'dark' ? 'bg-[#121110]' : 'bg-white'),\n // `focus-ring` is a design-system utility from the component-library\n // tailwind preset — outline-none + a black 2px focus-visible ring\n // with offset, so keyboard users can see the focused card.\n isInteractive ? 'cursor-pointer no-underline focus-ring' : null\n )\n\n const corner = topRight ? (\n <div className=\"pointer-events-auto absolute right-3 top-3 z-10\">\n {topRight}\n </div>\n ) : null\n\n if (href) {\n return (\n <a\n ref={rootRef as React.Ref<HTMLAnchorElement>}\n href={href}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n onClick={onClick}\n data-testid={dataTestId}\n className={className}\n >\n {children}\n {corner}\n </a>\n )\n }\n\n if (onClick) {\n return (\n <button\n ref={rootRef as React.Ref<HTMLButtonElement>}\n type=\"button\"\n onClick={onClick}\n aria-label={ariaLabel}\n data-testid={dataTestId}\n className={classNames(className, 'text-left')}\n >\n {children}\n {corner}\n </button>\n )\n }\n\n return (\n <div\n ref={rootRef as React.Ref<HTMLDivElement>}\n data-testid={dataTestId}\n className={className}\n >\n {children}\n {corner}\n </div>\n )\n}\n\nexport default CardShell\n","import classNames from 'classnames'\nimport React from 'react'\n\nimport { renderTypeIcon } from '../../../AttachmentCard'\nimport { getSourceType } from '../../../AttachmentCard/utils/mimeType'\n\nimport type { LinkAttachmentVariant } from './CardShell'\n\nexport interface CardThumbnailProps {\n variant: LinkAttachmentVariant\n /** Source URL of the hero image (or poster for video). */\n thumbnailUrl?: string\n /**\n * Playable media URL. When provided alongside a video / audio `mimeType`,\n * the hero region renders a native HTML5 player with controls instead of\n * the static thumbnail / placeholder.\n */\n sourceUrl?: string\n /** Alt text — typically the card's title. */\n title?: string\n /**\n * Drives the placeholder type icon when no `thumbnailUrl` is provided,\n * and selects between image / video / audio rendering when `sourceUrl`\n * is set. Defaults to a generic image icon when unset.\n */\n mimeType?: string\n /** Optional decorations layered into the top corners of the thumbnail. */\n topLeft?: React.ReactNode\n topRight?: React.ReactNode\n}\n\nconst PLACEHOLDER_BG: Record<LinkAttachmentVariant, string> = {\n dark: 'bg-white/10',\n light: 'bg-black/5',\n}\n\nconst PLACEHOLDER_ICON: Record<LinkAttachmentVariant, string> = {\n dark: 'size-16 text-white/25',\n light: 'size-16 text-black/25',\n}\n\n/**\n * 180px hero region shown above the card body. Renders, in priority order:\n * 1. A native `<video controls>` when `sourceUrl` is set and the mime is\n * video — `thumbnailUrl` acts as the poster.\n * 2. A native `<audio controls>` when `sourceUrl` is set and the mime is\n * audio — laid over the audio type-icon backdrop.\n * 3. The supplied `thumbnailUrl` image.\n * 4. A placeholder type-icon derived from `mimeType`.\n */\n/** Mime + sourceUrl gives us a playable audio attachment. */\nexport const isPlayableAudio = (mimeType?: string, sourceUrl?: string) =>\n !!sourceUrl && !!mimeType && getSourceType(mimeType) === 'audio'\n\n/**\n * Mime + sourceUrl gives us a playable video or audio attachment. Used by\n * Received to skip wrapping the shell in an interactive `<button>` so the\n * native media controls remain operable.\n */\nexport const isPlayableMedia = (mimeType?: string, sourceUrl?: string) => {\n if (!sourceUrl || !mimeType) return false\n const source = getSourceType(mimeType)\n return source === 'video' || source === 'audio'\n}\n\n/**\n * Background colour the LinkAttachment cards switch to when the source is\n * audio — flat neutral around the native `<audio>` chrome regardless of\n * the dark / light variant.\n */\nexport const AUDIO_BG_CLASS = 'bg-[#F2F3F4]'\n\nconst CardThumbnail: React.FC<CardThumbnailProps> = ({\n variant,\n thumbnailUrl,\n sourceUrl,\n title,\n mimeType = 'image/*',\n topLeft,\n topRight,\n}) => {\n const sourceType = getSourceType(mimeType)\n const isPlayableVideo = !!sourceUrl && sourceType === 'video'\n\n if (isPlayableAudio(mimeType, sourceUrl)) {\n // Audio collapses the hero entirely — the native player sits inside\n // the card chrome with a bit of padding so the card background\n // (typically `bg-[#F2F3F4]`) is visible around it.\n return (\n <div className=\"p-3\">\n <audio\n src={sourceUrl}\n controls\n preload=\"metadata\"\n className=\"block w-full\"\n >\n <track kind=\"captions\" />\n </audio>\n </div>\n )\n }\n\n return (\n <div\n className={classNames(\n 'relative h-[180px] w-full overflow-hidden',\n isPlayableVideo && 'bg-black'\n )}\n >\n {isPlayableVideo ? (\n <video\n src={sourceUrl}\n poster={thumbnailUrl}\n controls\n playsInline\n preload=\"metadata\"\n className=\"absolute inset-0 h-full w-full object-contain\"\n >\n <track kind=\"captions\" />\n </video>\n ) : thumbnailUrl ? (\n <img\n src={thumbnailUrl}\n alt={title ?? ''}\n draggable={false}\n className=\"absolute inset-0 h-full w-full object-cover\"\n />\n ) : (\n <div\n className={classNames(\n 'flex h-full w-full items-center justify-center',\n PLACEHOLDER_BG[variant]\n )}\n >\n {renderTypeIcon(mimeType, {\n className: PLACEHOLDER_ICON[variant],\n weight: 'regular',\n })}\n </div>\n )}\n\n {topLeft ? (\n <div className=\"pointer-events-auto absolute left-3 top-3 z-10\">\n {topLeft}\n </div>\n ) : null}\n {topRight ? (\n <div className=\"pointer-events-auto absolute right-3 top-3 z-10\">\n {topRight}\n </div>\n ) : null}\n </div>\n )\n}\n\nexport default CardThumbnail\n"],"names":["SCHEME_PATTERN","SAFE_SCHEMES","normalizeExternalHref","value","trimmed","schemeMatch","scheme","BUTTON_CLASS_BY_VARIANT","CardCta","variant","cta","className","classNames","normalizedHref","jsx","e","_a","TITLE_CLASS_BY_VARIANT","TITLE_DIMMED_CLASS","SECONDARY_CLASS_BY_VARIANT","CardBody","title","placeholderTitle","description","url","appIcon","trailingAction","isDark","displayTitle","hasTitle","hasDescription","trimmedUrl","hasUrl","hasCta","titleClass","secondaryClass","jsxs","SHELL_CLASS","CardShell","children","href","onClick","ariaLabel","rootRef","topRight","bgClassName","dataTestId","isInteractive","corner","PLACEHOLDER_BG","PLACEHOLDER_ICON","isPlayableAudio","mimeType","sourceUrl","getSourceType","isPlayableMedia","source","AUDIO_BG_CLASS","CardThumbnail","thumbnailUrl","topLeft","sourceType","isPlayableVideo"],"mappings":"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"}