@linktr.ee/messaging-react 2.0.1 → 2.1.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 (64) hide show
  1. package/dist/Card-CFFNq49v.js +163 -0
  2. package/dist/Card-CFFNq49v.js.map +1 -0
  3. package/dist/Card-CsJvUF_b.js +107 -0
  4. package/dist/Card-CsJvUF_b.js.map +1 -0
  5. package/dist/Card-D32U6KfZ.js +85 -0
  6. package/dist/Card-D32U6KfZ.js.map +1 -0
  7. package/dist/Card-DlMSDSdm.js +132 -0
  8. package/dist/Card-DlMSDSdm.js.map +1 -0
  9. package/dist/Card-DlSSJPip.js +60 -0
  10. package/dist/Card-DlSSJPip.js.map +1 -0
  11. package/dist/Card-zGbhRBwv.js +48 -0
  12. package/dist/Card-zGbhRBwv.js.map +1 -0
  13. package/dist/CardThumbnail-DTBuRQHF.js +239 -0
  14. package/dist/CardThumbnail-DTBuRQHF.js.map +1 -0
  15. package/dist/LockedThumbnail-DpJx169C.js +220 -0
  16. package/dist/LockedThumbnail-DpJx169C.js.map +1 -0
  17. package/dist/{index-Bex7eg3v.js → index-DfcRe-Hj.js} +618 -607
  18. package/dist/index-DfcRe-Hj.js.map +1 -0
  19. package/dist/index.d.ts +217 -28
  20. package/dist/index.js +16 -15
  21. package/package.json +1 -1
  22. package/src/components/CustomMessage/index.tsx +2 -3
  23. package/src/components/LinkAttachment/LinkAttachment.stories.tsx +307 -0
  24. package/src/components/LinkAttachment/components/Composer/Card.tsx +117 -0
  25. package/src/components/LinkAttachment/components/Composer/index.ts +2 -0
  26. package/src/components/LinkAttachment/components/Received/Card.tsx +132 -0
  27. package/src/components/LinkAttachment/components/Received/index.ts +2 -0
  28. package/src/components/LinkAttachment/components/Sent/Card.tsx +57 -0
  29. package/src/components/LinkAttachment/components/Sent/index.ts +2 -0
  30. package/src/components/LinkAttachment/components/_shared/CardBody.tsx +117 -0
  31. package/src/components/LinkAttachment/components/_shared/CardCta.tsx +69 -0
  32. package/src/components/LinkAttachment/components/_shared/CardShell.tsx +120 -0
  33. package/src/components/LinkAttachment/components/_shared/CardThumbnail.tsx +156 -0
  34. package/src/components/LinkAttachment/components/_shared/normalizeExternalHref.ts +56 -0
  35. package/src/components/LinkAttachment/index.tsx +68 -0
  36. package/src/components/LinkAttachment/types.ts +69 -0
  37. package/src/components/LockedAttachment/LockedAttachment.stories.tsx +230 -89
  38. package/src/components/LockedAttachment/components/Composer/Card.tsx +221 -0
  39. package/src/components/LockedAttachment/components/Composer/index.ts +2 -0
  40. package/src/components/LockedAttachment/components/Received/Card.tsx +191 -0
  41. package/src/components/LockedAttachment/components/Received/CardActions.tsx +91 -0
  42. package/src/components/LockedAttachment/components/Received/index.ts +2 -0
  43. package/src/components/LockedAttachment/components/Sent/Card.tsx +177 -0
  44. package/src/components/LockedAttachment/components/Sent/index.ts +2 -0
  45. package/src/components/LockedAttachment/components/_shared/CardBody.tsx +94 -0
  46. package/src/components/LockedAttachment/components/_shared/GalleryThumbnail.tsx +178 -0
  47. package/src/components/LockedAttachment/components/_shared/LockBadge.tsx +39 -0
  48. package/src/components/LockedAttachment/components/_shared/LockedCardShell.tsx +36 -0
  49. package/src/components/LockedAttachment/components/_shared/LockedThumbnail.tsx +128 -0
  50. package/src/components/LockedAttachment/index.tsx +43 -12
  51. package/src/components/LockedAttachment/types.ts +17 -0
  52. package/src/components/MediaMessage/index.tsx +8 -2
  53. package/src/index.ts +15 -1
  54. package/dist/Card-BKP9ml9O.js +0 -138
  55. package/dist/Card-BKP9ml9O.js.map +0 -1
  56. package/dist/Card-Bk_4lVzP.js +0 -127
  57. package/dist/Card-Bk_4lVzP.js.map +0 -1
  58. package/dist/index-Bex7eg3v.js.map +0 -1
  59. package/src/components/LockedAttachment/components/Creator/Card.tsx +0 -210
  60. package/src/components/LockedAttachment/components/Creator/index.tsx +0 -2
  61. package/src/components/LockedAttachment/components/Visitor/Card.tsx +0 -155
  62. package/src/components/LockedAttachment/components/Visitor/CardActions.tsx +0 -62
  63. package/src/components/LockedAttachment/components/Visitor/LockBadge.tsx +0 -12
  64. package/src/components/LockedAttachment/components/Visitor/index.ts +0 -2
