@linktr.ee/messaging-react 3.34.0 → 3.35.0-rc-1786104411

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1,2 +1,2 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("./index-BrVCdobn.cjs");exports.ActionButton=e.ActionButton;exports.Avatar=e.Avatar;exports.ChannelEmptyState=e.ChannelEmptyState;exports.ChannelList=e.ChannelList;exports.ChannelView=e.ChannelView;exports.CustomMessageProvider=e.CustomMessageProvider;exports.FaqList=e.FaqList;exports.FaqListItem=e.FaqListItem;exports.LinkAttachment=e.LinkAttachment;exports.LockedAttachment=e.LockedAttachment;exports.MediaMessage=e.MediaMessage;exports.MessageAttachment=e.MessageAttachment;exports.MessageBubble=e.MessageBubble;exports.MessageVoteButtons=e.MessageVoteButtons;exports.MessagingProvider=e.MessagingProvider;exports.MessagingShell=e.MessagingShell;exports.buildCompactMetaLabel=e.buildCompactMetaLabel;exports.formatFileSize=e.formatFileSize;exports.formatRelativeTime=e.formatRelativeTime;exports.getFileExtensionLabel=e.getFileExtensionLabel;exports.getMessageDisplayText=e.getMessageDisplayText;exports.isLinkAttachment=e.isLinkAttachment;exports.isUuidLike=e.isUuidLike;exports.messageAttachmentGroupPositionFromStream=e.bubbleGroupPositionFromStream;exports.normalizeLanguageCode=e.normalizeLanguageCode;exports.optimizeMessagingAttachmentUrl=e.optimizeMessagingAttachmentUrl;exports.resolveConversationParticipant=e.resolveConversationParticipant;exports.resolveLinkAttachment=e.resolveLinkAttachment;exports.resolveMediaFromMessage=e.resolveMediaFromMessage;exports.resolveParticipantDisplayName=e.resolveParticipantDisplayName;exports.useComposerLocked=e.useComposerLocked;exports.useCustomMessage=e.useCustomMessage;exports.useMessageVote=e.useMessageVote;exports.useMessaging=e.useMessaging;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("./index-DNWZLZ_A.cjs");exports.ActionButton=e.ActionButton;exports.Avatar=e.Avatar;exports.ChannelEmptyState=e.ChannelEmptyState;exports.ChannelList=e.ChannelList;exports.ChannelView=e.ChannelView;exports.CustomMessageProvider=e.CustomMessageProvider;exports.FaqList=e.FaqList;exports.FaqListItem=e.FaqListItem;exports.LinkAttachment=e.LinkAttachment;exports.LockedAttachment=e.LockedAttachment;exports.MediaMessage=e.MediaMessage;exports.MessageAttachment=e.MessageAttachment;exports.MessageBubble=e.MessageBubble;exports.MessageVoteButtons=e.MessageVoteButtons;exports.MessagingProvider=e.MessagingProvider;exports.MessagingShell=e.MessagingShell;exports.RichCard=e.RichCard;exports.buildCompactMetaLabel=e.buildCompactMetaLabel;exports.formatFileSize=e.formatFileSize;exports.formatRelativeTime=e.formatRelativeTime;exports.getFileExtensionLabel=e.getFileExtensionLabel;exports.getMessageDisplayText=e.getMessageDisplayText;exports.isLinkAttachment=e.isLinkAttachment;exports.isUuidLike=e.isUuidLike;exports.messageAttachmentGroupPositionFromStream=e.bubbleGroupPositionFromStream;exports.normalizeLanguageCode=e.normalizeLanguageCode;exports.optimizeMessagingAttachmentUrl=e.optimizeMessagingAttachmentUrl;exports.resolveConversationParticipant=e.resolveConversationParticipant;exports.resolveLinkAttachment=e.resolveLinkAttachment;exports.resolveMediaFromMessage=e.resolveMediaFromMessage;exports.resolveParticipantDisplayName=e.resolveParticipantDisplayName;exports.useComposerLocked=e.useComposerLocked;exports.useCustomMessage=e.useCustomMessage;exports.useMessageVote=e.useMessageVote;exports.useMessaging=e.useMessaging;
2
2
  //# sourceMappingURL=index.cjs.map
package/dist/index.d.ts CHANGED
@@ -18,6 +18,7 @@ import { MessagingUser } from '@linktr.ee/messaging-core';
18
18
  import { OfficialCtaActionKind } from '@linktr.ee/messaging-taxonomy';
