From 1317b250ae5570996652435c58090878c26fb486 Mon Sep 17 00:00:00 2001 From: Bartosz Kaszubowski Date: Thu, 11 Dec 2025 23:57:04 +0100 Subject: [PATCH 01/30] add tailwind styling setup # Conflicts: # bun.lock # package.json --- components/Libraries.tsx | 15 ++++------- components/NotFoundContent.tsx | 38 +++++++------------------- components/Package/NotFound.tsx | 17 ++++-------- context/CustomAppearanceProvider.tsx | 13 +++++++-- next.config.ts | 2 +- package.json | 4 +++ postcss.config.mjs | 7 +++++ scenes/ErrorScene.tsx | 40 ++++++++-------------------- styles/styles.css | 2 ++ 9 files changed, 56 insertions(+), 82 deletions(-) create mode 100644 postcss.config.mjs diff --git a/components/Libraries.tsx b/components/Libraries.tsx index 3c26675e4..7f6275f72 100644 --- a/components/Libraries.tsx +++ b/components/Libraries.tsx @@ -1,8 +1,9 @@ import dynamic from 'next/dynamic'; -import { StyleSheet, View } from 'react-native'; +import { View } from 'react-native'; +import tw from 'twrnc'; import LoadingContent from '~/components/Library/LoadingContent'; -import NotFoundContent from '~/components/NotFoundContent'; +import NotFound from '~/components/Package/NotFound'; import { type LibraryType } from '~/types'; type Props = { @@ -15,20 +16,14 @@ const LibraryWithLoading = dynamic(() => import('~/components/Library'), { export default function Libraries({ libraries }: Props) { if (!libraries || !libraries.length) { - return ; + return ; } return ( - + {libraries.map((item, index) => ( ))} ); } - -const styles = StyleSheet.create({ - librariesContainer: { - paddingTop: 12, - }, -}); diff --git a/components/NotFoundContent.tsx b/components/NotFoundContent.tsx index 5c657ecdb..b343cca79 100644 --- a/components/NotFoundContent.tsx +++ b/components/NotFoundContent.tsx @@ -1,5 +1,6 @@ import { type ReactElement } from 'react'; -import { Image, StyleSheet, View } from 'react-native'; +import { Image, View } from 'react-native'; +import tw from 'twrnc'; import { A, H3, P } from '~/common/styleguide'; @@ -15,11 +16,14 @@ export default function NotFoundContent({ bottomSlot, }: Props) { return ( - - {alt} -

{header}

- -

+ + {alt} +

{header}

+

Want to contribute a library you like? Submit a PR to the{' '} GitHub Repo.

