diff options
Diffstat (limited to 'apps/mobile')
35 files changed, 76 insertions, 93 deletions
diff --git a/apps/mobile/.eslintrc.js b/apps/mobile/.eslintrc.js deleted file mode 100644 index 53beac49..00000000 --- a/apps/mobile/.eslintrc.js +++ /dev/null @@ -1,4 +0,0 @@ -module.exports = { - root: true, - extends: ["universe/native"], -}; diff --git a/apps/mobile/app.json b/apps/mobile/app.json index e16baa37..20424263 100644 --- a/apps/mobile/app.json +++ b/apps/mobile/app.json @@ -12,9 +12,7 @@ "resizeMode": "contain", "backgroundColor": "#ffffff" }, - "assetBundlePatterns": [ - "**/*" - ], + "assetBundlePatterns": ["**/*"], "ios": { "supportsTablet": true, "bundleIdentifier": "app.hoarder.hoardermobile", diff --git a/apps/mobile/app/_layout.tsx b/apps/mobile/app/_layout.tsx index 6304ced5..b338af5e 100644 --- a/apps/mobile/app/_layout.tsx +++ b/apps/mobile/app/_layout.tsx @@ -1,13 +1,12 @@ import "@/globals.css"; import "expo-dev-client"; +import { useEffect } from "react"; +import { View } from "react-native"; import { useRouter } from "expo-router"; import { Stack } from "expo-router/stack"; import { useShareIntent } from "expo-share-intent"; import { StatusBar } from "expo-status-bar"; -import { useEffect } from "react"; -import { View } from "react-native"; - import { useLastSharedIntent } from "@/lib/last-shared-intent"; import { Providers } from "@/lib/providers"; diff --git a/apps/mobile/app/dashboard/(tabs)/_layout.tsx b/apps/mobile/app/dashboard/(tabs)/_layout.tsx index 5b2d810a..ce73a5c9 100644 --- a/apps/mobile/app/dashboard/(tabs)/_layout.tsx +++ b/apps/mobile/app/dashboard/(tabs)/_layout.tsx @@ -1,6 +1,6 @@ +import React from "react"; import { Tabs } from "expo-router"; import { ClipboardList, Home, Search, Settings } from "lucide-react-native"; -import React from "react"; export default function TabLayout() { return ( diff --git a/apps/mobile/app/dashboard/(tabs)/index.tsx b/apps/mobile/app/dashboard/(tabs)/index.tsx index b2349525..fe15956c 100644 --- a/apps/mobile/app/dashboard/(tabs)/index.tsx +++ b/apps/mobile/app/dashboard/(tabs)/index.tsx @@ -1,8 +1,7 @@ -import { Link, Stack } from "expo-router"; -import { SquarePen, Link as LinkIcon } from "lucide-react-native"; import { View } from "react-native"; - +import { Link, Stack } from "expo-router"; import BookmarkList from "@/components/bookmarks/BookmarkList"; +import { Link as LinkIcon, SquarePen } from "lucide-react-native"; function HeaderRight() { return ( diff --git a/apps/mobile/app/dashboard/(tabs)/lists.tsx b/apps/mobile/app/dashboard/(tabs)/lists.tsx index b534ddda..a293757b 100644 --- a/apps/mobile/app/dashboard/(tabs)/lists.tsx +++ b/apps/mobile/app/dashboard/(tabs)/lists.tsx @@ -1,7 +1,6 @@ -import { Link } from "expo-router"; import { useEffect, useState } from "react"; import { FlatList, View } from "react-native"; - +import { Link } from "expo-router"; import { api } from "@/lib/trpc"; export default function Lists() { diff --git a/apps/mobile/app/dashboard/(tabs)/search.tsx b/apps/mobile/app/dashboard/(tabs)/search.tsx index 980cab36..0025262e 100644 --- a/apps/mobile/app/dashboard/(tabs)/search.tsx +++ b/apps/mobile/app/dashboard/(tabs)/search.tsx @@ -1,12 +1,11 @@ -import { keepPreviousData } from "@tanstack/react-query"; import { useState } from "react"; import { View } from "react-native"; -import { useDebounce } from "use-debounce"; - import BookmarkList from "@/components/bookmarks/BookmarkList"; import { Divider } from "@/components/ui/Divider"; import { Input } from "@/components/ui/Input"; import { api } from "@/lib/trpc"; +import { keepPreviousData } from "@tanstack/react-query"; +import { useDebounce } from "use-debounce"; export default function Search() { const [search, setSearch] = useState(""); diff --git a/apps/mobile/app/dashboard/(tabs)/settings.tsx b/apps/mobile/app/dashboard/(tabs)/settings.tsx index 9f86d5ec..81b21fc8 100644 --- a/apps/mobile/app/dashboard/(tabs)/settings.tsx +++ b/apps/mobile/app/dashboard/(tabs)/settings.tsx @@ -1,7 +1,6 @@ -import { useRouter } from "expo-router"; import { useEffect } from "react"; import { Text, View } from "react-native"; - +import { useRouter } from "expo-router"; import Logo from "@/components/Logo"; import { Button } from "@/components/ui/Button"; import { useSession } from "@/lib/session"; diff --git a/apps/mobile/app/dashboard/add-link.tsx b/apps/mobile/app/dashboard/add-link.tsx index 69a9c7a2..568a36b6 100644 --- a/apps/mobile/app/dashboard/add-link.tsx +++ b/apps/mobile/app/dashboard/add-link.tsx @@ -1,7 +1,6 @@ -import { useRouter } from "expo-router"; import { useState } from "react"; -import { View, Text } from "react-native"; - +import { Text, View } from "react-native"; +import { useRouter } from "expo-router"; import { Button } from "@/components/ui/Button"; import { Input } from "@/components/ui/Input"; import { api } from "@/lib/trpc"; @@ -24,9 +23,9 @@ export default function AddNote() { }, onError: (e) => { let message; - if (e.data?.code === "BAD_REQUEST") { - const error = JSON.parse(e.message)[0]; - message = error.message; + if (e.data?.zodError) { + const zodError = e.data.zodError; + message = JSON.stringify(zodError); } else { message = `Something went wrong: ${e.message}`; } diff --git a/apps/mobile/app/dashboard/add-note.tsx b/apps/mobile/app/dashboard/add-note.tsx index cf775a15..1f903e94 100644 --- a/apps/mobile/app/dashboard/add-note.tsx +++ b/apps/mobile/app/dashboard/add-note.tsx @@ -1,7 +1,6 @@ -import { useRouter } from "expo-router"; import { useState } from "react"; -import { View, Text } from "react-native"; - +import { Text, View } from "react-native"; +import { useRouter } from "expo-router"; import { Button } from "@/components/ui/Button"; import { Input } from "@/components/ui/Input"; import { api } from "@/lib/trpc"; @@ -24,9 +23,9 @@ export default function AddNote() { }, onError: (e) => { let message; - if (e.data?.code === "BAD_REQUEST") { - const error = JSON.parse(e.message)[0]; - message = error.message; + if (e.data?.zodError) { + const zodError = e.data.zodError; + message = JSON.stringify(zodError); } else { message = `Something went wrong: ${e.message}`; } diff --git a/apps/mobile/app/dashboard/archive.tsx b/apps/mobile/app/dashboard/archive.tsx index d75cfe22..2c559684 100644 --- a/apps/mobile/app/dashboard/archive.tsx +++ b/apps/mobile/app/dashboard/archive.tsx @@ -1,5 +1,4 @@ import { View } from "react-native"; - import BookmarkList from "@/components/bookmarks/BookmarkList"; export default function Archive() { diff --git a/apps/mobile/app/dashboard/favourites.tsx b/apps/mobile/app/dashboard/favourites.tsx index 90374f18..42a139f9 100644 --- a/apps/mobile/app/dashboard/favourites.tsx +++ b/apps/mobile/app/dashboard/favourites.tsx @@ -1,5 +1,4 @@ import { View } from "react-native"; - import BookmarkList from "@/components/bookmarks/BookmarkList"; export default function Favourites() { diff --git a/apps/mobile/app/dashboard/lists/[slug].tsx b/apps/mobile/app/dashboard/lists/[slug].tsx index 54744874..fdd67763 100644 --- a/apps/mobile/app/dashboard/lists/[slug].tsx +++ b/apps/mobile/app/dashboard/lists/[slug].tsx @@ -1,6 +1,5 @@ -import { useLocalSearchParams, Stack } from "expo-router"; import { View } from "react-native"; - +import { Stack, useLocalSearchParams } from "expo-router"; import BookmarkList from "@/components/bookmarks/BookmarkList"; import FullPageSpinner from "@/components/ui/FullPageSpinner"; import { api } from "@/lib/trpc"; diff --git a/apps/mobile/app/error.tsx b/apps/mobile/app/error.tsx index 2ca227a4..d0e4a7df 100644 --- a/apps/mobile/app/error.tsx +++ b/apps/mobile/app/error.tsx @@ -1,4 +1,4 @@ -import { View, Text } from "react-native"; +import { Text, View } from "react-native"; export default function ErrorPage() { return ( diff --git a/apps/mobile/app/index.tsx b/apps/mobile/app/index.tsx index 5ce20cda..f075fd6d 100644 --- a/apps/mobile/app/index.tsx +++ b/apps/mobile/app/index.tsx @@ -1,7 +1,6 @@ -import { useRouter } from "expo-router"; import { useEffect } from "react"; import { View } from "react-native"; - +import { useRouter } from "expo-router"; import { useSession } from "@/lib/session"; export default function App() { diff --git a/apps/mobile/app/sharing.tsx b/apps/mobile/app/sharing.tsx index 64bbd933..f9f423b5 100644 --- a/apps/mobile/app/sharing.tsx +++ b/apps/mobile/app/sharing.tsx @@ -1,10 +1,10 @@ -import { Link, useLocalSearchParams, useRouter } from "expo-router"; -import { ShareIntent, useShareIntent } from "expo-share-intent"; +import type { ShareIntent } from "expo-share-intent"; import { useEffect, useMemo, useState } from "react"; -import { View, Text } from "react-native"; -import { z } from "zod"; - +import { Text, View } from "react-native"; +import { Link, useLocalSearchParams, useRouter } from "expo-router"; +import { useShareIntent } from "expo-share-intent"; import { api } from "@/lib/trpc"; +import { z } from "zod"; type Mode = | { type: "idle" } @@ -18,7 +18,7 @@ function SaveBookmark({ setMode }: { setMode: (mode: Mode) => void }) { const params = useLocalSearchParams(); const shareIntent = useMemo(() => { - if (params && params.shareIntent) { + if (params?.shareIntent) { if (typeof params.shareIntent === "string") { return JSON.parse(params.shareIntent) as ShareIntent; } diff --git a/apps/mobile/app/signin.tsx b/apps/mobile/app/signin.tsx index a89b0087..63e48f22 100644 --- a/apps/mobile/app/signin.tsx +++ b/apps/mobile/app/signin.tsx @@ -1,7 +1,6 @@ -import { useRouter } from "expo-router"; import { useEffect, useState } from "react"; -import { View, Text } from "react-native"; - +import { Text, View } from "react-native"; +import { useRouter } from "expo-router"; import Logo from "@/components/Logo"; import { Button } from "@/components/ui/Button"; import { Input } from "@/components/ui/Input"; diff --git a/apps/mobile/components/Logo.tsx b/apps/mobile/components/Logo.tsx index 57f7a5c3..f5f823b5 100644 --- a/apps/mobile/components/Logo.tsx +++ b/apps/mobile/components/Logo.tsx @@ -1,5 +1,5 @@ +import { Text, View } from "react-native"; import { PackageOpen } from "lucide-react-native"; -import { View, Text } from "react-native"; export default function Logo() { return ( diff --git a/apps/mobile/components/bookmarks/BookmarkCard.tsx b/apps/mobile/components/bookmarks/BookmarkCard.tsx index 25947790..93a059f8 100644 --- a/apps/mobile/components/bookmarks/BookmarkCard.tsx +++ b/apps/mobile/components/bookmarks/BookmarkCard.tsx @@ -1,16 +1,16 @@ -import { ZBookmark } from "@hoarder/trpc/types/bookmarks"; -import * as WebBrowser from "expo-web-browser"; -import { Star, Archive, Trash, ArchiveRestore } from "lucide-react-native"; -import { View, Text, Image, ScrollView, Pressable } from "react-native"; +import { Image, Pressable, ScrollView, Text, View } from "react-native"; import Markdown from "react-native-markdown-display"; +import * as WebBrowser from "expo-web-browser"; +import { api } from "@/lib/trpc"; +import { Archive, ArchiveRestore, Star, Trash } from "lucide-react-native"; + +import type { ZBookmark } from "@hoarder/trpc/types/bookmarks"; import { ActionButton } from "../ui/ActionButton"; import { Divider } from "../ui/Divider"; import { Skeleton } from "../ui/Skeleton"; import { useToast } from "../ui/Toast"; -import { api } from "@/lib/trpc"; - const MAX_LOADING_MSEC = 30 * 1000; export function isBookmarkStillCrawling(bookmark: ZBookmark) { @@ -155,6 +155,7 @@ function LinkCard({ bookmark }: { bookmark: ZBookmark }) { /> ) : ( <Image + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment source={require("@/assets/blur.jpeg")} className="h-56 w-full rounded-t-lg" /> @@ -168,7 +169,7 @@ function LinkCard({ bookmark }: { bookmark: ZBookmark }) { className="line-clamp-2 text-xl font-bold" onPress={() => WebBrowser.openBrowserAsync(url)} > - {bookmark.content.title || parsedUrl.host} + {bookmark.content.title ?? parsedUrl.host} </Text> <TagList bookmark={bookmark} /> <Divider orientation="vertical" className="mt-2 h-0.5 w-full" /> diff --git a/apps/mobile/components/bookmarks/BookmarkList.tsx b/apps/mobile/components/bookmarks/BookmarkList.tsx index 8e408709..79d3d79e 100644 --- a/apps/mobile/components/bookmarks/BookmarkList.tsx +++ b/apps/mobile/components/bookmarks/BookmarkList.tsx @@ -1,11 +1,10 @@ import { useEffect, useState } from "react"; import { Text, View } from "react-native"; import Animated, { LinearTransition } from "react-native-reanimated"; +import { api } from "@/lib/trpc"; -import BookmarkCard from "./BookmarkCard"; import FullPageSpinner from "../ui/FullPageSpinner"; - -import { api } from "@/lib/trpc"; +import BookmarkCard from "./BookmarkCard"; export default function BookmarkList({ favourited, diff --git a/apps/mobile/components/ui/ActionButton.tsx b/apps/mobile/components/ui/ActionButton.tsx index c51eb332..1f2e05ca 100644 --- a/apps/mobile/components/ui/ActionButton.tsx +++ b/apps/mobile/components/ui/ActionButton.tsx @@ -1,4 +1,5 @@ -import { ActivityIndicator, Pressable, PressableProps } from "react-native"; +import type { PressableProps } from "react-native"; +import { ActivityIndicator, Pressable } from "react-native"; export function ActionButton({ children, diff --git a/apps/mobile/components/ui/Button.tsx b/apps/mobile/components/ui/Button.tsx index 4c3cbc69..0f3b4ab3 100644 --- a/apps/mobile/components/ui/Button.tsx +++ b/apps/mobile/components/ui/Button.tsx @@ -1,7 +1,7 @@ -import { type VariantProps, cva } from "class-variance-authority"; +import type { VariantProps } from "class-variance-authority"; import { Text, TouchableOpacity } from "react-native"; - import { cn } from "@/lib/utils"; +import { cva } from "class-variance-authority"; const buttonVariants = cva( "flex flex-row items-center justify-center rounded-md", diff --git a/apps/mobile/components/ui/Divider.tsx b/apps/mobile/components/ui/Divider.tsx index 1da0a71e..cf1b4624 100644 --- a/apps/mobile/components/ui/Divider.tsx +++ b/apps/mobile/components/ui/Divider.tsx @@ -1,5 +1,4 @@ import { View } from "react-native"; - import { cn } from "@/lib/utils"; function Divider({ diff --git a/apps/mobile/components/ui/FullPageSpinner.tsx b/apps/mobile/components/ui/FullPageSpinner.tsx index 01187f11..89b66090 100644 --- a/apps/mobile/components/ui/FullPageSpinner.tsx +++ b/apps/mobile/components/ui/FullPageSpinner.tsx @@ -1,4 +1,4 @@ -import { View, ActivityIndicator } from "react-native"; +import { ActivityIndicator, View } from "react-native"; export default function FullPageSpinner() { return ( diff --git a/apps/mobile/components/ui/Input.tsx b/apps/mobile/components/ui/Input.tsx index 2fcb2764..01c9fb2f 100644 --- a/apps/mobile/components/ui/Input.tsx +++ b/apps/mobile/components/ui/Input.tsx @@ -1,6 +1,5 @@ import { forwardRef } from "react"; import { Text, TextInput, View } from "react-native"; - import { cn } from "@/lib/utils"; export interface InputProps @@ -15,14 +14,16 @@ const Input = forwardRef<React.ElementRef<typeof TextInput>, InputProps>( <View className={cn("flex flex-col gap-1.5", className)}> {label && <Text className={cn("text-base", labelClasses)}>{label}</Text>} <TextInput + ref={ref} className={cn( inputClasses, - "border-input rounded-lg border px-4 py-2.5", + "rounded-lg border border-input px-4 py-2.5", )} {...props} /> </View> ), ); +Input.displayName = "Input"; export { Input }; diff --git a/apps/mobile/components/ui/Skeleton.tsx b/apps/mobile/components/ui/Skeleton.tsx index 68b22e1e..3287b9ef 100644 --- a/apps/mobile/components/ui/Skeleton.tsx +++ b/apps/mobile/components/ui/Skeleton.tsx @@ -1,6 +1,6 @@ +import type { View } from "react-native"; import { useEffect, useRef } from "react"; -import { Animated, type View } from "react-native"; - +import { Animated } from "react-native"; import { cn } from "@/lib/utils"; function Skeleton({ @@ -28,7 +28,7 @@ function Skeleton({ return ( <Animated.View - className={cn("bg-muted rounded-md", className)} + className={cn("rounded-md bg-muted", className)} style={[{ opacity: fadeAnim }]} {...props} /> diff --git a/apps/mobile/components/ui/Toast.tsx b/apps/mobile/components/ui/Toast.tsx index fb319f84..9aa25e5c 100644 --- a/apps/mobile/components/ui/Toast.tsx +++ b/apps/mobile/components/ui/Toast.tsx @@ -1,6 +1,5 @@ import { createContext, useContext, useEffect, useRef, useState } from "react"; import { Animated, Text, View } from "react-native"; - import { cn } from "@/lib/utils"; const toastVariants = { @@ -67,7 +66,7 @@ function Toast({ ], }} > - <Text className="text-background text-left font-semibold">{message}</Text> + <Text className="text-left font-semibold text-background">{message}</Text> {showProgress && ( <View className="mt-2 rounded"> <Animated.View diff --git a/apps/mobile/lib/providers.tsx b/apps/mobile/lib/providers.tsx index 1717afb2..036e8ae2 100644 --- a/apps/mobile/lib/providers.tsx +++ b/apps/mobile/lib/providers.tsx @@ -1,13 +1,12 @@ +import { useEffect, useState } from "react"; +import { ToastProvider } from "@/components/ui/Toast"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { httpBatchLink } from "@trpc/client"; -import { useEffect, useState } from "react"; import superjson from "superjson"; import useAppSettings, { getAppSettings } from "./settings"; import { api } from "./trpc"; -import { ToastProvider } from "@/components/ui/Toast"; - function getTRPCClient(address: string) { return api.createClient({ links: [ @@ -16,10 +15,9 @@ function getTRPCClient(address: string) { async headers() { const settings = await getAppSettings(); return { - Authorization: - settings && settings.apiKey - ? `Bearer ${settings.apiKey}` - : undefined, + Authorization: settings?.apiKey + ? `Bearer ${settings.apiKey}` + : undefined, }; }, transformer: superjson, diff --git a/apps/mobile/lib/session.ts b/apps/mobile/lib/session.ts index e2ab245b..071748b9 100644 --- a/apps/mobile/lib/session.ts +++ b/apps/mobile/lib/session.ts @@ -10,7 +10,7 @@ export function useSession() { const logout = useCallback(() => { setSettings({ ...settings, apiKey: undefined }); - }, [settings]); + }, [settings, setSettings]); return { isLoggedIn, diff --git a/apps/mobile/lib/settings.ts b/apps/mobile/lib/settings.ts index 21f40528..13de067e 100644 --- a/apps/mobile/lib/settings.ts +++ b/apps/mobile/lib/settings.ts @@ -4,14 +4,15 @@ import { useStorageState } from "./storage-state"; const SETTING_NAME = "settings"; -export type Settings = { +export interface Settings { apiKey?: string; address: string; -}; +} export default function useAppSettings() { - let [[isLoading, settings], setSettings] = - useStorageState<Settings>(SETTING_NAME); + const [settingsState, setSettings] = useStorageState<Settings>(SETTING_NAME); + const [isLoading] = settingsState; + let [, settings] = settingsState; settings ||= { address: "https://demo.hoarder.app", diff --git a/apps/mobile/lib/storage-state.ts b/apps/mobile/lib/storage-state.ts index 4988f0e0..f45ddfe5 100644 --- a/apps/mobile/lib/storage-state.ts +++ b/apps/mobile/lib/storage-state.ts @@ -1,5 +1,5 @@ -import * as SecureStore from "expo-secure-store"; import * as React from "react"; +import * as SecureStore from "expo-secure-store"; type UseStateHook<T> = [[boolean, T | null], (value: T | null) => void]; @@ -8,7 +8,7 @@ function useAsyncState<T>( ): UseStateHook<T> { return React.useReducer( ( - state: [boolean, T | null], + _state: [boolean, T | null], action: T | null = null, ): [boolean, T | null] => [false, action], initialValue, @@ -34,7 +34,7 @@ export function useStorageState<T>(key: string): UseStateHook<T> { setState(null); return null; } - setState(JSON.parse(value)); + setState(JSON.parse(value) as T); }); }, [key]); diff --git a/apps/mobile/lib/trpc.ts b/apps/mobile/lib/trpc.ts index 6b428bd9..9b025df1 100644 --- a/apps/mobile/lib/trpc.ts +++ b/apps/mobile/lib/trpc.ts @@ -1,4 +1,5 @@ -import type { AppRouter } from "@hoarder/trpc/routers/_app"; import { createTRPCReact } from "@trpc/react-query"; +import type { AppRouter } from "@hoarder/trpc/routers/_app"; + export const api = createTRPCReact<AppRouter>(); diff --git a/apps/mobile/lib/utils.ts b/apps/mobile/lib/utils.ts index 365058ce..88283f01 100644 --- a/apps/mobile/lib/utils.ts +++ b/apps/mobile/lib/utils.ts @@ -1,4 +1,5 @@ -import { type ClassValue, clsx } from "clsx"; +import type { ClassValue } from "clsx"; +import { clsx } from "clsx"; import { twMerge } from "tailwind-merge"; export function cn(...inputs: ClassValue[]) { diff --git a/apps/mobile/metro.config.js b/apps/mobile/metro.config.js index c5630a83..f9679cb2 100644 --- a/apps/mobile/metro.config.js +++ b/apps/mobile/metro.config.js @@ -2,11 +2,11 @@ const { getDefaultConfig } = require("expo/metro-config"); const { FileStore } = require("metro-cache"); const { withNativeWind } = require("nativewind/metro"); - const path = require("path"); module.exports = withTurborepoManagedCache( withMonorepoPaths( + // eslint-disable-next-line no-undef withNativeWind(getDefaultConfig(__dirname), { input: "./globals.css", configPath: "./tailwind.config.ts", @@ -23,6 +23,7 @@ module.exports = withTurborepoManagedCache( * @returns {import('expo/metro-config').MetroConfig} */ function withMonorepoPaths(config) { + // eslint-disable-next-line no-undef const projectRoot = __dirname; const workspaceRoot = path.resolve(projectRoot, "../.."); @@ -49,10 +50,8 @@ function withMonorepoPaths(config) { */ function withTurborepoManagedCache(config) { config.cacheStores = [ + // eslint-disable-next-line no-undef new FileStore({ root: path.join(__dirname, "node_modules/.cache/metro") }), ]; return config; } - - - diff --git a/apps/mobile/tailwind.config.ts b/apps/mobile/tailwind.config.ts index ce0059d4..fef31daf 100644 --- a/apps/mobile/tailwind.config.ts +++ b/apps/mobile/tailwind.config.ts @@ -50,6 +50,7 @@ const config = { }, }, borderWidth: { + // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-assignment hairline: hairlineWidth(), }, keyframes: { |