19
19
  import { OfficialCtaActionPayload } from '@linktr.ee/messaging-taxonomy';
20
20
  import { Provider } from 'react';
21
+ import { RichCardImagePresentation } from '@linktr.ee/messaging-taxonomy';
21
22
  import { SendMessageAPIResponse } from 'stream-chat';
22
23
  import { SetStateAction } from 'react';
23
24
  import { StreamChat } from 'stream-chat';
@@ -1659,6 +1660,100 @@ export declare function resolveMediaFromMessage(message: LocalMessage): MediaMes
1659
1660
  */
1660
1661
  export declare function resolveParticipantDisplayName(user?: ParticipantDisplayUser | null): string;
1661
1662
 
1663
+ /**
1664
+ * The generic rich message card (`linktree_rich_card`) — the shared render
1665
+ * primitive for rich message surfaces. Render `RichCard.Sent` for the sender's
1666
+ * own copy (dark chrome) and `RichCard.Received` in the recipient's thread
1667
+ * (light chrome). The official welcome card and broadcast link/collection cards
1668
+ * are all this one card at different knobs:
1669
+ *
1670
+ * - `hero` + `primary` action → the official welcome card (stacked button)
1671
+ * - `hero` + `secondary` action → a single-link broadcast (inline button)
1672
+ * - `fan` + `secondary` action → a collection broadcast
1673
+ *
1674
+ * A standalone renderer, not tied to Stream's `Attachment`: the dispatching
1675
+ * consumer narrows an attachment with the taxonomy's `isRichCard`, then maps
1676
+ * the taxonomy `RichCard` fields onto these props (resolving each action's
1677
+ * `kind` to an `href` / `onClick`).
1678
+ */
1679
+ export declare const RichCard: {
1680
+ Sent: default_2.FC<RichCardBaseProps>;
1681
+ Received: default_2.FC<RichCardBaseProps>;
1682
+ };
1683
+
1684
+ /**
1685
+ * A single footer action button — the **presentational** half of a taxonomy
1686
+ * `RichCardAction`. The dispatching consumer resolves the action's `kind` to a
1687
+ * behaviour before handing it here: `open_external_url` → `href`; a `launch_*`
1688
+ * / `post_to_socials` kind → an `onClick` that opens its client surface. A kind
1689
+ * the consumer does not recognise is dropped (no button) rather than mapped, so
1690
+ * the card stands on its own image/title/subtitle — exactly the taxonomy's
1691
+ * unknown-kind rule.
1692
+ */
1693
+ export declare interface RichCardActionButton {
1694
+ label: string;
1695
+ /** Renders the button as an `<a target="_blank">` (for `open_external_url`). */
1696
+ href?: string;
1697
+ /** Called on activation (in addition to `href` navigation when both are set). */
1698
+ onClick?: () => void;
1699
+ /**
1700
+ * Button emphasis. The **first** action's variant drives the footer layout:
1701
+ * `primary` → a stacked full-width button; `secondary` → an inline compact
1702
+ * row. Defaults to `primary`.
1703
+ */
1704
+ variant?: 'primary' | 'secondary';
1705
+ }
1706
+
1707
+ /**
1708
+ * Shared props for the `RichCard.*` variants (Sent, Received). The generic
1709
+ * rich message card: a `hero` or `fan` image region, a title + subtitle, and a
1710
+ * `variant`-driven footer of action buttons. The official welcome card and
1711
+ * broadcast link/collection cards are all this one card at different knobs.
1712
+ */
1713
+ export declare interface RichCardBaseProps {
1714
+ title?: string;
1715
+ /** Subtitle / body copy — the base attachment's `text`. */
1716
+ subtitle?: string;
1717
+ /**
1718
+ * Max lines the subtitle wraps before clamping. Defaults to `3` — rich cards
1719
+ * carry prose, unlike the single-line link-preview default.
1720
+ */
1721
+ subtitleLines?: number;
1722
+ /** SAFE public thumbnail URLs. `hero` shows the first; `fan` shows up to 3. */
1723
+ images?: string[];
1724
+ /**
1725
+ * Image layout. Defaults from {@link images} length (≤1 → `hero`, ≥2 →
1726
+ * `fan`) via the taxonomy's `resolveRichCardImagePresentation`.
1727
+ */
1728
+ imagePresentation?: RichCardImagePresentation;
1729
+ /**
1730
+ * Footer actions. Empty / absent → no footer. Clients pass one today; the
1731
+ * shape is future-proof for several.
1732
+ */
1733
+ actions?: RichCardActionButton[];
1734
+ /** Optional 16×16 brand badge rendered before the title. */
1735
+ appIcon?: default_2.ReactNode;
1736
+ /**
1737
+ * Dominant colour of the hero image (`#RRGGBB` or bare hex), painted as the
1738
+ * card + tail surface. Only applied for a single on-screen `hero` image (a
1739
+ * `fan` card keeps the plain fill) — the same gate LinkAttachment uses.
1740
+ */
1741
+ accentColor?: string;
1742
+ /**
1743
+ * Position of this card inside its same-author run. `'single'` / `'end'`
1744
+ * paint the accent-coloured bubble tail; `'first'` / `'middle'` omit it.
1745
+ * Defaults to `'single'`.
1746
+ */
1747
+ groupPosition?: MessageAttachmentGroupPosition;
1748
+ }
1749
+
1750
+ export { RichCardImagePresentation }
1751
+
1752
+ export declare type RichCardReceivedCardProps = RichCardBaseProps;
1753
+
1754
+ /** Props for `RichCard.Sent` / `RichCard.Received`. */
1755
+ export declare type RichCardSentCardProps = RichCardBaseProps;
1756
+
1662
1757
  export declare interface SentBubbleProps extends MessageBubbleBaseProps {
1663
1758
  }