@@ -0,0 +1,156 @@
1
+ import classNames from 'classnames'
2
+ import React from 'react'
3
+
4
+ import { renderTypeIcon } from '../../../AttachmentCard'
5
+ import { getSourceType } from '../../../AttachmentCard/utils/mimeType'
6
+
7
+ import type { LinkAttachmentVariant } from './CardShell'
8
+
9
+ export interface CardThumbnailProps {
10
+ variant: LinkAttachmentVariant
11
+ /** Source URL of the hero image (or poster for video). */
12
+ thumbnailUrl?: string
13
+ /**
14
+ * Playable media URL. When provided alongside a video / audio `mimeType`,
15
+ * the hero region renders a native HTML5 player with controls instead of
16
+ * the static thumbnail / placeholder.
17
+ */
18
+ sourceUrl?: string
19
+ /** Alt text — typically the card's title. */
20
+ title?: string
21
+ /**
22
+ * Drives the placeholder type icon when no `thumbnailUrl` is provided,
23
+ * and selects between image / video / audio rendering when `sourceUrl`
24
+ * is set. Defaults to a generic image icon when unset.
25
+ */
26
+ mimeType?: string
27
+ /** Optional decorations layered into the top corners of the thumbnail. */
28
+ topLeft?: React.ReactNode
29
+ topRight?: React.ReactNode
30
+ }
31
+
32
+ const PLACEHOLDER_BG: Record<LinkAttachmentVariant, string> = {
33
+ dark: 'bg-white/10',
34
+ light: 'bg-black/5',
35
+ }
36
+
37
+ const PLACEHOLDER_ICON: Record<LinkAttachmentVariant, string> = {
38
+ dark: 'size-16 text-white/25',
39
+ light: 'size-16 text-black/25',
40
+ }
41
+
42
+ /**
43
+ * 180px hero region shown above the card body. Renders, in priority order:
44
+ * 1. A native `<video controls>` when `sourceUrl` is set and the mime is
45
+ * video — `thumbnailUrl` acts as the poster.
46
+ * 2. A native `<audio controls>` when `sourceUrl` is set and the mime is
47
+ * audio — laid over the audio type-icon backdrop.
48
+ * 3. The supplied `thumbnailUrl` image.
49
+ * 4. A placeholder type-icon derived from `mimeType`.
50
+ */
51
+ /** Mime + sourceUrl gives us a playable audio attachment. */
52
+ export const isPlayableAudio = (mimeType?: string, sourceUrl?: string) =>
53
+ !!sourceUrl && !!mimeType && getSourceType(mimeType) === 'audio'
54
+
55
+ /**
56
+ * Mime + sourceUrl gives us a playable video or audio attachment. Used by
57
+ * Received to skip wrapping the shell in an interactive `<button>` so the
58
+ * native media controls remain operable.
59
+ */
60
+ export const isPlayableMedia = (mimeType?: string, sourceUrl?: string) => {
61
+ if (!sourceUrl || !mimeType) return false
62
+ const source = getSourceType(mimeType)
63
+ return source === 'video' || source === 'audio'
64
+ }
65
+
66
+ /**
67
+ * Background colour the LinkAttachment cards switch to when the source is
68
+ * audio — flat neutral around the native `<audio>` chrome regardless of
69
+ * the dark / light variant.
70
+ */
71
+ export const AUDIO_BG_CLASS = 'bg-[#F2F3F4]'
72
+
73
+ const CardThumbnail: React.FC<CardThumbnailProps> = ({
74
+ variant,
75
+ thumbnailUrl,
76
+ sourceUrl,
77
+ title,
78
+ mimeType = 'image/*',
79
+ topLeft,
80
+ topRight,
81
+ }) => {
82
+ const sourceType = getSourceType(mimeType)
83
+ const isPlayableVideo = !!sourceUrl && sourceType === 'video'
84
+
85
+ if (isPlayableAudio(mimeType, sourceUrl)) {
86
+ // Audio collapses the hero entirely — the native player sits inside
87
+ // the card chrome with a bit of padding so the card background
88
+ // (typically `bg-[#F2F3F4]`) is visible around it.
89
+ return (
90
+ <div className="p-3">
91
+ <audio
92
+ src={sourceUrl}
93
+ controls
94
+ preload="metadata"
95
+ className="block w-full"
96
+ >
97
+ <track kind="captions" />
98
+ </audio>
99
+ </div>
100
+ )
101
+ }
102
+
103
+ return (
104
+ <div
105
+ className={classNames(
106
+ 'relative h-[180px] w-full overflow-hidden',
107
+ isPlayableVideo && 'bg-black'
108
+ )}
109
+ >
110
+ {isPlayableVideo ? (
111
+ <video
112
+ src={sourceUrl}
113
+ poster={thumbnailUrl}
114
+ controls
115
+ playsInline
116
+ preload="metadata"
117
+ className="absolute inset-0 h-full w-full object-contain"
118
+ >
119
+ <track kind="captions" />
120
+ </video>
121
+ ) : thumbnailUrl ? (
122
+ <img
123
+ src={thumbnailUrl}
124
+ alt={title ?? ''}
125
+ draggable={false}
126
+ className="absolute inset-0 h-full w-full object-cover"
127
+ />
128
+ ) : (
129
+ <div
130
+ className={classNames(
131
+ 'flex h-full w-full items-center justify-center',
132
+ PLACEHOLDER_BG[variant]
133
+ )}
134
+ >
135
+ {renderTypeIcon(mimeType, {
136
+ className: PLACEHOLDER_ICON[variant],
137
+ weight: 'regular',
138
+ })}
139
+ </div>
140
+ )}
141
+
142
+ {topLeft ? (
143
+ <div className="pointer-events-auto absolute left-3 top-3 z-10">
144
+ {topLeft}
145
+ </div>
146
+ ) : null}
147
+ {topRight ? (
148
+ <div className="pointer-events-auto absolute right-3 top-3 z-10">
149
+ {topRight}
150
+ </div>
151
+ ) : null}
152
+ </div>
153
+ )
154
+ }
155
+
156
+ export default CardThumbnail
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Scheme detector: `protocol:` per RFC 3986 — a letter followed by any
3
+ * combination of letters, digits, `+`, `.`, or `-` then `:`.
4
+ */
5
+ const SCHEME_PATTERN = /^([a-z][a-z0-9+.-]*):/i
6
+
7
+ /**
8
+ * Allowlist of schemes that are safe to forward into `<a href>` for
9
+ * external navigation. `javascript:` / `data:` / `vbscript:` etc. are
10
+ * intentionally **not** on this list — link-attachment data is
11
+ * effectively user-controlled, so passing them through would let a
12
+ * recipient click execute attacker-supplied code or markup.
13
+ */
14
+ const SAFE_SCHEMES = new Set(['http', 'https', 'mailto', 'tel', 'sms'])
15
+
16
+ /**
17
+ * Normalize a user-supplied URL into something safe to assign to
18
+ * `<a href>` for external navigation.
19
+ *
20
+ * Link attachments / link apps always point at external destinations
21
+ * (Spotify, TikTok, FAQ links, bare-hostname Linktree URLs like
22
+ * `tr.ee/briemix`, etc.). Without normalization, a bare hostname is
23
+ * treated as a relative path by the browser and clicks navigate within
24
+ * the host site (e.g. `https://linktr.ee/admin/tr.ee/briemix`) instead
25
+ * of opening the intended destination.
26
+ *
27
+ * Rules:
28
+ * - Empty / whitespace-only → returns `undefined` (no href).
29
+ * - Explicit scheme in the safe allowlist (`http`, `https`, `mailto`,
30
+ * `tel`, `sms`) → returned trimmed.
31
+ * - Explicit scheme **not** on the allowlist (`javascript:`, `data:`,
32
+ * `vbscript:`, custom protocols, …) → returns `undefined` so the
33
+ * shell falls back to a non-navigational chrome instead of letting
34
+ * an attacker-controlled URL execute on click.
35
+ * - Protocol-relative (`//example.com/…`) → returned as-is; browsers
36
+ * resolve these against the current page's scheme.
37
+ * - Site-relative path (`/admin/…`) → returned as-is so consumers can
38
+ * still opt into in-app navigation if they really want to.
39
+ * - Bare hostname or anything else → `https://` is prepended so the
40
+ * browser treats it as an external URL.
41
+ */
42
+ export function normalizeExternalHref(value?: string): string | undefined {
43
+ if (typeof value !== 'string') return undefined
44
+ const trimmed = value.trim()
45
+ if (trimmed === '') return undefined
46
+
47
+ const schemeMatch = SCHEME_PATTERN.exec(trimmed)
48
+ if (schemeMatch) {
49
+ const scheme = schemeMatch[1].toLowerCase()
50
+ return SAFE_SCHEMES.has(scheme) ? trimmed : undefined
51
+ }
52
+
53
+ if (trimmed.startsWith('//')) return trimmed
54
+ if (trimmed.startsWith('/')) return trimmed
55
+ return `https://${trimmed}`
56
+ }
@@ -0,0 +1,68 @@
1
+ import React, { Suspense } from 'react'
2
+
3
+ import type { ComposerCardProps } from './components/Composer/Card'
4
+ import type { ReceivedCardProps } from './components/Received/Card'
5
+ import type { SentCardProps } from './components/Sent/Card'
6
+
7
+ const ComposerCardLazy = React.lazy(
8
+ () => import('./components/Composer/Card')
9
+ )
10
+ const SentCardLazy = React.lazy(() => import('./components/Sent/Card'))
11
+ const ReceivedCardLazy = React.lazy(
12
+ () => import('./components/Received/Card')
13
+ )
14
+
15
+ const LinkAttachmentFallback = () => (
16
+ <div
17
+ className="h-[280px] w-[280px] animate-pulse rounded-md bg-black/[0.06] 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)]"
18
+ aria-hidden
19
+ />
20
+ )
21
+
22
+ const Composer = (props: ComposerCardProps) => (
23
+ <Suspense fallback={<LinkAttachmentFallback />}>
24
+ <ComposerCardLazy {...props} />
25
+ </Suspense>
26
+ )
27
+
28
+ const Sent = (props: SentCardProps) => (
29
+ <Suspense fallback={<LinkAttachmentFallback />}>
30
+ <SentCardLazy {...props} />
31
+ </Suspense>
32
+ )
33
+
34
+ const Received = (props: ReceivedCardProps) => (
35
+ <Suspense fallback={<LinkAttachmentFallback />}>
36
+ <ReceivedCardLazy {...props} />
37
+ </Suspense>
38
+ )
39
+
40
+ /**
41
+ * Link attachments (image / file media + 1P/3P Link Apps) shown in the chat
42
+ * thread. Mirrors the `LockedAttachment` API — render `LinkAttachment.Composer`
43
+ * while drafting, `LinkAttachment.Sent` after posting, and
44
+ * `LinkAttachment.Received` in the recipient's thread. Maps to the
45
+ * "Attachments" and "LinkApps" sections of the messaging design system.
46
+ *
47
+ * Two visual layouts via the `layout` prop:
48
+ * - **Featured** (default) — 180px hero thumbnail above the body. Used by
49
+ * image / file Attachments and by hero-image LinkApps (Spotify with
50
+ * cover art, TikTok with a frame, etc.).
51
+ * - **Classic** — compact card with no hero thumbnail; title /
52
+ * description / URL / CTA only. Used for LinkApp embeds without
53
+ * artwork (FAQ, Form) and any link preview that lacks OG imagery.
54
+ *
55
+ * Image / file Attachments use `layout="featured"` and skip the title /
56
+ * description / URL body entirely (`CardBody` collapses to nothing when no
57
+ * text content is provided). LinkApps always carry a title + description
58
+ * and prefix the title with an `appIcon` brand badge.
59
+ */
60
+ const LinkAttachment = { Composer, Sent, Received }
61
+
62
+ export default LinkAttachment
63
+ export type { ComposerCardProps, SentCardProps, ReceivedCardProps }
64
+ export type {
65
+ LinkAttachmentBaseProps,
66
+ LinkAttachmentCta,
67
+ LinkAttachmentLayout,
68
+ } from './types'
@@ -0,0 +1,69 @@
1
+ import type React from 'react'
2
+
3
+ /**
4
+ * Visual layout for a `LinkAttachment.*` card.
5
+ *
6
+ * - `featured` — full card with a 180px hero thumbnail above the body.
7
+ * The default; matches the "Attachments" frames and the hero-image
8
+ * "LinkApps" frames in Figma.
9
+ * - `classic` — compact card without a hero thumbnail. Title /
10
+ * description / URL / CTA only. Used for Link App embeds that don't
11
+ * carry artwork (and for plain link previews without OG imagery).
12
+ */
13
+ export type LinkAttachmentLayout = 'featured' | 'classic'
14
+
15
+ /**
16
+ * Shared props for the three `LinkAttachment.*` variants (Composer, Sent,
17
+ * Received). Maps to the "Attachments" + "LinkApps" sections of the messaging
18
+ * design system in Figma — a 280px-wide card with an optional 180px hero
19
+ * thumbnail, a title (optionally prefixed with a brand badge for Link Apps),
20
+ * a description, and either a URL footer or a CTA button.
21
+ */
22
+ export interface LinkAttachmentBaseProps {
23
+ title?: string
24
+ /** Placeholder shown in the title slot before one is configured (dark variants only). */
25
+ placeholderTitle?: string
26
+ /** Secondary description rendered below the title. */
27
+ description?: string
28
+ /**
29
+ * Optional URL displayed at the bottom of the card (e.g. `tr.ee/briemix`).
30
+ * Ignored when `cta` is provided. Also used as the navigation target for
31
+ * the Received card when no `cta` is set.
32
+ */
33
+ url?: string
34
+ /** MIME type of the hero thumbnail — drives the type icon for empty states. */
35
+ mimeType?: string
36
+ /** Hero thumbnail (180px tall) shown above the title block. */
37
+ thumbnailUrl?: string
38
+ /**
39
+ * Source URL for playable media (video, audio). When provided alongside a
40
+ * video / audio `mimeType`, the hero region renders an inline player with
41
+ * native controls instead of the static thumbnail / type-icon.
42
+ */
43
+ sourceUrl?: string
44
+ /**
45
+ * Visual layout — `'featured'` keeps the 180px hero thumbnail above the
46
+ * body, `'classic'` drops the hero entirely for a compact text-only
47
+ * card. Defaults to `'featured'`.
48
+ */
49
+ layout?: LinkAttachmentLayout
50
+ /**
51
+ * Optional 16x16 brand badge rendered before the title (used by Link Apps:
52
+ * Spotify, TikTok, FAQ, Form, etc.). Consumers render whatever they want
53
+ * — typically a colored 4px-rounded square containing a glyph or `<img>`.
54
+ */
55
+ appIcon?: React.ReactNode
56
+ /**
57
+ * Optional call-to-action rendered below the description. When set,
58
+ * replaces the URL footer (e.g. FAQ "View FAQs", Form "Complete form").
59
+ */
60
+ cta?: LinkAttachmentCta
61
+ }
62
+
63
+ export interface LinkAttachmentCta {
64
+ label: string
65
+ /** When set, the CTA renders as an `<a>` opening in a new tab. */
66
+ href?: string
67
+ /** When set, called on click (in addition to `href` navigation if provided). */
68
+ onClick?: () => void
69
+ }