feat(rn): impl markdown component for rn
- Added `es-toolkit` version 1.29.0 to `pnpm-lock.yaml` and `package.json`. - Updated `typescript` to version 5.6.3 in `pnpm-lock.yaml`. - Enhanced `CopyButton` component with new size option "tiny" and improved background color animation. - Updated `Label`, `Select`, and `TextField` components to adjust height for better UI consistency. - Refactored `PreviewUrl` component to use `ScrollView` and `MonoText` for improved text display. - Added Markdown support in `rsshub-form` for better content presentation. - Updated color handling in `colors.ts` for better theme management. Signed-off-by: Innei <i@innei.in> Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
parent
97bacc99f1
commit
93b38a7c70
|
|
@ -0,0 +1,7 @@
|
|||
import type { DOMProps } from "expo/dom"
|
||||
import type { FC } from "react"
|
||||
|
||||
declare global {
|
||||
export type WebComponent<P = object> = FC<P & { dom?: DOMProps }>
|
||||
}
|
||||
export {}
|
||||
|
|
@ -33,6 +33,7 @@
|
|||
"better-auth": "1.1.3",
|
||||
"cookie-es": "^1.2.2",
|
||||
"dayjs": "1.11.13",
|
||||
"es-toolkit": "1.29.0",
|
||||
"expo": "52.0.18",
|
||||
"expo-apple-authentication": "~7.1.2",
|
||||
"expo-blur": "~14.0.1",
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { cn } from "@follow/utils/src/utils"
|
|||
import { useRef } from "react"
|
||||
import { Pressable } from "react-native"
|
||||
import Animated, {
|
||||
interpolateColor,
|
||||
useAnimatedStyle,
|
||||
useSharedValue,
|
||||
withDelay,
|
||||
|
|
@ -11,8 +12,9 @@ import { useEventCallback } from "usehooks-ts"
|
|||
|
||||
import { CheckFilledIcon } from "@/src/icons/check_filled"
|
||||
import { Copy2CuteReIcon } from "@/src/icons/copy_2_cute_re"
|
||||
import { useColor } from "@/src/theme/colors"
|
||||
|
||||
type Size = "sm" | "md"
|
||||
type Size = "sm" | "md" | "tiny"
|
||||
interface CopyButtonProps {
|
||||
onCopy: () => void
|
||||
className?: string
|
||||
|
|
@ -20,15 +22,18 @@ interface CopyButtonProps {
|
|||
}
|
||||
|
||||
const sizeClassNames = {
|
||||
tiny: "size-6",
|
||||
sm: "size-8",
|
||||
md: "size-10",
|
||||
}
|
||||
|
||||
const sizeIconSize = {
|
||||
tiny: 14,
|
||||
sm: 18,
|
||||
md: 20,
|
||||
}
|
||||
|
||||
const AnimatedPressable = Animated.createAnimatedComponent(Pressable)
|
||||
export const CopyButton = ({ onCopy, className, size = "sm" }: CopyButtonProps) => {
|
||||
const initialIconScale = useSharedValue(1)
|
||||
const pressedIconScale = useSharedValue(0)
|
||||
|
|
@ -36,14 +41,26 @@ export const CopyButton = ({ onCopy, className, size = "sm" }: CopyButtonProps)
|
|||
transform: [{ scale: initialIconScale.value }],
|
||||
}))
|
||||
|
||||
const initialBgColor = useColor("gray3")
|
||||
const pressedBgColor = useColor("green")
|
||||
|
||||
const pressedStyle = useAnimatedStyle(() => ({
|
||||
position: "absolute",
|
||||
transform: [{ scale: pressedIconScale.value }],
|
||||
}))
|
||||
|
||||
const wrapperStyle = useAnimatedStyle(() => ({
|
||||
backgroundColor: interpolateColor(
|
||||
pressedIconScale.value,
|
||||
[0, 1],
|
||||
[initialBgColor, pressedBgColor],
|
||||
),
|
||||
}))
|
||||
|
||||
const animatedProgressingRef = useRef(false)
|
||||
const handlePress = useEventCallback(() => {
|
||||
onCopy()
|
||||
|
||||
if (animatedProgressingRef.current) return
|
||||
animatedProgressingRef.current = true
|
||||
initialIconScale.value = withTiming(0, { duration: 100 }, () => {
|
||||
|
|
@ -51,18 +68,25 @@ export const CopyButton = ({ onCopy, className, size = "sm" }: CopyButtonProps)
|
|||
pressedIconScale.value = withDelay(
|
||||
1000,
|
||||
withTiming(0, { duration: 100 }, () => {
|
||||
initialIconScale.value = withTiming(1, { duration: 100 }, () => {
|
||||
animatedProgressingRef.current = false
|
||||
})
|
||||
initialIconScale.value = withTiming(1, { duration: 100 })
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
setTimeout(
|
||||
() => {
|
||||
animatedProgressingRef.current = false
|
||||
},
|
||||
100 + 100 + 1000 + 100 + 100,
|
||||
)
|
||||
})
|
||||
return (
|
||||
<Pressable
|
||||
<AnimatedPressable
|
||||
hitSlop={10}
|
||||
style={wrapperStyle}
|
||||
className={cn(
|
||||
"bg-red items-center justify-center rounded-lg",
|
||||
"bg-gray-4 items-center justify-center rounded-lg",
|
||||
sizeClassNames[size],
|
||||
className,
|
||||
)}
|
||||
|
|
@ -74,6 +98,6 @@ export const CopyButton = ({ onCopy, className, size = "sm" }: CopyButtonProps)
|
|||
<Animated.View style={pressedStyle}>
|
||||
<CheckFilledIcon color="#fff" height={sizeIconSize[size]} width={sizeIconSize[size]} />
|
||||
</Animated.View>
|
||||
</Pressable>
|
||||
</AnimatedPressable>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ export const FormLabel: FC<
|
|||
> = ({ label, optional, className, style }) => {
|
||||
return (
|
||||
<View className={cn("flex-row", className)} style={style}>
|
||||
<Text className="text-label text-lg font-medium capitalize">{label}</Text>
|
||||
<Text className="text-label font-medium capitalize">{label}</Text>
|
||||
{!optional && <Text className="text-red ml-1 align-sub">*</Text>}
|
||||
</View>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -50,21 +50,21 @@ export function Select<T>({
|
|||
onValueChange(value)
|
||||
})
|
||||
|
||||
const systemFill = useColor("systemFill")
|
||||
const systemFill = useColor("text")
|
||||
return (
|
||||
<>
|
||||
{/* Trigger */}
|
||||
<Pressable onPress={() => setIsOpen(!isOpen)}>
|
||||
<View
|
||||
className={cn(
|
||||
"border-system-fill/80 bg-system-fill/30 h-12 flex-row items-center rounded-lg border pl-4 pr-2",
|
||||
"border-system-fill/80 bg-system-fill/30 h-10 flex-row items-center rounded-lg border pl-4 pr-2",
|
||||
wrapperClassName,
|
||||
)}
|
||||
style={wrapperStyle}
|
||||
>
|
||||
<Text className="text-text">{valueToLabelMap.get(currentValue)}</Text>
|
||||
<View className="ml-auto shrink-0">
|
||||
<MingcuteDownLineIcon color={systemFill} />
|
||||
<MingcuteDownLineIcon color={systemFill} height={16} width={16} />
|
||||
</View>
|
||||
</View>
|
||||
</Pressable>
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ export const TextField: FC<TextInputProps & TextFieldProps> = ({
|
|||
return (
|
||||
<View
|
||||
className={cn(
|
||||
"bg-system-fill/40 relative h-12 flex-row items-center rounded-lg px-4",
|
||||
"bg-system-fill/40 relative h-10 flex-row items-center rounded-lg px-4",
|
||||
wrapperClassName,
|
||||
)}
|
||||
style={wrapperStyle}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,70 @@
|
|||
"use dom"
|
||||
import "@/src/global.css"
|
||||
import "remark-gh-alerts/styles/github-base.css"
|
||||
import "remark-gh-alerts/styles/github-colors-dark-media.css"
|
||||
import "remark-gh-alerts/styles/github-colors-light.css"
|
||||
|
||||
import { cn } from "@follow/utils"
|
||||
import type { Components } from "hast-util-to-jsx-runtime"
|
||||
import { toJsxRuntime } from "hast-util-to-jsx-runtime"
|
||||
import { Fragment, jsx, jsxs } from "react/jsx-runtime"
|
||||
import rehypeStringify from "rehype-stringify"
|
||||
import remarkDirective from "remark-directive"
|
||||
import remarkGfm from "remark-gfm"
|
||||
import remarkGithubAlerts from "remark-gh-alerts"
|
||||
import remarkParse from "remark-parse"
|
||||
import remarkRehype from "remark-rehype"
|
||||
import { unified } from "unified"
|
||||
import { useDarkMode } from "usehooks-ts"
|
||||
import { VFile } from "vfile"
|
||||
|
||||
import { useCSSInjection } from "@/src/theme/web"
|
||||
|
||||
export interface RemarkOptions {
|
||||
components: Partial<Components>
|
||||
}
|
||||
const parseMarkdown = (content: string, options?: Partial<RemarkOptions>) => {
|
||||
const file = new VFile(content)
|
||||
const { components } = options || {}
|
||||
|
||||
const pipeline = unified()
|
||||
.use(remarkParse)
|
||||
|
||||
.use(remarkGfm)
|
||||
.use(remarkGithubAlerts)
|
||||
|
||||
.use(remarkDirective)
|
||||
|
||||
.use(remarkRehype, { allowDangerousHtml: true })
|
||||
.use(rehypeStringify, { allowDangerousHtml: true })
|
||||
|
||||
const tree = pipeline.parse(content)
|
||||
|
||||
const hastTree = pipeline.runSync(tree, file)
|
||||
|
||||
return {
|
||||
content: toJsxRuntime(hastTree, {
|
||||
Fragment,
|
||||
ignoreInvalidStyle: true,
|
||||
jsx: (type, props, key) => jsx(type as any, props, key),
|
||||
jsxs: (type, props, key) => jsxs(type as any, props, key),
|
||||
passNode: true,
|
||||
components: {
|
||||
...components,
|
||||
},
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
const MarkdownWeb: WebComponent<{ value: string }> = ({ value }) => {
|
||||
useCSSInjection()
|
||||
|
||||
const { isDarkMode } = useDarkMode()
|
||||
return (
|
||||
<div className={cn("text-text prose min-w-0", isDarkMode ? "prose-invert" : "prose")}>
|
||||
{parseMarkdown(value).content}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default MarkdownWeb
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
import { forwardRef } from "react"
|
||||
import type { TextProps } from "react-native"
|
||||
import { Platform, StyleSheet, Text } from "react-native"
|
||||
|
||||
export const MonoText = forwardRef<Text, TextProps>((props, ref) => {
|
||||
return <Text ref={ref} {...props} style={StyleSheet.flatten([props.style, styles.mono])} />
|
||||
})
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
mono: {
|
||||
fontFamily: Platform.select({
|
||||
ios: "Menlo-Regular",
|
||||
android: "monospace",
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
|
@ -2,9 +2,10 @@ import { cn, regexpPathToPath } from "@follow/utils"
|
|||
import type { FC } from "react"
|
||||
import { useMemo } from "react"
|
||||
import type { UseFormReturn } from "react-hook-form"
|
||||
import { Clipboard, Text, View } from "react-native"
|
||||
import { Clipboard, ScrollView, View } from "react-native"
|
||||
|
||||
import { CopyButton } from "@/src/components/common/CopyButton"
|
||||
import { MonoText } from "@/src/components/ui/typography/MonoText"
|
||||
|
||||
export const PreviewUrl: FC<{
|
||||
watch: UseFormReturn<any>["watch"]
|
||||
|
|
@ -25,13 +26,25 @@ export const PreviewUrl: FC<{
|
|||
|
||||
const renderedPath = `rsshub://${routePrefix}${fullPath}`
|
||||
return (
|
||||
<View className={cn("relative min-w-0", className)}>
|
||||
<Text className="text-text/80 w-full whitespace-pre-line break-words">{renderedPath}</Text>
|
||||
<View className={cn("bg-gray-2/20 relative min-w-0 rounded-lg px-4 py-3", className)}>
|
||||
<ScrollView
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
contentContainerClassName="pr-12"
|
||||
>
|
||||
<MonoText
|
||||
className="text-text/80 w-full whitespace-nowrap break-words text-sm"
|
||||
numberOfLines={1}
|
||||
>
|
||||
{renderedPath}
|
||||
</MonoText>
|
||||
</ScrollView>
|
||||
<CopyButton
|
||||
size="tiny"
|
||||
onCopy={() => {
|
||||
Clipboard.setString(renderedPath)
|
||||
}}
|
||||
className="absolute right-0 top-0"
|
||||
className="absolute right-1.5 top-2"
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -12,8 +12,8 @@ import { KeyboardProvider } from "react-native-keyboard-controller"
|
|||
|
||||
import { sqlite } from "../database"
|
||||
import { queryClient } from "../lib/query-client"
|
||||
import { getCurrentColors } from "../theme/colors"
|
||||
import { DarkTheme, DefaultTheme } from "../theme/navigation"
|
||||
import { getCurrentColors } from "../theme/utils"
|
||||
import { MigrationProvider } from "./migration"
|
||||
|
||||
export const RootProviders = ({ children }: { children: ReactNode }) => {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { Stack } from "expo-router"
|
||||
import { useColorScheme } from "react-native"
|
||||
|
||||
import { getSystemBackgroundColor } from "@/src/theme/colors"
|
||||
import { getSystemBackgroundColor } from "@/src/theme/utils"
|
||||
|
||||
export default function HeadlessLayout() {
|
||||
useColorScheme()
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { router, Stack, useLocalSearchParams } from "expo-router"
|
|||
import { useEffect, useMemo } from "react"
|
||||
import type { UseFormReturn } from "react-hook-form"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { View } from "react-native"
|
||||
import { Linking, Text, TouchableOpacity, View } from "react-native"
|
||||
import { KeyboardAwareScrollView } from "react-native-keyboard-controller"
|
||||
import { z } from "zod"
|
||||
|
||||
|
|
@ -14,6 +14,7 @@ import { ModalHeaderCloseButton } from "@/src/components/common/ModalSharedCompo
|
|||
import { FormLabel } from "@/src/components/ui/form/Label"
|
||||
import { Select } from "@/src/components/ui/form/Select"
|
||||
import { TextField } from "@/src/components/ui/form/TextField"
|
||||
import MarkdownWeb from "@/src/components/ui/typography/MarkdownWeb"
|
||||
import { PreviewUrl } from "@/src/modules/rsshub/preview-url"
|
||||
|
||||
interface RsshubFormParams {
|
||||
|
|
@ -102,19 +103,20 @@ function FormImpl({ route, routePrefix, name }: RsshubFormParams) {
|
|||
|
||||
return (
|
||||
<PortalProvider>
|
||||
<KeyboardAwareScrollView>
|
||||
<KeyboardAwareScrollView className="bg-system-grouped-background">
|
||||
<Stack.Screen
|
||||
options={{ headerLeft: ModalHeaderCloseButton, headerTitle: `${name} - ${routeName}` }}
|
||||
/>
|
||||
|
||||
<PreviewUrl
|
||||
className="my-6 px-6"
|
||||
className="m-2 mb-6"
|
||||
watch={form.watch}
|
||||
path={route.path}
|
||||
routePrefix={routePrefix}
|
||||
/>
|
||||
{/* Form */}
|
||||
<View className="gap-4 px-2">
|
||||
|
||||
<View className="bg-system-background mx-2 gap-4 rounded-lg px-3 py-6">
|
||||
{keys.map((keyItem) => {
|
||||
const parameters = normalizeRSSHubParameters(route.parameters[keyItem.name])
|
||||
const formRegister = form.register(keyItem.name)
|
||||
|
|
@ -145,15 +147,41 @@ function FormImpl({ route, routePrefix, name }: RsshubFormParams) {
|
|||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!!parameters && (
|
||||
<Text className="text-text/80 ml-2 mt-2 text-xs">{parameters.description}</Text>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
})}
|
||||
</View>
|
||||
<Maintainers maintainers={route.maintainers} />
|
||||
|
||||
<View className="mx-4 mt-4">
|
||||
<MarkdownWeb value={route.description} dom={{ matchContents: true }} />
|
||||
</View>
|
||||
</KeyboardAwareScrollView>
|
||||
</PortalProvider>
|
||||
)
|
||||
}
|
||||
|
||||
const Maintainers = ({ maintainers }: { maintainers?: string[] }) => {
|
||||
if (!maintainers || maintainers.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="text-text/80 mx-4 mt-2 flex flex-row flex-wrap gap-x-1 text-sm">
|
||||
<Text className="text-text/80 text-xs">This feed is provided by RSSHub, with credit to </Text>
|
||||
{maintainers.map((m) => (
|
||||
<TouchableOpacity key={m} onPress={() => Linking.openURL(`https://github.com/${m}`)}>
|
||||
<Text className="text-text/50 text-xs">@{m}</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const normalizeRSSHubParameters = (parameters: RSSHubParameter): RSSHubParameterObject | null =>
|
||||
parameters
|
||||
? typeof parameters === "string"
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { useColorScheme } from "nativewind"
|
|||
import { DebugButton } from "../modules/debug"
|
||||
import { RootProviders } from "../providers"
|
||||
import { usePrefetchSessionUser } from "../store/user/hooks"
|
||||
import { getSystemBackgroundColor } from "../theme/colors"
|
||||
import { getSystemBackgroundColor } from "../theme/utils"
|
||||
|
||||
export default function RootLayout() {
|
||||
useColorScheme()
|
||||
|
|
|
|||
|
|
@ -1,17 +1,18 @@
|
|||
import { useColorScheme, vars } from "nativewind"
|
||||
import { useMemo } from "react"
|
||||
import type { StyleProp, ViewStyle } from "react-native"
|
||||
import { Appearance, StyleSheet } from "react-native"
|
||||
|
||||
// @ts-expect-error
|
||||
const IS_DOM = typeof ReactNativeWebView !== "undefined"
|
||||
|
||||
const varPrefix = "--color"
|
||||
export const accentColor = "#FF5C00"
|
||||
const buildVars = (_vars: Record<string, string>) => {
|
||||
export const buildVars = (_vars: Record<string, string>) => {
|
||||
const cssVars = {} as Record<`${typeof varPrefix}-${string}`, string>
|
||||
for (const [key, value] of Object.entries(_vars)) {
|
||||
cssVars[`${varPrefix}-${key}`] = value
|
||||
}
|
||||
|
||||
return vars(cssVars)
|
||||
return IS_DOM ? cssVars : vars(cssVars)
|
||||
}
|
||||
|
||||
const lightPalette = {
|
||||
|
|
@ -54,13 +55,13 @@ const darkPalette = {
|
|||
gray5: "44 44 46",
|
||||
gray6: "28 28 30",
|
||||
}
|
||||
const palette = {
|
||||
export const palette = {
|
||||
// iOS color palette https://developer.apple.com/design/human-interface-guidelines/color
|
||||
light: buildVars(lightPalette),
|
||||
dark: buildVars(darkPalette),
|
||||
}
|
||||
|
||||
const lightVariants = {
|
||||
export const lightVariants = {
|
||||
// UIKit Colors
|
||||
label: "0 0 0",
|
||||
secondaryLabel: "122 122 122",
|
||||
|
|
@ -92,7 +93,7 @@ const lightVariants = {
|
|||
// Extended colors
|
||||
disabled: "235 235 228",
|
||||
}
|
||||
const darkVariants = {
|
||||
export const darkVariants = {
|
||||
// UIKit Colors
|
||||
label: "255 255 255",
|
||||
secondaryLabel: "172 172 178",
|
||||
|
|
@ -124,16 +125,6 @@ const darkVariants = {
|
|||
// Extended colors
|
||||
disabled: "85 85 85",
|
||||
}
|
||||
const variants = {
|
||||
light: buildVars(lightVariants),
|
||||
dark: buildVars(darkVariants),
|
||||
}
|
||||
|
||||
export const getCurrentColors = () => {
|
||||
const colorScheme = Appearance.getColorScheme() || "light"
|
||||
|
||||
return StyleSheet.compose(variants[colorScheme], palette[colorScheme]) as StyleProp<ViewStyle>
|
||||
}
|
||||
|
||||
/// Utils
|
||||
|
||||
|
|
@ -142,15 +133,33 @@ const toRgb = (hex: string) => {
|
|||
return `rgb(${r} ${g} ${b})`
|
||||
}
|
||||
|
||||
export const getSystemBackgroundColor = () => {
|
||||
const colorScheme = Appearance.getColorScheme() || "light"
|
||||
|
||||
const colors = colorScheme === "light" ? lightVariants : darkVariants
|
||||
return toRgb(colors.systemBackground)
|
||||
const mergedLightColors = {
|
||||
...lightVariants,
|
||||
...lightPalette,
|
||||
}
|
||||
const mergedDarkColors = {
|
||||
...darkVariants,
|
||||
...darkPalette,
|
||||
}
|
||||
const mergedColors = {
|
||||
light: mergedLightColors,
|
||||
dark: mergedDarkColors,
|
||||
}
|
||||
|
||||
export const useColor = (color: keyof typeof lightVariants | keyof typeof darkVariants) => {
|
||||
export const colorVariants = {
|
||||
light: buildVars(lightVariants),
|
||||
dark: buildVars(darkVariants),
|
||||
}
|
||||
|
||||
export const useColor = (color: keyof typeof mergedLightColors) => {
|
||||
const { colorScheme } = useColorScheme()
|
||||
const colors = colorScheme === "light" ? lightVariants : darkVariants
|
||||
const colors = mergedColors[colorScheme || "light"]
|
||||
return useMemo(() => toRgb(colors[color]), [color, colors])
|
||||
}
|
||||
|
||||
export const useColors = () => {
|
||||
const { colorScheme } = useColorScheme()
|
||||
return mergedColors[colorScheme || "light"]
|
||||
}
|
||||
|
||||
export type Colors = typeof mergedLightColors
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
import type { StyleProp, ViewStyle } from "react-native"
|
||||
import { Appearance, StyleSheet } from "react-native"
|
||||
|
||||
import { colorVariants, darkVariants, lightVariants, palette } from "./colors"
|
||||
|
||||
export const getCurrentColors = () => {
|
||||
const colorScheme = Appearance.getColorScheme() || "light"
|
||||
|
||||
return StyleSheet.compose(
|
||||
colorVariants[colorScheme],
|
||||
palette[colorScheme],
|
||||
) as StyleProp<ViewStyle>
|
||||
}
|
||||
|
||||
export const getSystemBackgroundColor = () => {
|
||||
const colorScheme = Appearance.getColorScheme() || "light"
|
||||
|
||||
const colors = colorScheme === "light" ? lightVariants : darkVariants
|
||||
return toRgb(colors.systemBackground)
|
||||
}
|
||||
|
||||
const toRgb = (hex: string) => {
|
||||
const [r, g, b] = hex.split(" ").map((s) => Number.parseInt(s))
|
||||
return `rgb(${r} ${g} ${b})`
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
import { useInsertionEffect } from "react"
|
||||
import { useDarkMode } from "usehooks-ts"
|
||||
|
||||
import { colorVariants, palette } from "./colors"
|
||||
|
||||
export const useCSSInjection = () => {
|
||||
const isDark = useDarkMode().isDarkMode
|
||||
|
||||
useInsertionEffect(() => {
|
||||
const style = document.createElement("style")
|
||||
|
||||
const vars1 = colorVariants[isDark ? "dark" : "light"]
|
||||
const vars2 = palette[isDark ? "dark" : "light"]
|
||||
|
||||
style.innerHTML = `:root {${[...Object.entries(vars1), ...Object.entries(vars2)]
|
||||
.map(([key, value]) => `${key}: ${value};`)
|
||||
.join("\n")}}`
|
||||
|
||||
document.head.append(style)
|
||||
return () => {
|
||||
style.remove()
|
||||
}
|
||||
}, [isDark])
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ export default resolveConfig({
|
|||
content: ["./src/**/*.{js,jsx,ts,tsx}"],
|
||||
presets: [require("nativewind/preset")],
|
||||
|
||||
plugins: [require("@tailwindcss/typography")],
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: {
|
||||
|
|
|
|||
|
|
@ -460,6 +460,9 @@ importers:
|
|||
dayjs:
|
||||
specifier: 1.11.13
|
||||
version: 1.11.13
|
||||
es-toolkit:
|
||||
specifier: 1.29.0
|
||||
version: 1.29.0
|
||||
expo:
|
||||
specifier: 52.0.18
|
||||
version: 52.0.18(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@expo/metro-runtime@4.0.0(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)))(bufferutil@4.0.8)(encoding@0.1.13)(graphql@16.8.1)(react-native-webview@13.12.5(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1)
|
||||
|
|
@ -13945,7 +13948,7 @@ packages:
|
|||
'@microsoft/api-extractor': ^7.36.0
|
||||
'@swc/core': ^1
|
||||
postcss: ^8.4.12
|
||||
typescript: '>=4.5.0'
|
||||
typescript: 5.6.3
|
||||
peerDependenciesMeta:
|
||||
'@microsoft/api-extractor':
|
||||
optional: true
|
||||
|
|
|
|||
Loading…
Reference in New Issue