@@ -27,25 +31,3 @@ export default function NotFoundContent({
); } - -const styles = StyleSheet.create({ - container: { - alignItems: 'center', - width: '100%', - paddingHorizontal: 24, - marginTop: 40, - marginBottom: 80, - }, - img: { - marginTop: 48, - marginBottom: 24, - width: 64, - height: 64, - }, - text: { - textAlign: 'center', - }, - separator: { - marginTop: 20, - }, -}); diff --git a/components/Package/NotFound.tsx b/components/Package/NotFound.tsx index f214d7298..2f8e24623 100644 --- a/components/Package/NotFound.tsx +++ b/components/Package/NotFound.tsx @@ -1,20 +1,22 @@ -import { StyleSheet } from 'react-native'; +import tw from 'twrnc'; import { Button } from '~/components/Button'; import ContentContainer from '~/components/ContentContainer'; import Navigation from '~/components/Navigation'; import NotFoundContent from '~/components/NotFoundContent'; +import PageMeta from '~/components/PageMeta'; export default function NotFound() { return ( <> - + + } /> + } @@ -23,12 +25,3 @@ export default function NotFound() { ); } - -const styles = StyleSheet.create({ - homeButton: { - color: 'inherit', - marginVertical: 20, - paddingHorizontal: 12, - paddingVertical: 6, - }, -}); diff --git a/context/CustomAppearanceProvider.tsx b/context/CustomAppearanceProvider.tsx index aec9c7499..95a35e179 100644 --- a/context/CustomAppearanceProvider.tsx +++ b/context/CustomAppearanceProvider.tsx @@ -1,6 +1,7 @@ import AsyncStorage from '@react-native-async-storage/async-storage'; import { type PropsWithChildren, useEffect, useState } from 'react'; -import { Appearance, View } from 'react-native'; +import { View } from 'react-native'; +import tw, { useDeviceContext, useAppColorScheme } from 'twrnc'; import CustomAppearanceContext, { type CustomAppearanceContextType, @@ -12,15 +13,21 @@ const shouldRehydrate = true; const defaultState = { isDark: false }; export default function CustomAppearanceProvider({ children }: PropsWithChildren) { - const colorScheme = Appearance.getColorScheme(); + const [colorScheme, , setColorScheme] = useAppColorScheme(tw); const [isDark, setIsDark] = useState(colorScheme === 'dark'); const [isLoaded, setLoaded] = useState(false); + useDeviceContext(tw, { + observeDeviceColorSchemeChanges: false, + initialColorScheme: colorScheme === 'dark' ? 'dark' : 'light', + }); + useEffect(() => { async function rehydrateAsync() { try { const { isDark } = await rehydrateAppearanceState(); setIsDark(isDark); + setColorScheme(isDark ? 'dark' : 'light'); } catch {} setLoaded(true); } @@ -33,10 +40,12 @@ export default function CustomAppearanceProvider({ children }: PropsWithChildren } else { return ( { setIsDark(isDark); + setColorScheme(isDark ? 'dark' : 'light'); void cacheAppearanceState({ isDark }); }, }}> diff --git a/next.config.ts b/next.config.ts index 665460df1..1d2f7da2c 100644 --- a/next.config.ts +++ b/next.config.ts @@ -16,6 +16,7 @@ const PACKAGES_TO_OPTIMIZE = [ 'react-native-web', 'react-shiki', 'shiki/*', + 'twrnc', ]; const withBundleAnalyzer = BundleAnalyzer({ @@ -36,7 +37,6 @@ export default withPlugins([withExpo, withImages, withFonts, withBundleAnalyzer] forceSwcTransforms: true, webpackBuildWorker: true, browserDebugInfoInTerminal: true, - useLightningcss: true, optimizePackageImports: PACKAGES_TO_OPTIMIZE, }, async headers() { diff --git a/package.json b/package.json index 7ee5c0d5a..fea5ae2b2 100644 --- a/package.json +++ b/package.json @@ -26,12 +26,14 @@ "@react-native-async-storage/async-storage": "^2.2.0", "@react-native-picker/picker": "^2.11.4", "@sentry/react": "^10.32.1", + "@tailwindcss/postcss": "^4.1.18", "crypto-js": "^4.2.0", "es-toolkit": "^1.43.0", "expo": "54.0.30", "expo-font": "^14.0.10", "next": "^16.1.1", "node-emoji": "^2.2.0", + "postcss": "^8.5.6", "react": "19.2.3", "react-content-loader": "^7.1.1", "react-dom": "19.2.3", @@ -45,6 +47,8 @@ "rehype-sanitize": "^6.0.0", "remark-emoji": "^5.0.2", "remark-gfm": "^4.0.1", + "tailwindcss": "^4.1.18", + "twrnc": "^4.11.1", "use-debounce": "^10.0.6" }, "devDependencies": { diff --git a/postcss.config.mjs b/postcss.config.mjs new file mode 100644 index 000000000..297374d80 --- /dev/null +++ b/postcss.config.mjs @@ -0,0 +1,7 @@ +const config = { + plugins: { + '@tailwindcss/postcss': {}, + }, +}; + +export default config; diff --git a/scenes/ErrorScene.tsx b/scenes/ErrorScene.tsx index f9edcba4c..815e98e1e 100644 --- a/scenes/ErrorScene.tsx +++ b/scenes/ErrorScene.tsx @@ -1,6 +1,7 @@ -import { Image, StyleSheet, View } from 'react-native'; +import { Image, View } from 'react-native'; +import tw from 'twrnc'; -import { H2, A, P } from '~/common/styleguide'; +import { H3, A, P } from '~/common/styleguide'; import Navigation from '~/components/Navigation'; import NotFound from '~/components/Package/NotFound'; import PageMeta from '~/components/PageMeta'; @@ -19,10 +20,14 @@ export default function ErrorScene({ statusCode, reason }: Props) { <> } /> - - No results -

Uh oh, something went wrong ({statusCode})

-

+ + Error +

Uh oh, something went wrong ({statusCode})

+

{reason ?? ( <> Help fix it? Submit a PR to the{' '} @@ -34,26 +39,3 @@ export default function ErrorScene({ statusCode, reason }: Props) { ); } - -const styles = StyleSheet.create({ - container: { - alignItems: 'center', - justifyContent: 'center', - width: '100%', - paddingHorizontal: 24, - paddingTop: 48, - paddingBottom: 72, - flex: 1, - }, - img: { - marginBottom: 24, - width: 64, - height: 64, - }, - text: { - textAlign: 'center', - }, - secondLine: { - marginTop: 20, - }, -}); diff --git a/styles/styles.css b/styles/styles.css index 4ac908366..1f0576e0d 100644 --- a/styles/styles.css +++ b/styles/styles.css @@ -1,3 +1,5 @@ +@import "tailwindcss"; + @font-face { font-family: "Optimistic Display"; src: url("/fonts/Optimistic-Display-Light.woff2") From ec79b521e78fe97309000596cfdc2878e71a3357 Mon Sep 17 00:00:00 2001 From: Bartosz Kaszubowski Date: Fri, 12 Dec 2025 11:05:55 +0100 Subject: [PATCH 02/30] variables and dark mode init support --- components/TopBar.tsx | 4 +- context/CustomAppearanceProvider.tsx | 20 +++++-- pages/_app.tsx | 66 +++++------------------ scenes/TrendingScene.tsx | 7 +-- styles/styles.css | 78 +++++++++++++++++++++++++++- 5 files changed, 112 insertions(+), 63 deletions(-) diff --git a/components/TopBar.tsx b/components/TopBar.tsx index 2456c323c..070f93dbf 100644 --- a/components/TopBar.tsx +++ b/components/TopBar.tsx @@ -2,6 +2,7 @@ import { Header as HtmlHeader } from '@expo/html-elements'; import Link from 'next/link'; import { useContext } from 'react'; import { StyleSheet, View } from 'react-native'; +import tw from 'twrnc'; import { layout, colors, H5, P, darkColors, useLayout } from '~/common/styleguide'; import ContentContainer from '~/components/ContentContainer'; @@ -19,8 +20,9 @@ export default function TopBar() { return ( diff --git a/context/CustomAppearanceProvider.tsx b/context/CustomAppearanceProvider.tsx index 95a35e179..2d0934c14 100644 --- a/context/CustomAppearanceProvider.tsx +++ b/context/CustomAppearanceProvider.tsx @@ -17,6 +17,19 @@ export default function CustomAppearanceProvider({ children }: PropsWithChildren const [isDark, setIsDark] = useState(colorScheme === 'dark'); const [isLoaded, setLoaded] = useState(false); + function toggleTheme(isDark: boolean) { + const el = document.documentElement; + + if (isDark) { + el.classList.add('dark'); + } else { + el.classList.remove('dark'); + } + + setColorScheme(isDark ? 'dark' : 'light'); + setIsDark(isDark); + } + useDeviceContext(tw, { observeDeviceColorSchemeChanges: false, initialColorScheme: colorScheme === 'dark' ? 'dark' : 'light', @@ -26,8 +39,7 @@ export default function CustomAppearanceProvider({ children }: PropsWithChildren async function rehydrateAsync() { try { const { isDark } = await rehydrateAppearanceState(); - setIsDark(isDark); - setColorScheme(isDark ? 'dark' : 'light'); + toggleTheme(isDark); } catch {} setLoaded(true); } @@ -40,12 +52,10 @@ export default function CustomAppearanceProvider({ children }: PropsWithChildren } else { return ( { - setIsDark(isDark); - setColorScheme(isDark ? 'dark' : 'light'); + toggleTheme(isDark); void cacheAppearanceState({ isDark }); }, }}> diff --git a/pages/_app.tsx b/pages/_app.tsx index 75cd315e7..32e000418 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -3,9 +3,7 @@ import { type AppProps } from 'next/app'; import Head from 'next/head'; import { SafeAreaProvider } from 'react-native-safe-area-context'; -import { colors, darkColors } from '~/common/styleguide'; import Footer from '~/components/Footer'; -import CustomAppearanceContext from '~/context/CustomAppearanceContext'; import CustomAppearanceProvider from '~/context/CustomAppearanceProvider'; import '~/styles/styles.css'; @@ -24,57 +22,19 @@ Sentry.init({ function App({ pageProps, Component }: AppProps) { return ( - - {context => ( - - - - - - -