dynamic rich text color
This commit is contained in:
@@ -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
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user