1664
1759
 
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { a as e, b as t, C as i, c as o, d as n, e as m, F as r, f as g, L as l, h as M, M as c, i as u, j as L, k as h, l as p, m as d, n as v, p as C, q as A, s as k, t as F, u as b, v as P, w as f, x, o as y, y as z, z as B, B as S, D as q, E as D, G as E, H as V, I as w } from "./index-Czf2XyYL.js";
1
+ import { a as e, b as t, C as i, c as o, d as n, e as m, F as r, f as g, L as l, h as M, M as c, i as u, j as h, k as L, l as d, m as p, R as C, n as v, p as A, q as k, s as F, t as b, u as P, v as f, w as x, x as y, o as z, y as B, z as S, B as q, D, E, G as R, H as V, I as w } from "./index-BaeffiED.js";
2
2
  export {
3
3
  e as ActionButton,
4
4
  t as Avatar,
@@ -12,26 +12,27 @@ export {
12
12
  M as LockedAttachment,
13
13
  c as MediaMessage,
14
14
  u as MessageAttachment,
15
- L as MessageBubble,
16
- h as MessageVoteButtons,
17
- p as MessagingProvider,
18
- d as MessagingShell,
15
+ h as MessageBubble,
16
+ L as MessageVoteButtons,
17
+ d as MessagingProvider,
18
+ p as MessagingShell,
19
+ C as RichCard,
19
20
  v as buildCompactMetaLabel,
20
- C as formatFileSize,
21
- A as formatRelativeTime,
22
- k as getFileExtensionLabel,
23
- F as getMessageDisplayText,
24
- b as isLinkAttachment,
25
- P as isUuidLike,
26
- f as messageAttachmentGroupPositionFromStream,
27
- x as normalizeLanguageCode,
28
- y as optimizeMessagingAttachmentUrl,
29
- z as resolveConversationParticipant,
30
- B as resolveLinkAttachment,
31
- S as resolveMediaFromMessage,
32
- q as resolveParticipantDisplayName,
33
- D as useComposerLocked,
34
- E as useCustomMessage,
21
+ A as formatFileSize,
22
+ k as formatRelativeTime,
23
+ F as getFileExtensionLabel,
24
+ b as getMessageDisplayText,
25
+ P as isLinkAttachment,
26
+ f as isUuidLike,
27
+ x as messageAttachmentGroupPositionFromStream,
28
+ y as normalizeLanguageCode,
29
+ z as optimizeMessagingAttachmentUrl,
30
+ B as resolveConversationParticipant,
31
+ S as resolveLinkAttachment,
32
+ q as resolveMediaFromMessage,
33
+ D as resolveParticipantDisplayName,
34
+ E as useComposerLocked,
35
+ R as useCustomMessage,
35
36
  V as useMessageVote,
36
37
  w as useMessaging
37
38
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linktr.ee/messaging-react",
3
- "version": "3.34.0",
3
+ "version": "3.35.0-rc-1786104411",
4
4
  "description": "React messaging components built on messaging-core for web applications",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -50,8 +50,8 @@
50
50
  },
51
51
  "dependencies": {
52
52
  "@linktr.ee/component-library": "11.8.6",
53
- "@linktr.ee/messaging-core": "^2.4.0",
54
- "@linktr.ee/messaging-taxonomy": "^0.6.0",
53
+ "@linktr.ee/messaging-core": "2.4.0-rc-1786104411",
54
+ "@linktr.ee/messaging-taxonomy": "^0.7.0",
55
55
  "@phosphor-icons/react": "^2.1.10"
56
56
  },
57
57
  "devDependencies": {
@@ -9,6 +9,18 @@ import { normalizeExternalHref } from './normalizeExternalHref'
9
9
  export interface CardCtaProps {
10
10
  variant: LinkAttachmentVariant
11
11
  cta: LinkAttachmentCta
12
+ /**
13
+ * Full-width pill (the default — LinkAttachment's single footer button).
14
+ * `false` sizes the button to its content, for an inline multi-action footer
15
+ * row (RichCard's `secondary` footer).
16
+ */
17
+ fullWidth?: boolean
18
+ /**
19
+ * Omit the default `mt-2` top margin when the caller owns spacing — e.g. a
20
+ * multi-action footer that spaces its buttons with its own `gap`. Defaults to
21
+ * `false`, preserving LinkAttachment's button-below-description offset.
22
+ */
23
+ standalone?: boolean
12
24
  }
13
25
 
14
26
  type CtaEmphasis = NonNullable<LinkAttachmentCta['variant']>
@@ -37,10 +49,17 @@ const BUTTON_CLASS_BY_VARIANT: Record<LinkAttachmentVariant, Record<CtaEmphasis,
37
49
  * Renders as `<a target="_blank">` when `cta.href` is set, otherwise as a
38
50
  * plain `<button>`.
39
51
  */
40
- const CardCta: React.FC<CardCtaProps> = ({ variant, cta }) => {
52
+ const CardCta: React.FC<CardCtaProps> = ({
53
+ variant,
54
+ cta,
55
+ fullWidth = true,
56
+ standalone = false,
57
+ }) => {
41
58
  const emphasis: CtaEmphasis = cta.variant ?? 'primary'
42
59
  const className = classNames(
43
- 'mt-2 inline-flex h-10 w-full items-center justify-center rounded-full px-4 text-sm font-medium leading-none transition-colors',
60
+ 'inline-flex h-10 items-center justify-center rounded-full px-4 text-sm font-medium leading-none transition-colors',
61
+ fullWidth && 'w-full',
62
+ !standalone && 'mt-2',
44
63
  BUTTON_CLASS_BY_VARIANT[variant][emphasis]
45
64
  )
46
65
 
@@ -0,0 +1,364 @@
1
+ import type { Meta, StoryFn } from '@storybook/react'
2
+ import React from 'react'
3
+
4
+ import type { RichCardActionButton, RichCardBaseProps } from './types'
5
+
6
+ import RichCard from '.'
7
+
8
+ const IMAGE = '/image-thumbnail.jpg'
9
+ const IMAGE_2 = '/video-thumbnail.jpg'
10
+ const IMAGE_3 = '/document-thumbnail.jpg'
11
+
12
+ const meta: Meta = {
13
+ title: 'RichCard',
14
+ parameters: {
15
+ layout: 'fullscreen',
16
+ // Matrix canvases are design-review surfaces, not regression targets — a
17
+ // single-pixel change in one cell would diff the whole grid. Default off;
18
+ // the stories that ARE regression targets opt back in per-story below.
19
+ chromatic: { disableSnapshot: true },
20
+ },
21
+ }
22
+ export default meta
23
+
24
+ /* ── Story scaffold (mirrors LinkAttachment.stories) ─────────────────── */
25
+
26
+ const Table = ({ children }: { children: React.ReactNode }) => (
27
+ <div className="min-h-screen w-full bg-[#F9F7F4] p-12">
28
+ <table className="border-separate border-spacing-4">{children}</table>
29
+ </div>
30
+ )
31
+
32
+ const TableHead = ({ columns }: { columns: string[] }) => (
33
+ <thead>
34
+ <tr>
35
+ <th className="pb-2 text-left text-xs font-medium text-black/40" />
36
+ {columns.map((column) => (
37
+ <th
38
+ key={column}
39
+ className="pb-2 text-left text-xs font-medium text-black/40"
40
+ >
41
+ {column}
42
+ </th>
43
+ ))}
44
+ </tr>
45
+ </thead>
46
+ )
47
+
48
+ const RowLabel = ({ children }: { children: React.ReactNode }) => (
49
+ <td className="pr-4 pt-2 text-right align-top text-xs font-medium text-black/40">
50
+ {children}
51
+ </td>
52
+ )
53
+
54
+ const Cell = ({ children }: { children: React.ReactNode }) => (
55
+ <td className="align-top">{children}</td>
56
+ )
57
+
58
+ /* ── Shared fixtures ─────────────────────────────────────────────────── */
59
+
60
+ const primary = (label: string): RichCardActionButton => ({
61
+ label,
62
+ variant: 'primary',
63
+ onClick: () => alert(`Tapped ${label}`),
64
+ })
65
+ const secondaryLink = (label: string, url: string): RichCardActionButton => ({
66
+ label,
67
+ variant: 'secondary',
68
+ href: url,
69
+ })
70
+
71
+ // The three canonical surfaces the card converges (interactive-surfaces.md).
72
+ const OFFICIAL_WELCOME: RichCardBaseProps = {
73
+ title: 'Set up your welcome message',
74
+ subtitle: 'Automatically greet new followers the moment they message you.',
75
+ images: [IMAGE],
76
+ imagePresentation: 'hero',
77
+ actions: [primary('Set up welcome message')],
78
+ }
79
+ const SINGLE_LINK: RichCardBaseProps = {
80
+ title: 'Check out my new drop',
81
+ subtitle: 'Fresh merch, live now — grab it before it’s gone.',
82
+ images: [IMAGE],
83
+ imagePresentation: 'hero',
84
+ actions: [secondaryLink('Shop now', 'tr.ee/briedrop')],
85
+ }
86
+ const COLLECTION: RichCardBaseProps = {
87
+ title: 'My summer collection',
88
+ subtitle: 'Three new looks, styled and ready.',
89
+ images: [IMAGE, IMAGE_2, IMAGE_3],
90
+ imagePresentation: 'fan',
91
+ actions: [secondaryLink('View collection', 'tr.ee/briesummer')],
92
+ }
93
+
94
+ const bothSides = (props: RichCardBaseProps) => (
95
+ <>
96
+ <Cell>
97
+ <RichCard.Received {...props} />
98
+ </Cell>
99
+ <Cell>
100
+ <RichCard.Sent {...props} />
101
+ </Cell>
102
+ </>
103
+ )
104
+
105
+ /* ── Instances — the headline: one card, three surfaces ──────────────── */
106
+
107
+ /**
108
+ * The three real surfaces that converge on `linktree_rich_card`, each shown
109
+ * Received (light) and Sent (dark). They differ only by knobs: the official
110
+ * welcome card is `hero + primary`; a single-link broadcast is `hero +
111
+ * secondary`; a collection is `fan + secondary`.
112
+ */
113
+ export const Instances: StoryFn = () => (
114
+ <Table>
115
+ <TableHead columns={['Received (light)', 'Sent (dark)']} />
116
+ <tbody>
117
+ <tr>
118
+ <RowLabel>Official welcome — hero · primary</RowLabel>
119
+ {bothSides(OFFICIAL_WELCOME)}
120
+ </tr>
121
+ <tr>
122
+ <RowLabel>Single link — hero · secondary</RowLabel>
123
+ {bothSides(SINGLE_LINK)}
124
+ </tr>
125
+ <tr>
126
+ <RowLabel>Collection — fan · secondary</RowLabel>
127
+ {bothSides(COLLECTION)}
128
+ </tr>
129
+ </tbody>
130
+ </Table>
131
+ )
132
+ Instances.parameters = { chromatic: { disableSnapshot: false } }
133
+
134
+ /* ── Image presentation — hero / fan / count default ─────────────────── */
135
+
136
+ const PRESENTATION_BASE: RichCardBaseProps = {
137
+ title: 'My summer collection',
138
+ subtitle: 'Three new looks, styled and ready.',
139
+ actions: [secondaryLink('View collection', 'tr.ee/briesummer')],
140
+ }
141
+
142
+ /**
143
+ * The `imagePresentation` knob: `hero` (one prominent image), `fan` (2–3 fanned
144
+ * thumbnails), and the count-derived default when the knob is omitted (≤1 →
145
+ * hero, ≥2 → fan). Shown on both surfaces.
146
+ */
147
+ export const ImagePresentation: StoryFn = () => (
148
+ <Table>
149
+ <TableHead
150
+ columns={[
151
+ 'hero (1)',
152
+ 'fan (2)',
153
+ 'fan (3)',
154
+ 'default — 1 img',
155
+ 'default — 2 imgs',
156
+ ]}
157
+ />
158
+ <tbody>
159
+ {(['Received', 'Sent'] as const).map((side) => {
160
+ const Card = RichCard[side]
161
+ return (
162
+ <tr key={side}>
163
+ <RowLabel>{side}</RowLabel>
164
+ <Cell>
165
+ <Card {...PRESENTATION_BASE} images={[IMAGE]} imagePresentation="hero" />
166
+ </Cell>
167
+ <Cell>
168
+ <Card
169
+ {...PRESENTATION_BASE}
170
+ images={[IMAGE, IMAGE_2]}
171
+ imagePresentation="fan"
172
+ />
173
+ </Cell>
174
+ <Cell>
175
+ <Card
176
+ {...PRESENTATION_BASE}
177
+ images={[IMAGE, IMAGE_2, IMAGE_3]}
178
+ imagePresentation="fan"
179
+ />
180
+ </Cell>
181
+ <Cell>
182
+ <Card {...PRESENTATION_BASE} images={[IMAGE]} />
183
+ </Cell>
184
+ <Cell>
185
+ <Card {...PRESENTATION_BASE} images={[IMAGE, IMAGE_2]} />
186
+ </Cell>
187
+ </tr>
188
+ )
189
+ })}
190
+ </tbody>
191
+ </Table>
192
+ )
193
+ ImagePresentation.parameters = { chromatic: { disableSnapshot: false } }
194
+
195
+ /* ── Footer layout — variant drives stacked vs inline ────────────────── */
196
+
197
+ const FOOTER_BASE: RichCardBaseProps = {
198
+ title: 'A note from Brie',
199
+ subtitle: 'Pick how you’d like to keep in touch.',
200
+ images: [IMAGE],
201
+ imagePresentation: 'hero',
202
+ }
203
+
204
+ /**
205
+ * The footer layout is the visual face of the **first** action's `variant`:
206
+ * `primary` → a stacked full-width button; `secondary` → an inline compact row.
207
+ * Shown single-action (today's case) and two-action (future-proof), on both
208
+ * surfaces.
209
+ */
210
+ export const FooterLayout: StoryFn = () => (
211
+ <Table>
212
+ <TableHead
213
+ columns={[
214
+ 'primary — single',
215
+ 'secondary — single',
216
+ 'primary — two',
217
+ 'secondary — two',
218
+ ]}
219
+ />
220
+ <tbody>
221
+ {(['Received', 'Sent'] as const).map((side) => {
222
+ const Card = RichCard[side]
223
+ return (
224
+ <tr key={side}>
225
+ <RowLabel>{side}</RowLabel>
226
+ <Cell>
227
+ <Card {...FOOTER_BASE} actions={[primary('Set up welcome message')]} />
228
+ </Cell>
229
+ <Cell>
230
+ <Card
231
+ {...FOOTER_BASE}
232
+ actions={[secondaryLink('Shop now', 'tr.ee/x')]}
233
+ />
234
+ </Cell>
235
+ <Cell>
236
+ <Card
237
+ {...FOOTER_BASE}
238
+ actions={[primary('Set up now'), primary('Maybe later')]}
239
+ />
240
+ </Cell>
241
+ <Cell>
242
+ <Card
243
+ {...FOOTER_BASE}
244
+ actions={[
245
+ secondaryLink('Shop', 'tr.ee/x'),
246
+ secondaryLink('Share', 'tr.ee/y'),
247
+ ]}
248
+ />
249
+ </Cell>
250
+ </tr>
251
+ )
252
+ })}
253
+ </tbody>
254
+ </Table>
255
+ )
256
+ FooterLayout.parameters = { chromatic: { disableSnapshot: false } }
257
+
258
+ /* ── Accent chin — hero-only derivation + gate ───────────────────────── */
259
+
260
+ const ACCENTS: Array<{ label: string; accentColor?: string }> = [
261
+ { label: '#7008E7', accentColor: '#7008E7' },
262
+ { label: '#738883', accentColor: '#738883' },
263
+ { label: 'none', accentColor: undefined },
264
+ { label: 'malformed', accentColor: 'rebeccapurple' },
265
+ ]
266
+
267
+ /**
268
+ * Accent derivation reuses LinkAttachment's `useChinPalette`: a validated hex
269
+ * paints the card + tail surface, hue kept and lightness swapped to a WCAG-safe
270
+ * stop; `none` / malformed keep the plain fill. It is gated to a single on-
271
+ * screen `hero` image — the bottom row shows a `fan` card ignoring the accent.
272
+ */
273
+ export const AccentChin: StoryFn = () => (
274
+ <Table>
275
+ <TableHead columns={ACCENTS.map(({ label }) => label)} />
276
+ <tbody>
277
+ <tr>
278
+ <RowLabel>Sent (last in run)</RowLabel>
279
+ {ACCENTS.map(({ label, accentColor }) => (
280
+ <Cell key={label}>
281
+ <RichCard.Sent {...SINGLE_LINK} accentColor={accentColor} />
282
+ </Cell>
283
+ ))}
284
+ </tr>
285
+ <tr>
286
+ <RowLabel>Received</RowLabel>
287
+ {ACCENTS.map(({ label, accentColor }) => (
288
+ <Cell key={label}>
289
+ <RichCard.Received {...SINGLE_LINK} accentColor={accentColor} />
290
+ </Cell>
291
+ ))}
292
+ </tr>
293
+ <tr>
294
+ <RowLabel>fan — accent ignored</RowLabel>
295
+ {ACCENTS.map(({ label, accentColor }) => (
296
+ <Cell key={label}>
297
+ <RichCard.Sent {...COLLECTION} accentColor={accentColor} />
298
+ </Cell>
299
+ ))}
300
+ </tr>
301
+ </tbody>
302
+ </Table>
303
+ )
304
+ AccentChin.parameters = { chromatic: { disableSnapshot: false } }
305
+
306
+ /* ── Content edges — clamps, missing pieces, tailless ────────────────── */
307
+
308
+ const LONG_TITLE =
309
+ 'Set up your welcome message so every new follower gets greeted the moment they say hello'
310
+ const LONG_SUBTITLE =
311
+ 'Thanks for reaching out! This is the kind of longer prose body an official message carries — it runs well past a single line and should wrap up to the clamp instead of clipping like a link preview, then trail off with an ellipsis once it reaches the limit.'
312
+
313
+ /**
314
+ * Edge cases: a title past two lines (clamps at 2), a long prose subtitle
315
+ * (clamps at the 3-line default), an image-less card (chin only, content-sized),
316
+ * a title-only card, an action-only card, and a mid-run card (no bubble tail).
317
+ */
318
+ export const ContentEdges: StoryFn = () => (
319
+ <Table>
320
+ <TableHead
321
+ columns={[
322
+ 'long title',
323
+ 'long subtitle',
324
+ 'no image',
325
+ 'title only',
326
+ 'mid-run (no tail)',
327
+ ]}
328
+ />
329
+ <tbody>
330
+ {(['Received', 'Sent'] as const).map((side) => {
331
+ const Card = RichCard[side]
332
+ return (
333
+ <tr key={side}>
334
+ <RowLabel>{side}</RowLabel>
335
+ <Cell>
336
+ <Card
337
+ {...OFFICIAL_WELCOME}
338
+ title={LONG_TITLE}
339
+ subtitle="Short subtitle."
340
+ />
341
+ </Cell>
342
+ <Cell>
343
+ <Card {...OFFICIAL_WELCOME} subtitle={LONG_SUBTITLE} />
344
+ </Cell>
345
+ <Cell>
346
+ <Card
347
+ title="No image, just copy"
348
+ subtitle="An image-less rich card sizes to its content instead of the 250px box."
349
+ actions={[primary('Got it')]}
350
+ />
351
+ </Cell>
352
+ <Cell>
353
+ <Card {...OFFICIAL_WELCOME} subtitle={undefined} actions={undefined} />
354
+ </Cell>
355
+ <Cell>
356
+ <Card {...OFFICIAL_WELCOME} groupPosition="middle" />
357
+ </Cell>
358
+ </tr>
359
+ )
360
+ })}
361
+ </tbody>
362
+ </Table>
363
+ )
364
+ ContentEdges.parameters = { chromatic: { disableSnapshot: false } }