import type { Meta, StoryFn } from '@storybook/react'
import React from 'react'
import type { RichCardActionButton, RichCardBaseProps } from './types'
import RichCard from '.'
const IMAGE = '/image-thumbnail.jpg'
const IMAGE_2 = '/video-thumbnail.jpg'
const IMAGE_3 = '/document-thumbnail.jpg'
const meta: Meta = {
title: 'RichCard',
parameters: {
layout: 'fullscreen',
// Matrix canvases are design-review surfaces, not regression targets — a
// single-pixel change in one cell would diff the whole grid. Default off;
// the stories that ARE regression targets opt back in per-story below.
chromatic: { disableSnapshot: true },
},
}
export default meta
/* ── Story scaffold (mirrors LinkAttachment.stories) ─────────────────── */
const Table = ({ children }: { children: React.ReactNode }) => (
)
const TableHead = ({ columns }: { columns: string[] }) => (
|
{columns.map((column) => (
{column}
|
))}
)
const RowLabel = ({ children }: { children: React.ReactNode }) => (
{children}
|
)
const Cell = ({ children }: { children: React.ReactNode }) => (
{children} |
)
/* ── Shared fixtures ─────────────────────────────────────────────────── */
const primary = (label: string): RichCardActionButton => ({
label,
variant: 'primary',
onClick: () => alert(`Tapped ${label}`),
})
const secondaryLink = (label: string, url: string): RichCardActionButton => ({
label,
variant: 'secondary',
href: url,
})
const secondary = (label: string): RichCardActionButton => ({
label,
variant: 'secondary',
onClick: () => alert(`Tapped ${label}`),
})
// The three canonical surfaces the card converges (interactive-surfaces.md).
const OFFICIAL_WELCOME: RichCardBaseProps = {
title: 'Set up your welcome message',
subtitle: 'Automatically greet new followers the moment they message you.',
images: [IMAGE],
imagePresentation: 'hero',
actions: [primary('Set up welcome message')],
}
const SINGLE_LINK: RichCardBaseProps = {
title: 'Check out my new drop',
subtitle: 'Fresh merch, live now — grab it before it’s gone.',
images: [IMAGE],
imagePresentation: 'hero',
actions: [secondaryLink('Shop now', 'tr.ee/briedrop')],
}
const COLLECTION: RichCardBaseProps = {
title: 'My summer collection',
subtitle: 'Three new looks, styled and ready.',
images: [IMAGE, IMAGE_2, IMAGE_3],
imagePresentation: 'fan',
actions: [secondaryLink('View collection', 'tr.ee/briesummer')],
}
const NEWLINE_SUBTITLE: RichCardBaseProps = {
...OFFICIAL_WELCOME,
title: 'Start every conversation with a personal welcome message!',
subtitle:
'👋 Say hello as people arrive\n✨ Spark conversations\n💬 Add automated FAQs',
subtitleLines: 6,
}
const bothSides = (props: RichCardBaseProps) => (
<>
|
|
>
)
/* ── Instances — the headline: one card, three surfaces ──────────────── */
/**
* The three real surfaces that converge on `linktree_rich_card`, each shown
* Received (light) and Sent (dark). They differ only by knobs: the official
* welcome card is `hero + primary`; a single-link broadcast is `hero +
* secondary`; a collection is `fan + secondary`. A fourth row shows a
* newline-separated subtitle (the onboarding result-card shape).
*/
export const Instances: StoryFn = () => (
Official welcome — hero · primary
{bothSides(OFFICIAL_WELCOME)}
Single link — hero · secondary
{bothSides(SINGLE_LINK)}
Collection — fan · secondary
{bothSides(COLLECTION)}
Subtitle with newlines
{bothSides(NEWLINE_SUBTITLE)}
)
Instances.parameters = { chromatic: { disableSnapshot: false } }
/* ── Image presentation — hero / fan / count default ─────────────────── */
const PRESENTATION_BASE: RichCardBaseProps = {
title: 'My summer collection',
subtitle: 'Three new looks, styled and ready.',
actions: [secondaryLink('View collection', 'tr.ee/briesummer')],
}
/**
* The `imagePresentation` knob: `hero` (one prominent image), `fan` (2–3 fanned
* thumbnails), and the count-derived default when the knob is omitted (≤1 →
* hero, ≥2 → fan). Shown on both surfaces.
*/
export const ImagePresentation: StoryFn = () => (
{(['Received', 'Sent'] as const).map((side) => {
const Card = RichCard[side]
return (
{side}
|
|
|
|
|
|
)
})}
)
ImagePresentation.parameters = { chromatic: { disableSnapshot: false } }
/* ── Footer layout — variant drives stacked vs inline ────────────────── */
const FOOTER_BASE: RichCardBaseProps = {
title: 'A note from Brie',
subtitle: 'Pick how you’d like to keep in touch.',
images: [IMAGE],
imagePresentation: 'hero',
}
/**
* Footer emphasis + layout. A card has **one primary (main) action**; any
* further action is `secondary`, so the hierarchy is carried by emphasis
* (filled main vs subtle alternate) — not by two identical buttons. The first
* action's variant also drives layout: `primary` → stacked full-width;
* `secondary` → inline compact row.
*
* The last column is the anti-pattern (two primaries) — no hierarchy; author
* `primary + secondary` instead.
*/
export const FooterLayout: StoryFn = () => (
{(['Received', 'Sent'] as const).map((side) => {
const Card = RichCard[side]
return (
{side}
|
|
|
|
|
)
})}
)
FooterLayout.parameters = { chromatic: { disableSnapshot: false } }
/* ── Accent chin — hero-only derivation + gate ───────────────────────── */
const ACCENTS: Array<{ label: string; accentColor?: string }> = [
{ label: '#7008E7', accentColor: '#7008E7' },
{ label: '#738883', accentColor: '#738883' },
{ label: 'none', accentColor: undefined },
{ label: 'malformed', accentColor: 'rebeccapurple' },
]
/**
* Accent derivation reuses LinkAttachment's `useChinPalette`: a validated hex
* paints the card + tail surface, hue kept and lightness swapped to a WCAG-safe
* stop; `none` / malformed keep the plain fill. It is gated to a single on-
* screen `hero` image — the bottom row shows a `fan` card ignoring the accent.
*/
export const AccentChin: StoryFn = () => (
label)} />
Sent (last in run)
{ACCENTS.map(({ label, accentColor }) => (
|
|
))}
Received
{ACCENTS.map(({ label, accentColor }) => (
|
|
))}
fan — accent ignored
{ACCENTS.map(({ label, accentColor }) => (
|
|
))}
)
AccentChin.parameters = { chromatic: { disableSnapshot: false } }
/* ── Content edges — clamps, missing pieces, tailless ────────────────── */
const LONG_TITLE =
'Set up your welcome message so every new follower gets greeted the moment they say hello'
const LONG_SUBTITLE =
'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.'
/**
* Edge cases: a title past two lines (clamps at 2), a long prose subtitle
* (clamps at the 3-line default), an image-less card (chin only, content-sized),
* a title-only card, an action-only card, and a mid-run card (no bubble tail).
*/
export const ContentEdges: StoryFn = () => (
{(['Received', 'Sent'] as const).map((side) => {
const Card = RichCard[side]
return (
{side}
|
|
|
|
|
|
)
})}
)
ContentEdges.parameters = { chromatic: { disableSnapshot: false } }