@linktr.ee/messaging-react 3.31.1 → 3.31.2-rc-1785926578
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/{Card-EWN07XIl.cjs → Card-CF1TZXD5.cjs} +2 -2
- package/dist/{Card-EWN07XIl.cjs.map → Card-CF1TZXD5.cjs.map} +1 -1
- package/dist/{Card-CmlaSw7i.js → Card-CqYFVF6m.js} +2 -2
- package/dist/{Card-CmlaSw7i.js.map → Card-CqYFVF6m.js.map} +1 -1
- package/dist/assets/index.css +1 -1
- package/dist/index-Ct_Jgy8P.cjs +2 -0
- package/dist/index-Ct_Jgy8P.cjs.map +1 -0
- package/dist/index-ilHx7pIO.js +5672 -0
- package/dist/index-ilHx7pIO.js.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +6 -5
- package/dist/index.js +1 -1
- package/package.json +3 -5
- package/src/components/LinkAttachment/LinkAttachment.stories.tsx +42 -27
- package/src/components/LinkAttachment/LinkAttachment.test.tsx +46 -39
- package/src/components/LinkAttachment/components/Composer/Card.tsx +1 -1
- package/src/components/LinkAttachment/components/Received/Card.tsx +2 -2
- package/src/components/LinkAttachment/components/Sent/Card.tsx +2 -2
- package/src/components/LinkAttachment/components/_shared/BubbleTail.tsx +16 -6
- package/src/components/LinkAttachment/components/_shared/CardBody.tsx +4 -5
- package/src/components/LinkAttachment/components/_shared/CardShell.tsx +27 -19
- package/src/components/LinkAttachment/components/_shared/hex.ts +10 -12
- package/src/components/LinkAttachment/components/_shared/useChinPalette.ts +45 -48
- package/src/components/LinkAttachment/types.ts +6 -5
- package/src/styles.css +41 -4
- package/dist/index-CN3errpB.js +0 -7828
- package/dist/index-CN3errpB.js.map +0 -1
- package/dist/index-CcitzVoW.cjs +0 -5
- package/dist/index-CcitzVoW.cjs.map +0 -1
- package/src/components/LinkAttachment/components/_shared/accentSurface.test.ts +0 -135
- package/src/components/LinkAttachment/components/_shared/accentSurface.ts +0 -95
- package/src/components/LinkAttachment/components/_shared/chinContrast.test.ts +0 -95
- package/src/components/LinkAttachment/components/_shared/chinContrast.ts +0 -79
package/dist/index.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("./index-
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("./index-Ct_Jgy8P.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;
|
|
2
2
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.d.ts
CHANGED
|
@@ -696,16 +696,17 @@ export declare interface LinkAttachmentBaseProps {
|
|
|
696
696
|
/**
|
|
697
697
|
* Dominant colour extracted from `thumbnailUrl`, as a hex string
|
|
698
698
|
* (`#RRGGBB` or `#RGB`). Paints the card and — when the card is the last
|
|
699
|
-
* bubble in a same-author run — its tail.
|
|
700
|
-
*
|
|
699
|
+
* bubble in a same-author run — its tail. Its hue is kept while the lightness
|
|
700
|
+
* is swapped to a fixed per-side stop (`oklch(from …)` in `styles.css`); the
|
|
701
|
+
* stops clear WCAG AA at every hue by construction, so callers never pass a
|
|
701
702
|
* text colour.
|
|
702
703
|
*
|
|
703
704
|
* Only painted while the hero image it was extracted from is actually on
|
|
704
705
|
* screen. Omitted, unparseable, `layout: 'classic'`, no `thumbnailUrl`,
|
|
705
706
|
* `status` other than `'ready'`, a playable-audio `mimeType` + `sourceUrl`
|
|
706
|
-
* (whose hero is the native player rather than an image), or a hero that
|
|
707
|
-
*
|
|
708
|
-
*
|
|
707
|
+
* (whose hero is the native player rather than an image), or a hero that fails
|
|
708
|
+
* to load => the card keeps the plain bubble fill. Browsers without
|
|
709
|
+
* relative-colour-syntax support also keep that fill (progressive enhancement).
|
|
709
710
|
*/
|
|
710
711
|
accentColor?: string;
|
|
711
712
|
/**
|
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-
|
|
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-ilHx7pIO.js";
|
|
2
2
|
export {
|
|
3
3
|
e as ActionButton,
|
|
4
4
|
t as Avatar,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linktr.ee/messaging-react",
|
|
3
|
-
"version": "3.31.
|
|
3
|
+
"version": "3.31.2-rc-1785926578",
|
|
4
4
|
"description": "React messaging components built on messaging-core for web applications",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.cjs",
|
|
@@ -50,10 +50,9 @@
|
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
52
|
"@linktr.ee/component-library": "11.8.6",
|
|
53
|
-
"@linktr.ee/messaging-core": "
|
|
53
|
+
"@linktr.ee/messaging-core": "2.4.0-rc-1785926578",
|
|
54
54
|
"@linktr.ee/messaging-taxonomy": "^0.5.1",
|
|
55
|
-
"@phosphor-icons/react": "^2.1.10"
|
|
56
|
-
"culori": "^4.0.2"
|
|
55
|
+
"@phosphor-icons/react": "^2.1.10"
|
|
57
56
|
},
|
|
58
57
|
"devDependencies": {
|
|
59
58
|
"@storybook/addon-essentials": "^8.5.0",
|
|
@@ -67,7 +66,6 @@
|
|
|
67
66
|
"@testing-library/jest-dom": "^6.1.5",
|
|
68
67
|
"@testing-library/react": "^14.1.2",
|
|
69
68
|
"@testing-library/user-event": "^14.5.1",
|
|
70
|
-
"@types/culori": "^4",
|
|
71
69
|
"@types/react": "^18.3.24",
|
|
72
70
|
"@types/react-dom": "^18.3.7",
|
|
73
71
|
"@vitejs/plugin-react": "^4.2.1",
|
|
@@ -7,9 +7,8 @@ import {
|
|
|
7
7
|
import type { Meta, StoryFn } from '@storybook/react'
|
|
8
8
|
import React from 'react'
|
|
9
9
|
|
|
10
|
-
import { accentSurface } from './components/_shared/accentSurface'
|
|
11
10
|
import type { LinkAttachmentVariant } from './components/_shared/CardShell'
|
|
12
|
-
import {
|
|
11
|
+
import { HEX_PATTERN, toCssHex } from './components/_shared/hex'
|
|
13
12
|
|
|
14
13
|
import LinkAttachment from '.'
|
|
15
14
|
|
|
@@ -388,9 +387,9 @@ const AUDIO_PREVIEW = {
|
|
|
388
387
|
* player. The artwork is still handed over as `thumbnailUrl` and there is
|
|
389
388
|
* still no `<img>`, so `onImageError` can never clear the accent — the gate
|
|
390
389
|
* has to be up front. These three cells are the regression that prompted the
|
|
391
|
-
* rule:
|
|
392
|
-
*
|
|
393
|
-
*
|
|
390
|
+
* rule: the derived surface outranks the `bg-[#F2F3F4]` audio fill, so a
|
|
391
|
+
* purple card would ship around a native player with the artwork nowhere in
|
|
392
|
+
* sight.
|
|
394
393
|
* - **No `thumbnailUrl`** leaves the type-icon placeholder, which is not an
|
|
395
394
|
* image the accent could have come from. Stream reaches this whenever a link
|
|
396
395
|
* preview carries a playable clip but no `image_url`.
|
|
@@ -462,8 +461,8 @@ AccentWithoutVisibleHero.parameters = { chromatic: { disableSnapshot: false } }
|
|
|
462
461
|
|
|
463
462
|
/**
|
|
464
463
|
* Max-chroma sRGB colour for a hue — three phase-shifted cosines, so stepping
|
|
465
|
-
* the hue walks the wheel at the most saturated triple each hue has.
|
|
466
|
-
* the
|
|
464
|
+
* the hue walks the wheel at the most saturated triple each hue has. Many of
|
|
465
|
+
* these sit outside sRGB at the surface stops, where the browser gamut-maps.
|
|
467
466
|
*/
|
|
468
467
|
const vividHex = (hue: number): string =>
|
|
469
468
|
`#${[0, 120, 240]
|
|
@@ -479,7 +478,7 @@ const HUE_SWEEP = Array.from({ length: 24 }, (_, i) => ({
|
|
|
479
478
|
accentColor: vividHex(i * 15),
|
|
480
479
|
}))
|
|
481
480
|
|
|
482
|
-
// Neutrals and near-limits where the
|
|
481
|
+
// Neutrals and near-limits where the ink and the fallback actually decide
|
|
483
482
|
// something, plus the two inputs that must never derive a surface.
|
|
484
483
|
const EDGE_SAMPLES: Array<{ label: string; accentColor?: string }> = [
|
|
485
484
|
{ label: 'mock #738883', accentColor: '#738883' },
|
|
@@ -492,13 +491,24 @@ const EDGE_SAMPLES: Array<{ label: string; accentColor?: string }> = [
|
|
|
492
491
|
{ label: 'malformed', accentColor: 'rebeccapurple' },
|
|
493
492
|
]
|
|
494
493
|
|
|
494
|
+
// Ink is fixed per side — white on the dark stop, near-black on the pale stop.
|
|
495
|
+
const INK_BY_VARIANT: Record<LinkAttachmentVariant, string> = {
|
|
496
|
+
dark: '#FFFFFF',
|
|
497
|
+
light: '#040403',
|
|
498
|
+
}
|
|
499
|
+
// The plain bubble fill each side falls back to, as a self-contained arbitrary
|
|
500
|
+
// value (mirrors `CardShell`). The accent rule in `styles.css` paints over it.
|
|
501
|
+
const FALLBACK_BG_CLASS: Record<LinkAttachmentVariant, string> = {
|
|
502
|
+
dark: 'bg-[#1e2330]',
|
|
503
|
+
light: 'bg-[#f1f0ee]',
|
|
504
|
+
}
|
|
505
|
+
|
|
495
506
|
/**
|
|
496
|
-
* One derivation cell
|
|
497
|
-
*
|
|
498
|
-
* `
|
|
499
|
-
*
|
|
500
|
-
*
|
|
501
|
-
* fallback fill, tagged.
|
|
507
|
+
* One derivation cell driven by the *same* CSS the card uses: a
|
|
508
|
+
* `.messaging-link-card` publishing `--accent`, so `styles.css` derives the
|
|
509
|
+
* surface via `oklch(from …)`. A refused accent (`none` / `malformed`) publishes
|
|
510
|
+
* nothing and shows the fallback fill. The resolved surface hex lives only in
|
|
511
|
+
* the browser, so the cell labels its input.
|
|
502
512
|
*/
|
|
503
513
|
const DerivedSwatch = ({
|
|
504
514
|
accentColor,
|
|
@@ -507,17 +517,23 @@ const DerivedSwatch = ({
|
|
|
507
517
|
accentColor?: string
|
|
508
518
|
variant: LinkAttachmentVariant
|
|
509
519
|
}) => {
|
|
510
|
-
const
|
|
511
|
-
|
|
520
|
+
const accentHex =
|
|
521
|
+
accentColor != null && HEX_PATTERN.test(accentColor.trim())
|
|
522
|
+
? toCssHex(accentColor)
|
|
523
|
+
: undefined
|
|
512
524
|
return (
|
|
513
525
|
<div
|
|
514
|
-
className=
|
|
515
|
-
|
|
526
|
+
className={`messaging-link-card flex h-12 flex-col justify-between rounded-md px-2 py-1.5 text-[11px] font-medium ${FALLBACK_BG_CLASS[variant]}`}
|
|
527
|
+
data-variant={variant}
|
|
528
|
+
style={
|
|
529
|
+
{
|
|
530
|
+
color: INK_BY_VARIANT[variant],
|
|
531
|
+
...(accentHex ? { '--accent': accentHex } : {}),
|
|
532
|
+
} as React.CSSProperties
|
|
533
|
+
}
|
|
516
534
|
>
|
|
517
535
|
<span>Yoga on Ice</span>
|
|
518
|
-
<span className="font-mono opacity-80">
|
|
519
|
-
{surface ?? `fallback ${painted}`}
|
|
520
|
-
</span>
|
|
536
|
+
<span className="font-mono opacity-80">{accentHex ?? 'fallback'}</span>
|
|
521
537
|
</div>
|
|
522
538
|
)
|
|
523
539
|
}
|
|
@@ -527,12 +543,11 @@ const GridHead = ({ children }: { children: React.ReactNode }) => (
|
|
|
527
543
|
)
|
|
528
544
|
|
|
529
545
|
/**
|
|
530
|
-
* Isolates the
|
|
531
|
-
*
|
|
532
|
-
*
|
|
533
|
-
*
|
|
534
|
-
*
|
|
535
|
-
* as both a colour change and a hex text diff across adjacent rows.
|
|
546
|
+
* Isolates the accent-chin surface: for every hue at maximum chroma and every
|
|
547
|
+
* neutral/edge input, the `dark` (Sent) and `light` (Received) surfaces CSS
|
|
548
|
+
* derives from the published `--accent`. One snapshot, no hero pixels — the
|
|
549
|
+
* dense diff target for the derivation. In-gamut inputs (the neutral/edge rows)
|
|
550
|
+
* are stable; the max-chroma hue-sweep rows depend on the browser's gamut map.
|
|
536
551
|
*/
|
|
537
552
|
export const AccentDerivation: StoryFn = () => (
|
|
538
553
|
<div className="min-h-screen w-full bg-[#F9F7F4] p-8">
|
|
@@ -119,28 +119,32 @@ describe('LinkAttachment accent chin (MES-1349, MES-1388)', () => {
|
|
|
119
119
|
thumbnailUrl: 'https://cdn.example.com/yoga.jpg',
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
122
|
+
// The colour derivation itself is CSS (`oklch(from …)` in styles.css) and is
|
|
123
|
+
// not computed by jsdom, so these tests assert the contract React owns: the
|
|
124
|
+
// raw extracted hex is published as `--accent` (and dropped when there is no
|
|
125
|
+
// visible hero), leaving the surface derivation to the browser.
|
|
126
|
+
const accentOf = (element: HTMLElement) =>
|
|
127
|
+
element.style.getPropertyValue('--accent')
|
|
128
|
+
|
|
129
|
+
it('publishes the accent for the sender and inks the chin white', async () => {
|
|
125
130
|
renderWithProviders(
|
|
126
131
|
<LinkAttachment.Sent {...CARD_PROPS} accentColor={ACCENT} />
|
|
127
132
|
)
|
|
128
133
|
const root = await screen.findByRole('link')
|
|
129
|
-
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
expect(
|
|
133
|
-
expect(root).not.toHaveStyle({ backgroundColor: ACCENT })
|
|
134
|
+
expect(accentOf(root)).toBe(ACCENT)
|
|
135
|
+
// No inline background: the surface is derived from `--accent` in CSS, the
|
|
136
|
+
// raw accent is never painted verbatim.
|
|
137
|
+
expect(root.style.backgroundColor).toBe('')
|
|
134
138
|
expect(screen.getByText(CARD_PROPS.title)).toHaveStyle({ color: '#FFFFFF' })
|
|
135
139
|
})
|
|
136
140
|
|
|
137
|
-
it('
|
|
141
|
+
it('publishes the accent for the receiver and inks the chin near-black', async () => {
|
|
138
142
|
renderWithProviders(
|
|
139
143
|
<LinkAttachment.Received {...CARD_PROPS} accentColor={ACCENT} />
|
|
140
144
|
)
|
|
141
145
|
const root = await screen.findByRole('link')
|
|
142
|
-
expect(
|
|
143
|
-
expect(root
|
|
146
|
+
expect(accentOf(root)).toBe(ACCENT)
|
|
147
|
+
expect(root.style.backgroundColor).toBe('')
|
|
144
148
|
expect(screen.getByText(CARD_PROPS.title)).toHaveStyle({ color: '#040403' })
|
|
145
149
|
})
|
|
146
150
|
|
|
@@ -151,13 +155,13 @@ describe('LinkAttachment accent chin (MES-1349, MES-1388)', () => {
|
|
|
151
155
|
const sent = renderWithProviders(
|
|
152
156
|
<LinkAttachment.Sent {...CARD_PROPS} accentColor={accentColor} />
|
|
153
157
|
)
|
|
154
|
-
expect(
|
|
158
|
+
expect(accentOf(await screen.findByRole('link'))).toBe(accentColor)
|
|
155
159
|
sent.unmount()
|
|
156
160
|
|
|
157
161
|
const received = renderWithProviders(
|
|
158
162
|
<LinkAttachment.Received {...CARD_PROPS} accentColor={accentColor} />
|
|
159
163
|
)
|
|
160
|
-
expect(
|
|
164
|
+
expect(accentOf(await screen.findByRole('link'))).toBe(accentColor)
|
|
161
165
|
received.unmount()
|
|
162
166
|
}
|
|
163
167
|
})
|
|
@@ -165,7 +169,7 @@ describe('LinkAttachment accent chin (MES-1349, MES-1388)', () => {
|
|
|
165
169
|
it('falls back to a self-contained fill, not a consumer theme token', async () => {
|
|
166
170
|
renderWithProviders(<LinkAttachment.Sent {...CARD_PROPS} />)
|
|
167
171
|
const root = await screen.findByRole('link')
|
|
168
|
-
expect(
|
|
172
|
+
expect(accentOf(root)).toBe('')
|
|
169
173
|
|
|
170
174
|
// The fill has to survive in consumers that do not share our Tailwind
|
|
171
175
|
// theme. This package ships no utilities, so consumers generate our classes
|
|
@@ -186,8 +190,8 @@ describe('LinkAttachment accent chin (MES-1349, MES-1388)', () => {
|
|
|
186
190
|
<LinkAttachment.Sent {...CARD_PROPS} accentColor="rebeccapurple" />
|
|
187
191
|
)
|
|
188
192
|
const root = await screen.findByRole('link')
|
|
189
|
-
expect(root
|
|
190
|
-
// Ink still comes from the
|
|
193
|
+
expect(accentOf(root)).toBe('')
|
|
194
|
+
// Ink still comes from the side — here the sender's fallback fill.
|
|
191
195
|
expect(screen.getByText(CARD_PROPS.title)).toHaveStyle({ color: '#FFFFFF' })
|
|
192
196
|
})
|
|
193
197
|
|
|
@@ -196,12 +200,12 @@ describe('LinkAttachment accent chin (MES-1349, MES-1388)', () => {
|
|
|
196
200
|
<LinkAttachment.Sent {...CARD_PROPS} accentColor={ACCENT} />
|
|
197
201
|
)
|
|
198
202
|
const root = await screen.findByRole('link')
|
|
199
|
-
expect(root
|
|
203
|
+
expect(accentOf(root)).toBe(ACCENT)
|
|
200
204
|
|
|
201
205
|
// A surface derived from an image the viewer cannot see is a lie, so the
|
|
202
206
|
// card falls all the way back rather than keeping a colour with no source.
|
|
203
207
|
fireEvent.error(screen.getByRole('img'))
|
|
204
|
-
expect(root
|
|
208
|
+
expect(accentOf(root)).toBe('')
|
|
205
209
|
expect(screen.getByText(CARD_PROPS.title)).toHaveStyle({ color: '#FFFFFF' })
|
|
206
210
|
})
|
|
207
211
|
|
|
@@ -222,12 +226,12 @@ describe('LinkAttachment accent chin (MES-1349, MES-1388)', () => {
|
|
|
222
226
|
const shell = container.querySelector(
|
|
223
227
|
'[class*="select-none"]'
|
|
224
228
|
) as HTMLElement
|
|
225
|
-
expect(shell.style.
|
|
229
|
+
expect(shell.style.getPropertyValue('--accent')).toBe(ACCENT)
|
|
226
230
|
expect(container.querySelector('video')).toHaveAttribute('poster')
|
|
227
231
|
|
|
228
232
|
fireEvent.error(container.querySelector('img') as HTMLImageElement)
|
|
229
233
|
|
|
230
|
-
expect(shell.style.
|
|
234
|
+
expect(shell.style.getPropertyValue('--accent')).toBe('')
|
|
231
235
|
expect(screen.getByText(CARD_PROPS.title)).toHaveStyle({ color: '#FFFFFF' })
|
|
232
236
|
// The failed poster is dropped with the accent, so the player falls back to
|
|
233
237
|
// its own first frame instead of retrying a 404.
|
|
@@ -252,7 +256,7 @@ describe('LinkAttachment accent chin (MES-1349, MES-1388)', () => {
|
|
|
252
256
|
<LinkAttachment.Sent {...CARD_PROPS} {...AUDIO} accentColor={ACCENT} />
|
|
253
257
|
)
|
|
254
258
|
const sent = container.firstElementChild as HTMLElement
|
|
255
|
-
expect(sent.style.
|
|
259
|
+
expect(sent.style.getPropertyValue('--accent')).toBe('')
|
|
256
260
|
expect(sent.className).toContain(AUDIO_BG_CLASS)
|
|
257
261
|
expect(screen.getByText(CARD_PROPS.title)).toHaveStyle({ color: '#040403' })
|
|
258
262
|
expect(screen.queryByRole('img')).toBeNull()
|
|
@@ -266,7 +270,7 @@ describe('LinkAttachment accent chin (MES-1349, MES-1388)', () => {
|
|
|
266
270
|
/>
|
|
267
271
|
)
|
|
268
272
|
const received = await screen.findByTestId('link-attachment')
|
|
269
|
-
expect(received.style.
|
|
273
|
+
expect(received.style.getPropertyValue('--accent')).toBe('')
|
|
270
274
|
expect(received.className).toContain(AUDIO_BG_CLASS)
|
|
271
275
|
})
|
|
272
276
|
|
|
@@ -281,29 +285,27 @@ describe('LinkAttachment accent chin (MES-1349, MES-1388)', () => {
|
|
|
281
285
|
/>
|
|
282
286
|
)
|
|
283
287
|
const root = await screen.findByRole('link')
|
|
284
|
-
expect(root
|
|
288
|
+
expect(accentOf(root)).toBe('')
|
|
285
289
|
// Variant fill, asserted by shape not literal — see the self-contained-fill
|
|
286
290
|
// test above for why this must stay an arbitrary value (MES-1388).
|
|
287
291
|
expect(root.className).toMatch(/bg-\[/)
|
|
288
292
|
expect(screen.getByText(CARD_PROPS.title)).toHaveStyle({ color: '#FFFFFF' })
|
|
289
293
|
})
|
|
290
294
|
|
|
291
|
-
it('normalizes a hashless accent so the
|
|
295
|
+
it('normalizes a hashless accent so the derivation actually resolves', async () => {
|
|
292
296
|
// The extractor returns a bare hex often enough that dropping it would lose
|
|
293
|
-
// the colour silently — `
|
|
297
|
+
// the colour silently — `oklch(from 7008E7 …)` never resolves, so the
|
|
298
|
+
// published `--accent` must carry the `#`.
|
|
294
299
|
const hashless = renderWithProviders(
|
|
295
300
|
<LinkAttachment.Sent {...CARD_PROPS} accentColor="7008E7" />
|
|
296
301
|
)
|
|
297
|
-
|
|
298
|
-
expect(derived).not.toBe('')
|
|
302
|
+
expect(accentOf(await screen.findByRole('link'))).toBe('#7008E7')
|
|
299
303
|
hashless.unmount()
|
|
300
304
|
|
|
301
305
|
renderWithProviders(
|
|
302
306
|
<LinkAttachment.Sent {...CARD_PROPS} accentColor={ACCENT} />
|
|
303
307
|
)
|
|
304
|
-
expect((await screen.findByRole('link'))
|
|
305
|
-
derived
|
|
306
|
-
)
|
|
308
|
+
expect(accentOf(await screen.findByRole('link'))).toBe('#7008E7')
|
|
307
309
|
})
|
|
308
310
|
|
|
309
311
|
it('lets a loading status outrank the audio hero on all three cards', () => {
|
|
@@ -349,13 +351,15 @@ describe('LinkAttachment accent chin (MES-1349, MES-1388)', () => {
|
|
|
349
351
|
groupPosition="end"
|
|
350
352
|
/>
|
|
351
353
|
)
|
|
352
|
-
// The tail paints outside the shell's own box, so it
|
|
353
|
-
//
|
|
354
|
-
// would disagree.
|
|
354
|
+
// The tail paints outside the shell's own box, so it can't inherit the
|
|
355
|
+
// shell's computed background — it publishes the same `--accent` and derives
|
|
356
|
+
// the surface itself in CSS, or the tail and the card would disagree.
|
|
355
357
|
const card = await screen.findByRole('link')
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
358
|
+
const tail = screen.getByTestId('link-card-tail')
|
|
359
|
+
expect(tail.style.getPropertyValue('--accent')).toBe(
|
|
360
|
+
card.style.getPropertyValue('--accent')
|
|
361
|
+
)
|
|
362
|
+
expect(tail.style.getPropertyValue('--accent')).toBe(ACCENT)
|
|
359
363
|
unmount()
|
|
360
364
|
|
|
361
365
|
// Mid-run the following bubble owns the tail.
|
|
@@ -370,10 +374,13 @@ describe('LinkAttachment accent chin (MES-1349, MES-1388)', () => {
|
|
|
370
374
|
})
|
|
371
375
|
|
|
372
376
|
it('falls back to the variant fill for the tail when there is no accent', async () => {
|
|
377
|
+
// No accent published, so no `--accent`: the tail's `[data-variant='dark']`
|
|
378
|
+
// rule in styles.css paints the sender's plain `#1e2330` bubble fill (the
|
|
379
|
+
// computed value isn't observable in jsdom — see the note on `accentOf`).
|
|
373
380
|
renderWithProviders(<LinkAttachment.Sent {...CARD_PROPS} />)
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
381
|
+
const tail = await screen.findByTestId('link-card-tail')
|
|
382
|
+
expect(tail.style.getPropertyValue('--accent')).toBe('')
|
|
383
|
+
expect(tail).toHaveAttribute('data-variant', 'dark')
|
|
377
384
|
})
|
|
378
385
|
|
|
379
386
|
it('exposes the loading skeleton as a labelled busy region', async () => {
|
|
@@ -130,7 +130,7 @@ const ComposerCard: React.FC<ComposerCardProps> = ({
|
|
|
130
130
|
<CardShell
|
|
131
131
|
variant="dark"
|
|
132
132
|
fixedHeight={!isClassic}
|
|
133
|
-
|
|
133
|
+
accentHex={chin.accentHex}
|
|
134
134
|
busy={chin.isLoading}
|
|
135
135
|
topRight={isClassic ? dismissButton : undefined}
|
|
136
136
|
>
|
|
@@ -108,7 +108,7 @@ const ReceivedCard: React.FC<ReceivedCardProps> = ({
|
|
|
108
108
|
|
|
109
109
|
return (
|
|
110
110
|
<BubbleTail
|
|
111
|
-
|
|
111
|
+
accentHex={chin.accentHex}
|
|
112
112
|
side="receiver"
|
|
113
113
|
// Only the last card in a same-author run carries the tail; the audio
|
|
114
114
|
// player card is not a bubble, so it never does.
|
|
@@ -123,7 +123,7 @@ const ReceivedCard: React.FC<ReceivedCardProps> = ({
|
|
|
123
123
|
onClick={shellOnClick}
|
|
124
124
|
ariaLabel={title ?? 'Open attachment preview'}
|
|
125
125
|
fixedHeight={layout === 'featured' && audioBg == null}
|
|
126
|
-
|
|
126
|
+
accentHex={chin.accentHex}
|
|
127
127
|
busy={chin.isLoading}
|
|
128
128
|
bgClassName={audioBg}
|
|
129
129
|
data-testid="link-attachment"
|
|
@@ -92,7 +92,7 @@ const SentCard: React.FC<SentCardProps> = ({
|
|
|
92
92
|
|
|
93
93
|
return (
|
|
94
94
|
<BubbleTail
|
|
95
|
-
|
|
95
|
+
accentHex={chin.accentHex}
|
|
96
96
|
side="sender"
|
|
97
97
|
// Only the last card in a same-author run carries the tail; the audio
|
|
98
98
|
// player card is not a bubble, so it never does.
|
|
@@ -105,7 +105,7 @@ const SentCard: React.FC<SentCardProps> = ({
|
|
|
105
105
|
href={shellHref}
|
|
106
106
|
onClick={shellOnClick}
|
|
107
107
|
fixedHeight={layout === 'featured' && !isAudio}
|
|
108
|
-
|
|
108
|
+
accentHex={chin.accentHex}
|
|
109
109
|
busy={chin.isLoading}
|
|
110
110
|
bgClassName={isAudio ? AUDIO_BG_CLASS : undefined}
|
|
111
111
|
>
|
|
@@ -3,11 +3,13 @@ import React from 'react'
|
|
|
3
3
|
|
|
4
4
|
export interface BubbleTailProps {
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* Raw extracted accent colour, published as `--accent` so the tail derives
|
|
7
|
+
* the same surface as the card in CSS (`styles.css`), or `undefined` to fall
|
|
8
|
+
* back to this side's plain bubble fill. The tail paints outside the shell's
|
|
9
|
+
* box and can't inherit its background, so it runs the derivation off its own
|
|
10
|
+
* `--accent`.
|
|
9
11
|
*/
|
|
10
|
-
|
|
12
|
+
accentHex?: string
|
|
11
13
|
/** Sender tails hang off the right edge, receiver tails mirror to the left. */
|
|
12
14
|
side: 'sender' | 'receiver'
|
|
13
15
|
/**
|
|
@@ -30,13 +32,20 @@ export interface BubbleTailProps {
|
|
|
30
32
|
* tinted from the text-bubble surface would clash with a coloured card.
|
|
31
33
|
*/
|
|
32
34
|
const BubbleTail: React.FC<BubbleTailProps> = ({
|
|
33
|
-
|
|
35
|
+
accentHex,
|
|
34
36
|
side,
|
|
35
37
|
visible,
|
|
36
38
|
children,
|
|
37
39
|
}) => {
|
|
38
40
|
if (!visible) return <>{children}</>
|
|
39
41
|
|
|
42
|
+
// Sender tails sit on the dark stop, receiver tails on the pale one — the
|
|
43
|
+
// same mapping the cards use (`SentCard` is `dark`, `ReceivedCard` `light`).
|
|
44
|
+
const variant = side === 'sender' ? 'dark' : 'light'
|
|
45
|
+
const accentStyle = accentHex
|
|
46
|
+
? ({ '--accent': accentHex } as React.CSSProperties)
|
|
47
|
+
: undefined
|
|
48
|
+
|
|
40
49
|
return (
|
|
41
50
|
// `isolate` keeps the tail's negative z-index from escaping behind the
|
|
42
51
|
// thread background; `w-fit` stops the wrapper from stretching in the
|
|
@@ -46,7 +55,8 @@ const BubbleTail: React.FC<BubbleTailProps> = ({
|
|
|
46
55
|
<span
|
|
47
56
|
aria-hidden
|
|
48
57
|
data-testid="link-card-tail"
|
|
49
|
-
|
|
58
|
+
data-variant={variant}
|
|
59
|
+
style={accentStyle}
|
|
50
60
|
className={classNames(
|
|
51
61
|
'messaging-link-card-tail',
|
|
52
62
|
side === 'receiver' && 'messaging-link-card-tail--received'
|
|
@@ -15,11 +15,10 @@ export interface CardBodyProps {
|
|
|
15
15
|
/** Footer URL shown below the description. Ignored when `cta` is set. */
|
|
16
16
|
url?: string
|
|
17
17
|
/**
|
|
18
|
-
* Ink for both chin lines, from `
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* variant default still reads (MES-1388).
|
|
18
|
+
* Ink for both chin lines, from `useChinPalette` — fixed per surface (white on
|
|
19
|
+
* the dark stop, near-black on the pale stop and the audio strip). Replaces the
|
|
20
|
+
* variant's own `text-*` classes (including their opacity) on both lines, so a
|
|
21
|
+
* card whose surface is not the variant default still reads.
|
|
23
22
|
*/
|
|
24
23
|
chinTextColor?: string
|
|
25
24
|
/**
|
|
@@ -35,11 +35,12 @@ export interface CardShellProps {
|
|
|
35
35
|
*/
|
|
36
36
|
bgClassName?: string
|
|
37
37
|
/**
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
38
|
+
* Raw extracted accent colour, published as the `--accent` custom property.
|
|
39
|
+
* `styles.css` derives the card surface from it (`oklch(from …)`) and paints
|
|
40
|
+
* it over the `bg-[…]` fallback where the browser supports it. `undefined`
|
|
41
|
+
* keeps the variant / `bgClassName` fill.
|
|
41
42
|
*/
|
|
42
|
-
|
|
43
|
+
accentHex?: string
|
|
43
44
|
/**
|
|
44
45
|
* Locks the card to Figma's 250px box and makes it a flex column, so the
|
|
45
46
|
* hero absorbs whatever height the chin leaves. Only the `featured` layout
|
|
@@ -88,34 +89,32 @@ const CardShell: React.FC<CardShellProps> = ({
|
|
|
88
89
|
rootRef,
|
|
89
90
|
topRight,
|
|
90
91
|
bgClassName,
|
|
91
|
-
|
|
92
|
+
accentHex,
|
|
92
93
|
fixedHeight = false,
|
|
93
94
|
busy = false,
|
|
94
95
|
'data-testid': dataTestId,
|
|
95
96
|
}) => {
|
|
96
97
|
const isInteractive = href != null || onClick != null
|
|
97
98
|
const className = classNames(
|
|
99
|
+
'messaging-link-card',
|
|
98
100
|
SHELL_CLASS,
|
|
99
101
|
fixedHeight ? FIXED_HEIGHT_CLASS : 'block',
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
//
|
|
102
|
+
// The plain fallback fill, kept underneath the `--accent` derivation: the
|
|
103
|
+
// `.messaging-link-card[style*='--accent']` rule in `styles.css` outranks
|
|
104
|
+
// this single class, so a published accent paints over it and an unset one
|
|
105
|
+
// lands on this surface.
|
|
103
106
|
//
|
|
104
|
-
//
|
|
105
|
-
// Tailwind utilities
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
// on the Arbor preset and has no such colour, so `bg-shade` was silently
|
|
110
|
-
// dropped, the chin rendered transparent, and the own-message `color: white`
|
|
111
|
-
// left the title and URL invisible (MES-1388). An arbitrary value is
|
|
112
|
-
// generated from the class name alone and cannot depend on consumer theme.
|
|
107
|
+
// Must be an arbitrary value, not a theme token: this package ships no
|
|
108
|
+
// Tailwind utilities, so consumers generate our classes by scanning
|
|
109
|
+
// `messaging-react/dist` against their own theme. A theme colour a consumer
|
|
110
|
+
// lacks compiles to nothing and leaves the chin transparent; an arbitrary
|
|
111
|
+
// value is generated from the class name alone.
|
|
113
112
|
//
|
|
114
113
|
// Both values are the plain message bubble's own fill, so a card with no
|
|
115
114
|
// usable accent matches the text bubbles around it: `#1e2330` is
|
|
116
115
|
// `--str-chat__own-message-bubble-background-color` and `#f1f0ee` is
|
|
117
|
-
// `--str-chat__message-bubble-background-color
|
|
118
|
-
//
|
|
116
|
+
// `--str-chat__message-bubble-background-color`, mirrored by the bubble
|
|
117
|
+
// tail's own fills in `styles.css`.
|
|
119
118
|
bgClassName ?? (variant === 'dark' ? 'bg-[#1e2330]' : 'bg-[#f1f0ee]'),
|
|
120
119
|
// `focus-ring` is a design-system utility from the component-library
|
|
121
120
|
// tailwind preset — outline-none + a black 2px focus-visible ring
|
|
@@ -127,6 +126,12 @@ const CardShell: React.FC<CardShellProps> = ({
|
|
|
127
126
|
? { role: 'status', 'aria-busy': true, 'aria-label': LOADING_LABEL }
|
|
128
127
|
: undefined
|
|
129
128
|
|
|
129
|
+
// Published as a custom property, not a `background`: the `[style*='--accent']`
|
|
130
|
+
// rule in `styles.css` keys off its presence and derives the surface in CSS.
|
|
131
|
+
const accentStyle = accentHex
|
|
132
|
+
? ({ '--accent': accentHex } as React.CSSProperties)
|
|
133
|
+
: undefined
|
|
134
|
+
|
|
130
135
|
const corner = topRight ? (
|
|
131
136
|
<div className="pointer-events-auto absolute right-3 top-3 z-10">
|
|
132
137
|
{topRight}
|
|
@@ -142,6 +147,7 @@ const CardShell: React.FC<CardShellProps> = ({
|
|
|
142
147
|
rel="noopener noreferrer"
|
|
143
148
|
onClick={onClick}
|
|
144
149
|
data-testid={dataTestId}
|
|
150
|
+
data-variant={variant}
|
|
145
151
|
className={className}
|
|
146
152
|
style={accentStyle}
|
|
147
153
|
{...busyProps}
|
|
@@ -160,6 +166,7 @@ const CardShell: React.FC<CardShellProps> = ({
|
|
|
160
166
|
onClick={onClick}
|
|
161
167
|
aria-label={ariaLabel}
|
|
162
168
|
data-testid={dataTestId}
|
|
169
|
+
data-variant={variant}
|
|
163
170
|
className={classNames(className, 'text-left')}
|
|
164
171
|
style={accentStyle}
|
|
165
172
|
{...busyProps}
|
|
@@ -174,6 +181,7 @@ const CardShell: React.FC<CardShellProps> = ({
|
|
|
174
181
|
<div
|
|
175
182
|
ref={rootRef as React.Ref<HTMLDivElement>}
|
|
176
183
|
data-testid={dataTestId}
|
|
184
|
+
data-variant={variant}
|
|
177
185
|
className={className}
|
|
178
186
|
style={accentStyle}
|
|
179
187
|
{...busyProps}
|