dynamic rich text color

This commit is contained in:
Samuel Newman
2024-11-21 18:46:38 +00:00
parent f9ade07f81
commit 6f7ee7efab
6 changed files with 131 additions and 15 deletions
+3
View File
@@ -156,6 +156,7 @@
"lodash.isequal": "^4.5.0",
"lodash.shuffle": "^4.2.0",
"lodash.throttle": "^4.1.1",
"mix-css-color": "^0.2.0",
"multiformats": "^13.1.0",
"nanoid": "^5.0.5",
"normalize-url": "^8.0.0",
@@ -200,6 +201,7 @@
"tippy.js": "^6.3.7",
"tlds": "^1.234.0",
"tldts": "^6.1.46",
"wcag-contrast": "^3.0.0",
"zeego": "^1.6.2",
"zod": "^3.20.2"
},
@@ -227,6 +229,7 @@
"@types/react-dom": "^18.2.18",
"@types/react-responsive": "^8.0.5",
"@types/react-test-renderer": "^17.0.1",
"@types/wcag-contrast": "^3.0.3",
"@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.18.0",
"babel-jest": "^29.7.0",
+14 -14
View File
@@ -8,13 +8,14 @@ import {useNavigation} from '@react-navigation/native'
import {NavigationProp} from '#/lib/routes/types'
import {toShortUrl} from '#/lib/strings/url-helpers'
import {isNative} from '#/platform/detection'
import {atoms as a, flatten, native, TextStyleProp, useTheme, web} from '#/alf'
import {atoms as a, flatten, native, TextStyleProp, web} from '#/alf'
import {isOnlyEmoji} from '#/alf/typography'
import {useInteractionState} from '#/components/hooks/useInteractionState'
import {InlineLinkText, LinkProps} from '#/components/Link'
import {ProfileHoverCard} from '#/components/ProfileHoverCard'
import {TagMenu, useTagMenuControl} from '#/components/TagMenu'
import {Text, TextProps} from '#/components/Typography'
import {useDynamicColor} from './hooks/useDynamicColor'
const WORD_WRAP = {wordWrap: 1}
@@ -29,6 +30,7 @@ export type RichTextProps = TextStyleProp &
onLinkPress?: LinkProps['onPress']
interactiveStyle?: TextStyle
emojiMultiplier?: number
dynamicColor?: boolean
}
export function RichText({
@@ -43,6 +45,7 @@ export function RichText({
onLinkPress,
interactiveStyle,
emojiMultiplier = 1.85,
dynamicColor = false,
}: RichTextProps) {
const richText = React.useMemo(
() =>
@@ -50,10 +53,13 @@ export function RichText({
[value],
)
const interactiveColor = useDynamicColor({enabled: dynamicColor})
const flattenedStyle = flatten(style)
const plainStyles = [a.leading_snug, flattenedStyle]
const interactiveStyles = [
a.leading_snug,
interactiveColor,
flatten(interactiveStyle),
flattenedStyle,
]
@@ -182,7 +188,6 @@ function RichTextTag({
selectable?: boolean
authorHandle?: string
} & TextStyleProp) {
const t = useTheme()
const {_} = useLingui()
const control = useTagMenuControl()
const {
@@ -224,22 +229,17 @@ function RichTextTag({
{...web({
onMouseEnter: onHoverIn,
onMouseLeave: onHoverOut,
onFocus: onFocus,
onBlur: onBlur,
})}
// @ts-ignore
onFocus={onFocus}
onBlur={onBlur}
style={[
web({
cursor: 'pointer',
}),
{color: t.palette.primary_500},
(hovered || focused) && {
...web({
web({cursor: 'pointer'}),
web(
(hovered || focused) && {
outline: 0,
textDecorationLine: 'underline',
textDecorationColor: t.palette.primary_500,
}),
},
},
),
style,
]}>
{text}
+61
View File
@@ -0,0 +1,61 @@
import {useMemo} from 'react'
import {StyleProp, TextStyle} from 'react-native'
import mixColors from 'mix-css-color'
import {rgb as compareContrast} from 'wcag-contrast'
import {useTheme} from '#/alf'
export function useDynamicColor({enabled} = {enabled: false}) {
const t = useTheme()
const color = enabled
? getDynamicColor(t.name !== 'light', t.palette.primary_500)
: t.palette.primary_500
return useMemo(
() =>
({
color,
textDecorationColor: color,
} satisfies StyleProp<TextStyle>),
[color],
)
}
// the goal is to return a blue color that should be an acceptable constrast with
// the background color. the default blue is rgb(16, 131, 254)
// we want to color mix it with the background color
// and flip to whiteish if it's going to be unacceptable low contrast
const BASE_COLOR_LIGHT = 'rgb(16, 131, 254)'
const BASE_COLOR_DARK = 'rgb(32, 139, 254)'
function getDynamicColor(dark: boolean, themeColor: string) {
const baseColor = dark ? BASE_COLOR_DARK : BASE_COLOR_LIGHT
const blendedColor = mixColors(baseColor, themeColor, 75)
debugColorContrast(
blendedColor,
// for debug, we simulate the linear gradient on the profile
mixColors(themeColor, '#ffffff', 30),
)
let [h, s, l] = blendedColor.hsla
return `hsl(${h}, ${s}%, ${l * 1.1}%)`
}
function debugColorContrast(
foreground: ReturnType<typeof mixColors>,
background: ReturnType<typeof mixColors>,
) {
const score = getContrastScore(foreground, background)
console.log(
`%c ${score > 4.5 ? 'PASS' : 'FAIL'}: ${Math.trunc(score * 10) / 10} `,
`color: ${foreground.hex}; background: ${background.hex}`,
)
}
function getContrastScore(
foreground: ReturnType<typeof mixColors>,
background: ReturnType<typeof mixColors>,
) {
const [r1, g1, b1] = foreground.rgba
const [r2, g2, b2] = background.rgba
return compareContrast([r1, g1, b1], [r2, g2, b2])
}
@@ -264,6 +264,7 @@ let ProfileHeaderLabeler = ({
value={descriptionRT}
enableTags
authorHandle={profile.handle}
dynamicColor
/>
</View>
) : undefined}
@@ -257,6 +257,7 @@ let ProfileHeaderStandard = ({
value={descriptionRT}
enableTags
authorHandle={profile.handle}
dynamicColor
/>
</View>
) : undefined}
+51 -1
View File
@@ -7204,6 +7204,11 @@
resolved "https://registry.yarnpkg.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz#60be8d21baab8c305132eb9cb912ed497852aadc"
integrity sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==
"@types/wcag-contrast@^3.0.3":
version "3.0.3"
resolved "https://registry.yarnpkg.com/@types/wcag-contrast/-/wcag-contrast-3.0.3.tgz#0b0339ab210b859eeee555b3fef58d293216905c"
integrity sha512-oprevfwJSLfpQK4KaWsRKJuNoebV76+xhmbXiWJGy+FkS34LpCgCMNIwRXWTb8xmmSxUE2ycFOYE7uyRVRm3LA==
"@types/ws@^8.5.5":
version "8.5.5"
resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.5.tgz#af587964aa06682702ee6dcbc7be41a80e4b28eb"
@@ -8871,7 +8876,7 @@ color-name@1.1.3:
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==
color-name@^1.0.0, color-name@~1.1.4:
color-name@^1.0.0, color-name@^1.1.4, color-name@~1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
@@ -10314,6 +10319,11 @@ eslint@^8.19.0:
strip-ansi "^6.0.1"
text-table "^0.2.0"
esm@^3.0.84:
version "3.2.25"
resolved "https://registry.yarnpkg.com/esm/-/esm-3.2.25.tgz#342c18c29d56157688ba5ce31f8431fbb795cc10"
integrity sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==
espree@^9.6.0, espree@^9.6.1:
version "9.6.1"
resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f"
@@ -11707,6 +11717,11 @@ hermes-profile-transformer@^0.0.6:
dependencies:
source-map "^0.7.3"
hex-rgb@^4.1.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/hex-rgb/-/hex-rgb-4.3.0.tgz#af5e974e83bb2fefe44d55182b004ec818c07776"
integrity sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw==
history@^5.3.0:
version "5.3.0"
resolved "https://registry.yarnpkg.com/history/-/history-5.3.0.tgz#1548abaa245ba47992f063a0783db91ef201c73b"
@@ -14296,6 +14311,14 @@ minizlib@^2.1.1:
minipass "^3.0.0"
yallist "^4.0.0"
mix-css-color@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/mix-css-color/-/mix-css-color-0.2.0.tgz#413a2346effcb36a815b1d09bcbac12bcf3aac63"
integrity sha512-mZugANySFPE21tjELbQddhC6HAZNzqp7gDxmW8fJFURSWtJ0nuXU26dyrb/1AR6ZYxdEAtW2bbWT9QnRtI6Jzg==
dependencies:
parse-css-color "^0.1.2"
pure-color "^1.3.0"
mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3:
version "0.5.3"
resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113"
@@ -14931,6 +14954,14 @@ parent-module@^1.0.0:
dependencies:
callsites "^3.0.0"
parse-css-color@^0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/parse-css-color/-/parse-css-color-0.1.2.tgz#9d178f2d2b5eb7245c0deb22ea35046b79ff3f83"
integrity sha512-z7v/tf0edGsnlm9VONQtH+u/YVrdUqZXrSBzqM13scef8Abl2VyZfYsZaJoyb/AyY4SIxtoJChSQ4MURHfY3Sg==
dependencies:
color-name "^1.1.4"
hex-rgb "^4.1.0"
parse-json@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0"
@@ -15934,6 +15965,11 @@ punycode@^2.1.0, punycode@^2.1.1:
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f"
integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==
pure-color@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/pure-color/-/pure-color-1.3.0.tgz#1fe064fb0ac851f0de61320a8bf796836422f33e"
integrity sha512-QFADYnsVoBMw1srW7OVKEYjG+MbIa49s54w1MA1EDY6r2r/sTcKKYqRX1f4GYvnXP7eN/Pe9HFcX+hwzmrXRHA==
pure-rand@^6.0.0:
version "6.0.2"
resolved "https://registry.yarnpkg.com/pure-rand/-/pure-rand-6.0.2.tgz#a9c2ddcae9b68d736a8163036f088a2781c8b306"
@@ -16655,6 +16691,13 @@ relateurl@^0.2.7:
resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9"
integrity sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==
relative-luminance@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/relative-luminance/-/relative-luminance-2.0.1.tgz#2babddf3a5a59673227d6f02e0f68e13989e3d13"
integrity sha512-wFuITNthJilFPwkK7gNJcULxXBcfFZvZORsvdvxeOdO44wCeZnuQkf3nFFzOR/dpJNxYsdRZJLsepWbyKhnMww==
dependencies:
esm "^3.0.84"
remove-trailing-slash@^0.1.0:
version "0.1.1"
resolved "https://registry.yarnpkg.com/remove-trailing-slash/-/remove-trailing-slash-0.1.1.tgz#be2285a59f39c74d1bce4f825950061915e3780d"
@@ -18763,6 +18806,13 @@ wbuf@^1.1.0, wbuf@^1.7.3:
dependencies:
minimalistic-assert "^1.0.0"
wcag-contrast@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/wcag-contrast/-/wcag-contrast-3.0.0.tgz#51c2df43f15162993006454b3362844205889efb"
integrity sha512-RWbpg/S7FOXDCwqC2oFhN/vh8dHzj0OS6dpyOSDHyQFSmqmR+lAUStV/ziTT1GzDqL9wol+nZQB4vCi5yEak+w==
dependencies:
relative-luminance "^2.0.0"
wcwidth@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8"