@linktr.ee/messaging-react 3.31.1 → 3.31.2-rc-1785926144
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-Ch6N2pkc.js} +2 -2
- package/dist/{Card-CmlaSw7i.js.map → Card-Ch6N2pkc.js.map} +1 -1
- package/dist/assets/index.css +1 -1
- package/dist/index-BKkCNnea.js +5677 -0
- package/dist/index-BKkCNnea.js.map +1 -0
- package/dist/index-Ct_Jgy8P.cjs +2 -0
- package/dist/index-Ct_Jgy8P.cjs.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 +44 -27
- package/src/components/LinkAttachment/LinkAttachment.test.tsx +47 -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 +22 -9
- package/src/components/LinkAttachment/components/_shared/hex.ts +7 -7
- package/src/components/LinkAttachment/components/_shared/useChinPalette.ts +36 -27
- package/src/components/LinkAttachment/types.ts +6 -5
- package/src/styles.css +39 -0
- 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-BKkCNnea.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-1785926144",
|
|
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-1785926144",
|
|
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. These are
|
|
465
|
+
* the out-of-gamut extremes where the CSS derivation and the old JS one diverge.
|
|
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,25 @@ 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
|
-
*
|
|
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 …)`. No JS colour maths — this is exactly the render
|
|
510
|
+
* path a colour-library swap moves. A refused accent (`none` / `malformed`)
|
|
511
|
+
* publishes nothing and shows the fallback fill. The resolved surface hex is
|
|
512
|
+
* the browser's now, not JS's, so only the input is labelled.
|
|
502
513
|
*/
|
|
503
514
|
const DerivedSwatch = ({
|
|
504
515
|
accentColor,
|
|
@@ -507,17 +518,23 @@ const DerivedSwatch = ({
|
|
|
507
518
|
accentColor?: string
|
|
508
519
|
variant: LinkAttachmentVariant
|
|
509
520
|
}) => {
|
|
510
|
-
const
|
|
511
|
-
|
|
521
|
+
const accentHex =
|
|
522
|
+
accentColor != null && HEX_PATTERN.test(accentColor.trim())
|
|
523
|
+
? toCssHex(accentColor)
|
|
524
|
+
: undefined
|
|
512
525
|
return (
|
|
513
526
|
<div
|
|
514
|
-
className=
|
|
515
|
-
|
|
527
|
+
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]}`}
|
|
528
|
+
data-variant={variant}
|
|
529
|
+
style={
|
|
530
|
+
{
|
|
531
|
+
color: INK_BY_VARIANT[variant],
|
|
532
|
+
...(accentHex ? { '--accent': accentHex } : {}),
|
|
533
|
+
} as React.CSSProperties
|
|
534
|
+
}
|
|
516
535
|
>
|
|
517
536
|
<span>Yoga on Ice</span>
|
|
518
|
-
<span className="font-mono opacity-80">
|
|
519
|
-
{surface ?? `fallback ${painted}`}
|
|
520
|
-
</span>
|
|
537
|
+
<span className="font-mono opacity-80">{accentHex ?? 'fallback'}</span>
|
|
521
538
|
</div>
|
|
522
539
|
)
|
|
523
540
|
}
|
|
@@ -527,12 +544,12 @@ const GridHead = ({ children }: { children: React.ReactNode }) => (
|
|
|
527
544
|
)
|
|
528
545
|
|
|
529
546
|
/**
|
|
530
|
-
* Isolates the
|
|
531
|
-
*
|
|
532
|
-
*
|
|
533
|
-
*
|
|
534
|
-
*
|
|
535
|
-
*
|
|
547
|
+
* Isolates the accent-chin surface: for every hue at maximum chroma and every
|
|
548
|
+
* neutral/edge input, the `dark` (Sent) and `light` (Received) surfaces CSS
|
|
549
|
+
* derives from the published `--accent`. One snapshot, no hero pixels — the
|
|
550
|
+
* dense diff target for the MES-1412 culori→CSS swap. In-gamut inputs match the
|
|
551
|
+
* old JS output exactly; the max-chroma hue-sweep rows are where the browser's
|
|
552
|
+
* gamut clip diverges from culori's `clampChroma`, and will visibly diff.
|
|
536
553
|
*/
|
|
537
554
|
export const AccentDerivation: StoryFn = () => (
|
|
538
555
|
<div className="min-h-screen w-full bg-[#F9F7F4] p-8">
|
|
@@ -119,28 +119,33 @@ 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 the unit tests assert the *contract* React owns:
|
|
124
|
+
// the raw extracted hex is published as `--accent` (and dropped when there is
|
|
125
|
+
// no visible hero), leaving the surface derivation to the browser. Pixel
|
|
126
|
+
// parity is covered by the `AccentDerivation` Chromatic story (MES-1412).
|
|
127
|
+
const accentOf = (element: HTMLElement) =>
|
|
128
|
+
element.style.getPropertyValue('--accent')
|
|
129
|
+
|
|
130
|
+
it('publishes the accent for the sender and inks the chin white', async () => {
|
|
125
131
|
renderWithProviders(
|
|
126
132
|
<LinkAttachment.Sent {...CARD_PROPS} accentColor={ACCENT} />
|
|
127
133
|
)
|
|
128
134
|
const root = await screen.findByRole('link')
|
|
129
|
-
|
|
130
|
-
//
|
|
131
|
-
// what MES-1388
|
|
132
|
-
expect(
|
|
133
|
-
expect(root).not.toHaveStyle({ backgroundColor: ACCENT })
|
|
135
|
+
expect(accentOf(root)).toBe(ACCENT)
|
|
136
|
+
// No inline background: the surface is derived from `--accent` in CSS, not
|
|
137
|
+
// painted verbatim (painting the raw accent is what MES-1388 reported).
|
|
138
|
+
expect(root.style.backgroundColor).toBe('')
|
|
134
139
|
expect(screen.getByText(CARD_PROPS.title)).toHaveStyle({ color: '#FFFFFF' })
|
|
135
140
|
})
|
|
136
141
|
|
|
137
|
-
it('
|
|
142
|
+
it('publishes the accent for the receiver and inks the chin near-black', async () => {
|
|
138
143
|
renderWithProviders(
|
|
139
144
|
<LinkAttachment.Received {...CARD_PROPS} accentColor={ACCENT} />
|
|
140
145
|
)
|
|
141
146
|
const root = await screen.findByRole('link')
|
|
142
|
-
expect(
|
|
143
|
-
expect(root
|
|
147
|
+
expect(accentOf(root)).toBe(ACCENT)
|
|
148
|
+
expect(root.style.backgroundColor).toBe('')
|
|
144
149
|
expect(screen.getByText(CARD_PROPS.title)).toHaveStyle({ color: '#040403' })
|
|
145
150
|
})
|
|
146
151
|
|
|
@@ -151,13 +156,13 @@ describe('LinkAttachment accent chin (MES-1349, MES-1388)', () => {
|
|
|
151
156
|
const sent = renderWithProviders(
|
|
152
157
|
<LinkAttachment.Sent {...CARD_PROPS} accentColor={accentColor} />
|
|
153
158
|
)
|
|
154
|
-
expect(
|
|
159
|
+
expect(accentOf(await screen.findByRole('link'))).toBe(accentColor)
|
|
155
160
|
sent.unmount()
|
|
156
161
|
|
|
157
162
|
const received = renderWithProviders(
|
|
158
163
|
<LinkAttachment.Received {...CARD_PROPS} accentColor={accentColor} />
|
|
159
164
|
)
|
|
160
|
-
expect(
|
|
165
|
+
expect(accentOf(await screen.findByRole('link'))).toBe(accentColor)
|
|
161
166
|
received.unmount()
|
|
162
167
|
}
|
|
163
168
|
})
|
|
@@ -165,7 +170,7 @@ describe('LinkAttachment accent chin (MES-1349, MES-1388)', () => {
|
|
|
165
170
|
it('falls back to a self-contained fill, not a consumer theme token', async () => {
|
|
166
171
|
renderWithProviders(<LinkAttachment.Sent {...CARD_PROPS} />)
|
|
167
172
|
const root = await screen.findByRole('link')
|
|
168
|
-
expect(
|
|
173
|
+
expect(accentOf(root)).toBe('')
|
|
169
174
|
|
|
170
175
|
// The fill has to survive in consumers that do not share our Tailwind
|
|
171
176
|
// theme. This package ships no utilities, so consumers generate our classes
|
|
@@ -186,8 +191,8 @@ describe('LinkAttachment accent chin (MES-1349, MES-1388)', () => {
|
|
|
186
191
|
<LinkAttachment.Sent {...CARD_PROPS} accentColor="rebeccapurple" />
|
|
187
192
|
)
|
|
188
193
|
const root = await screen.findByRole('link')
|
|
189
|
-
expect(root
|
|
190
|
-
// Ink still comes from the
|
|
194
|
+
expect(accentOf(root)).toBe('')
|
|
195
|
+
// Ink still comes from the side — here the sender's fallback fill.
|
|
191
196
|
expect(screen.getByText(CARD_PROPS.title)).toHaveStyle({ color: '#FFFFFF' })
|
|
192
197
|
})
|
|
193
198
|
|
|
@@ -196,12 +201,12 @@ describe('LinkAttachment accent chin (MES-1349, MES-1388)', () => {
|
|
|
196
201
|
<LinkAttachment.Sent {...CARD_PROPS} accentColor={ACCENT} />
|
|
197
202
|
)
|
|
198
203
|
const root = await screen.findByRole('link')
|
|
199
|
-
expect(root
|
|
204
|
+
expect(accentOf(root)).toBe(ACCENT)
|
|
200
205
|
|
|
201
206
|
// A surface derived from an image the viewer cannot see is a lie, so the
|
|
202
207
|
// card falls all the way back rather than keeping a colour with no source.
|
|
203
208
|
fireEvent.error(screen.getByRole('img'))
|
|
204
|
-
expect(root
|
|
209
|
+
expect(accentOf(root)).toBe('')
|
|
205
210
|
expect(screen.getByText(CARD_PROPS.title)).toHaveStyle({ color: '#FFFFFF' })
|
|
206
211
|
})
|
|
207
212
|
|
|
@@ -222,12 +227,12 @@ describe('LinkAttachment accent chin (MES-1349, MES-1388)', () => {
|
|
|
222
227
|
const shell = container.querySelector(
|
|
223
228
|
'[class*="select-none"]'
|
|
224
229
|
) as HTMLElement
|
|
225
|
-
expect(shell.style.
|
|
230
|
+
expect(shell.style.getPropertyValue('--accent')).toBe(ACCENT)
|
|
226
231
|
expect(container.querySelector('video')).toHaveAttribute('poster')
|
|
227
232
|
|
|
228
233
|
fireEvent.error(container.querySelector('img') as HTMLImageElement)
|
|
229
234
|
|
|
230
|
-
expect(shell.style.
|
|
235
|
+
expect(shell.style.getPropertyValue('--accent')).toBe('')
|
|
231
236
|
expect(screen.getByText(CARD_PROPS.title)).toHaveStyle({ color: '#FFFFFF' })
|
|
232
237
|
// The failed poster is dropped with the accent, so the player falls back to
|
|
233
238
|
// its own first frame instead of retrying a 404.
|
|
@@ -252,7 +257,7 @@ describe('LinkAttachment accent chin (MES-1349, MES-1388)', () => {
|
|
|
252
257
|
<LinkAttachment.Sent {...CARD_PROPS} {...AUDIO} accentColor={ACCENT} />
|
|
253
258
|
)
|
|
254
259
|
const sent = container.firstElementChild as HTMLElement
|
|
255
|
-
expect(sent.style.
|
|
260
|
+
expect(sent.style.getPropertyValue('--accent')).toBe('')
|
|
256
261
|
expect(sent.className).toContain(AUDIO_BG_CLASS)
|
|
257
262
|
expect(screen.getByText(CARD_PROPS.title)).toHaveStyle({ color: '#040403' })
|
|
258
263
|
expect(screen.queryByRole('img')).toBeNull()
|
|
@@ -266,7 +271,7 @@ describe('LinkAttachment accent chin (MES-1349, MES-1388)', () => {
|
|
|
266
271
|
/>
|
|
267
272
|
)
|
|
268
273
|
const received = await screen.findByTestId('link-attachment')
|
|
269
|
-
expect(received.style.
|
|
274
|
+
expect(received.style.getPropertyValue('--accent')).toBe('')
|
|
270
275
|
expect(received.className).toContain(AUDIO_BG_CLASS)
|
|
271
276
|
})
|
|
272
277
|
|
|
@@ -281,29 +286,27 @@ describe('LinkAttachment accent chin (MES-1349, MES-1388)', () => {
|
|
|
281
286
|
/>
|
|
282
287
|
)
|
|
283
288
|
const root = await screen.findByRole('link')
|
|
284
|
-
expect(root
|
|
289
|
+
expect(accentOf(root)).toBe('')
|
|
285
290
|
// Variant fill, asserted by shape not literal — see the self-contained-fill
|
|
286
291
|
// test above for why this must stay an arbitrary value (MES-1388).
|
|
287
292
|
expect(root.className).toMatch(/bg-\[/)
|
|
288
293
|
expect(screen.getByText(CARD_PROPS.title)).toHaveStyle({ color: '#FFFFFF' })
|
|
289
294
|
})
|
|
290
295
|
|
|
291
|
-
it('normalizes a hashless accent so the
|
|
296
|
+
it('normalizes a hashless accent so the derivation actually resolves', async () => {
|
|
292
297
|
// The extractor returns a bare hex often enough that dropping it would lose
|
|
293
|
-
// the colour silently — `
|
|
298
|
+
// the colour silently — `oklch(from 7008E7 …)` never resolves (MES-1349), so
|
|
299
|
+
// the published `--accent` must carry the `#`.
|
|
294
300
|
const hashless = renderWithProviders(
|
|
295
301
|
<LinkAttachment.Sent {...CARD_PROPS} accentColor="7008E7" />
|
|
296
302
|
)
|
|
297
|
-
|
|
298
|
-
expect(derived).not.toBe('')
|
|
303
|
+
expect(accentOf(await screen.findByRole('link'))).toBe('#7008E7')
|
|
299
304
|
hashless.unmount()
|
|
300
305
|
|
|
301
306
|
renderWithProviders(
|
|
302
307
|
<LinkAttachment.Sent {...CARD_PROPS} accentColor={ACCENT} />
|
|
303
308
|
)
|
|
304
|
-
expect((await screen.findByRole('link'))
|
|
305
|
-
derived
|
|
306
|
-
)
|
|
309
|
+
expect(accentOf(await screen.findByRole('link'))).toBe('#7008E7')
|
|
307
310
|
})
|
|
308
311
|
|
|
309
312
|
it('lets a loading status outrank the audio hero on all three cards', () => {
|
|
@@ -349,13 +352,15 @@ describe('LinkAttachment accent chin (MES-1349, MES-1388)', () => {
|
|
|
349
352
|
groupPosition="end"
|
|
350
353
|
/>
|
|
351
354
|
)
|
|
352
|
-
// The tail paints outside the shell's own box, so it
|
|
353
|
-
//
|
|
354
|
-
// would disagree.
|
|
355
|
+
// The tail paints outside the shell's own box, so it can't inherit the
|
|
356
|
+
// shell's computed background — it publishes the same `--accent` and derives
|
|
357
|
+
// the surface itself in CSS, or the tail and the card would disagree.
|
|
355
358
|
const card = await screen.findByRole('link')
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
+
const tail = screen.getByTestId('link-card-tail')
|
|
360
|
+
expect(tail.style.getPropertyValue('--accent')).toBe(
|
|
361
|
+
card.style.getPropertyValue('--accent')
|
|
362
|
+
)
|
|
363
|
+
expect(tail.style.getPropertyValue('--accent')).toBe(ACCENT)
|
|
359
364
|
unmount()
|
|
360
365
|
|
|
361
366
|
// Mid-run the following bubble owns the tail.
|
|
@@ -370,10 +375,13 @@ describe('LinkAttachment accent chin (MES-1349, MES-1388)', () => {
|
|
|
370
375
|
})
|
|
371
376
|
|
|
372
377
|
it('falls back to the variant fill for the tail when there is no accent', async () => {
|
|
378
|
+
// No accent published, so no `--accent`: the tail's `[data-variant='dark']`
|
|
379
|
+
// rule in styles.css paints the sender's plain `#1e2330` bubble fill (the
|
|
380
|
+
// computed value isn't observable in jsdom — see the note on `accentOf`).
|
|
373
381
|
renderWithProviders(<LinkAttachment.Sent {...CARD_PROPS} />)
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
382
|
+
const tail = await screen.findByTestId('link-card-tail')
|
|
383
|
+
expect(tail.style.getPropertyValue('--accent')).toBe('')
|
|
384
|
+
expect(tail).toHaveAttribute('data-variant', 'dark')
|
|
377
385
|
})
|
|
378
386
|
|
|
379
387
|
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`). `undefined` falls the
|
|
8
|
+
* tail back to this side's plain bubble fill. The tail paints outside the
|
|
9
|
+
* shell's box, so it can't inherit the shell's computed background — it runs
|
|
10
|
+
* the derivation itself off its own `--accent` (MES-1412).
|
|
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 (MES-1388).
|
|
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 in CSS (`oklch(from …)`) and
|
|
40
|
+
* paints it over the `bg-[…]` fallback where the browser supports it —
|
|
41
|
+
* MES-1412. `undefined` 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,18 +89,21 @@ 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 where the browser
|
|
105
|
+
// resolves `oklch(from …)`, and an accent that can't (or was never set)
|
|
106
|
+
// lands on this sane surface.
|
|
103
107
|
//
|
|
104
108
|
// MUST be an arbitrary value, not a theme token. This package ships no
|
|
105
109
|
// Tailwind utilities — `dist/assets/index.css` is only the hand-written
|
|
@@ -115,7 +119,7 @@ const CardShell: React.FC<CardShellProps> = ({
|
|
|
115
119
|
// usable accent matches the text bubbles around it: `#1e2330` is
|
|
116
120
|
// `--str-chat__own-message-bubble-background-color` and `#f1f0ee` is
|
|
117
121
|
// `--str-chat__message-bubble-background-color`. Mirrored in
|
|
118
|
-
//
|
|
122
|
+
// and mirrored by the bubble tail's own fills in `styles.css`.
|
|
119
123
|
bgClassName ?? (variant === 'dark' ? 'bg-[#1e2330]' : 'bg-[#f1f0ee]'),
|
|
120
124
|
// `focus-ring` is a design-system utility from the component-library
|
|
121
125
|
// tailwind preset — outline-none + a black 2px focus-visible ring
|
|
@@ -127,6 +131,12 @@ const CardShell: React.FC<CardShellProps> = ({
|
|
|
127
131
|
? { role: 'status', 'aria-busy': true, 'aria-label': LOADING_LABEL }
|
|
128
132
|
: undefined
|
|
129
133
|
|
|
134
|
+
// Published as a custom property, not a `background`: the `[style*='--accent']`
|
|
135
|
+
// rule in `styles.css` keys off its presence and derives the surface in CSS.
|
|
136
|
+
const accentStyle = accentHex
|
|
137
|
+
? ({ '--accent': accentHex } as React.CSSProperties)
|
|
138
|
+
: undefined
|
|
139
|
+
|
|
130
140
|
const corner = topRight ? (
|
|
131
141
|
<div className="pointer-events-auto absolute right-3 top-3 z-10">
|
|
132
142
|
{topRight}
|
|
@@ -142,6 +152,7 @@ const CardShell: React.FC<CardShellProps> = ({
|
|
|
142
152
|
rel="noopener noreferrer"
|
|
143
153
|
onClick={onClick}
|
|
144
154
|
data-testid={dataTestId}
|
|
155
|
+
data-variant={variant}
|
|
145
156
|
className={className}
|
|
146
157
|
style={accentStyle}
|
|
147
158
|
{...busyProps}
|
|
@@ -160,6 +171,7 @@ const CardShell: React.FC<CardShellProps> = ({
|
|
|
160
171
|
onClick={onClick}
|
|
161
172
|
aria-label={ariaLabel}
|
|
162
173
|
data-testid={dataTestId}
|
|
174
|
+
data-variant={variant}
|
|
163
175
|
className={classNames(className, 'text-left')}
|
|
164
176
|
style={accentStyle}
|
|
165
177
|
{...busyProps}
|
|
@@ -174,6 +186,7 @@ const CardShell: React.FC<CardShellProps> = ({
|
|
|
174
186
|
<div
|
|
175
187
|
ref={rootRef as React.Ref<HTMLDivElement>}
|
|
176
188
|
data-testid={dataTestId}
|
|
189
|
+
data-variant={variant}
|
|
177
190
|
className={className}
|
|
178
191
|
style={accentStyle}
|
|
179
192
|
{...busyProps}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The hex contract
|
|
2
|
+
* The hex contract for the accent colour `useChinPalette` publishes.
|
|
3
3
|
*
|
|
4
|
-
* `
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* The extracted `accent_color` (from `getDominantColorFromImageUrl` via a Stream
|
|
5
|
+
* attachment field) is always a hex, but CSS `oklch(from …)` would resolve any
|
|
6
|
+
* colour name — including `rebeccapurple` — so the input is narrowed to a hex
|
|
7
|
+
* before it is published as `--accent`, rather than widening that contract by
|
|
8
|
+
* accident.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
/**
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
*/
|
|
20
20
|
export const HEX_PATTERN = /^#?(?:[0-9a-f]{3}|[0-9a-f]{6})$/i
|
|
21
21
|
|
|
22
|
-
/** Normalizes a hex for CSS
|
|
22
|
+
/** Normalizes a hex for CSS, adding the `#` the extractor omits. */
|
|
23
23
|
export function toCssHex(hex: string): string {
|
|
24
24
|
const trimmed = hex.trim()
|
|
25
25
|
return trimmed.startsWith('#') ? trimmed : `#${trimmed}`
|