├── .github └── workflows │ └── deploy-preview.yml ├── .gitignore ├── .yarn └── releases │ └── yarn-4.7.0.cjs ├── .yarnrc.yml ├── README.md ├── lerna.json ├── package.json ├── packages ├── example-native │ ├── .gitignore │ ├── README.md │ ├── app.json │ ├── app │ │ ├── (tabs) │ │ │ ├── _layout.tsx │ │ │ ├── effects.tsx │ │ │ └── index.tsx │ │ ├── +not-found.tsx │ │ ├── _layout.tsx │ │ └── global.css │ ├── assets │ │ └── images │ │ │ ├── adaptive-icon.png │ │ │ ├── icon.png │ │ │ └── splash-icon.png │ ├── babel.config.js │ ├── components │ │ ├── HapticTab.tsx │ │ ├── ThemedText.tsx │ │ ├── ThemedView.tsx │ │ └── ui │ │ │ ├── Header.tsx │ │ │ ├── IconSymbol.ios.tsx │ │ │ ├── IconSymbol.tsx │ │ │ ├── SafeScrollView.tsx │ │ │ ├── TabBarBackground.ios.tsx │ │ │ └── TabBarBackground.tsx │ ├── constants │ │ └── Colors.ts │ ├── hooks │ │ ├── useColorScheme.ts │ │ ├── useColorScheme.web.ts │ │ └── useThemeColor.ts │ ├── metro.config.js │ ├── nativewind-env.d.ts │ ├── package.json │ ├── tailwind.config.js │ └── tsconfig.json ├── example-web │ ├── .gitignore │ ├── README.md │ ├── common │ │ ├── SearchDialogContext.tsx │ │ ├── icon-imports.ts │ │ └── utils.ts │ ├── components │ │ ├── ButtonsRow.tsx │ │ ├── DemoTile.tsx │ │ ├── Sidebar.tsx │ │ ├── SidebarLink.tsx │ │ ├── headers.tsx │ │ └── search │ │ │ ├── QuickActionItem.tsx │ │ │ ├── SearchMenu.tsx │ │ │ └── StyleguideItem.tsx │ ├── data │ │ ├── quickActionEntries.ts │ │ └── styleguideEntries.ts │ ├── hooks │ │ └── useCopy.tsx │ ├── merge-icon-imports.js │ ├── next.config.js │ ├── package.json │ ├── pages │ │ ├── _app.tsx │ │ ├── _document.tsx │ │ ├── colors.tsx │ │ ├── icons.tsx │ │ ├── index.tsx │ │ ├── layouts.tsx │ │ ├── typography.tsx │ │ └── ui │ │ │ ├── components.tsx │ │ │ └── search.tsx │ ├── postcss.config.js │ ├── public │ │ ├── favicon.png │ │ ├── global.css │ │ └── icon.png │ ├── tailwind.config.js │ └── tsconfig.json ├── search-ui │ ├── LICENSE │ ├── README.md │ ├── index.ts │ ├── package.json │ ├── rollup.config.mjs │ ├── src │ │ ├── Items │ │ │ ├── ExpoBlogItem.tsx │ │ │ ├── ExpoDocsItem.tsx │ │ │ ├── FootnoteSection.tsx │ │ │ ├── RNDirectoryItem.tsx │ │ │ ├── RNDocsItem.tsx │ │ │ ├── icons.tsx │ │ │ └── index.ts │ │ ├── components │ │ │ ├── BarLoader.tsx │ │ │ ├── CommandFooter.tsx │ │ │ ├── CommandItemBaseWithCopy.tsx │ │ │ ├── CommandMenu.tsx │ │ │ └── CommandMenuTrigger.tsx │ │ ├── styles │ │ │ └── expo-search-ui.css │ │ ├── types.ts │ │ └── utils.ts │ └── tsconfig.json ├── styleguide-base │ ├── LICENSE │ ├── README.md │ ├── index.ts │ ├── package.json │ ├── rollup.config.mjs │ ├── src │ │ ├── breakpoints.ts │ │ ├── palette.ts │ │ ├── sizing.ts │ │ ├── spacing.ts │ │ └── themes.ts │ └── tsconfig.json ├── styleguide-icons │ ├── .env.example │ ├── .gitignore │ ├── LICENSE │ ├── README.md │ ├── check-env.sh │ ├── figma.config.js │ ├── package.json │ ├── postbundle.js │ ├── rollup.config.mjs │ ├── src │ │ ├── index-stub.js │ │ ├── index.ts │ │ └── mergeClasses.ts │ ├── stub.d.ts │ ├── svgr-icon-template.js │ └── tsconfig.json ├── styleguide-native │ ├── LICENSE │ ├── README.md │ ├── index.ts │ ├── package.json │ ├── rollup.config.mjs │ ├── src │ │ ├── logos │ │ │ ├── DocsLogo.tsx │ │ │ ├── ExpoGoLogo.tsx │ │ │ ├── Logo.tsx │ │ │ ├── SnackLogo.tsx │ │ │ ├── WordMarkLogo.tsx │ │ │ └── index.tsx │ │ ├── styles │ │ │ ├── shadows.ts │ │ │ └── typography.ts │ │ └── types │ │ │ └── index.ts │ └── tsconfig.json └── styleguide │ ├── LICENSE │ ├── README.md │ ├── index.ts │ ├── package.json │ ├── rollup.config.mjs │ ├── src │ ├── components │ │ ├── Button │ │ │ ├── Button.tsx │ │ │ ├── ButtonBase.tsx │ │ │ ├── helpers.ts │ │ │ └── index.ts │ │ ├── Link │ │ │ ├── Link.tsx │ │ │ ├── LinkBase.tsx │ │ │ └── index.ts │ │ ├── Theme │ │ │ ├── BlockingSetInitialColorMode.tsx │ │ │ ├── ThemeProvider.tsx │ │ │ └── index.ts │ │ └── index.ts │ ├── helpers │ │ └── mergeClasses.ts │ ├── logos │ │ ├── DocsLogo.tsx │ │ ├── ExpoGoLogo.tsx │ │ ├── Logo.tsx │ │ ├── OrbitLogo.tsx │ │ ├── RouterLogo.tsx │ │ ├── SnackLogo.tsx │ │ ├── WordMarkLogo.tsx │ │ └── index.tsx │ └── styles │ │ ├── colors.ts │ │ ├── expo-theme.css │ │ ├── shadows.ts │ │ ├── themes.ts │ │ └── typography.ts │ ├── tailwind.js │ └── tsconfig.json ├── tsconfig.json └── yarn.lock /.github/workflows/deploy-preview.yml: -------------------------------------------------------------------------------- 1 | name: Check packages and deploy website 2 | 3 | env: 4 | NODE_ENV: production 5 | VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} 6 | VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} 7 | FILE_ID: zHeGLN45wIgf39kqdvKpoj 8 | 9 | on: 10 | push: 11 | branches: [main] 12 | paths: 13 | - .github/workflows/deploy.yml 14 | - packages/** 15 | pull_request: 16 | paths: 17 | - .github/workflows/deploy.yml 18 | - packages/** 19 | 20 | jobs: 21 | deploy: 22 | runs-on: ubuntu-22.04 23 | steps: 24 | - name: 👀 Checkout 25 | uses: actions/checkout@v4 26 | 27 | - name: ⬢ Setup Node 28 | uses: actions/setup-node@v4 29 | with: 30 | node-version: 22 31 | 32 | - name: 🧶 Yarn install 33 | run: yarn install --immutable 34 | 35 | - name: 🚨 Lint code 36 | run: yarn lint --max-warnings 0 37 | 38 | - name: 📦️ Build packages 39 | env: 40 | FIGMA_TOKEN: ${{ secrets.FIGMA_TOKEN }} 41 | run: yarn build 42 | 43 | - name: ⬇️️ Pull example-web metadata for preview 44 | if: github.ref != 'refs/heads/main' 45 | working-directory: packages/example-web 46 | run: yarn dlx vercel pull --token ${{ secrets.VERCEL_TOKEN }} 47 | - name: 📦️ Build example-app for preview 48 | if: github.ref != 'refs/heads/main' 49 | working-directory: packages/example-web 50 | run: yarn dlx vercel build --token ${{ secrets.VERCEL_TOKEN }} 51 | 52 | - name: ⬇️️ Pull example-web metadata 53 | if: github.ref == 'refs/heads/main' 54 | working-directory: packages/example-web 55 | run: yarn dlx vercel pull --token ${{ secrets.VERCEL_TOKEN }} --environment=production 56 | - name: 📦️ Build example-app 57 | if: github.ref == 'refs/heads/main' 58 | working-directory: packages/example-web 59 | run: yarn dlx vercel build --token ${{ secrets.VERCEL_TOKEN }} --prod 60 | 61 | - name: 🚀 Deploy website 62 | uses: BetaHuhn/deploy-to-vercel-action@v1 63 | with: 64 | GITHUB_TOKEN: ${{ secrets.EXPO_BOT_GH_TOKEN }} 65 | VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} 66 | PREBUILT: true 67 | WORKING_DIRECTORY: packages/example-web 68 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .eslintcache 3 | .idea 4 | *.log 5 | *.tsbuildinfo 6 | node_modules 7 | 8 | # build 9 | dist 10 | tmp 11 | out 12 | build 13 | 14 | # env files 15 | .env 16 | 17 | # Next 18 | .next 19 | next-env.d.ts 20 | 21 | # yarn 22 | .yarn/* 23 | !.yarn/patches 24 | !.yarn/plugins 25 | !.yarn/releases 26 | !.yarn/sdks 27 | !.yarn/versions 28 | yarn-debug.log* 29 | yarn-error.log* 30 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | compressionLevel: mixed 2 | 3 | nodeLinker: node-modules 4 | 5 | yarnPath: .yarn/releases/yarn-4.7.0.cjs 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 |

5 | @expo/styleguide-* 6 |

7 | 8 | A collection of packages used to share styles and icons across Expo websites and projects. 9 | 10 | ## Get started 11 | 12 | 1. Install dependencies with 13 | 14 | ```bash 15 | yarn 16 | ``` 17 | 18 | 2. Configure styleguide-icons. Read [**packages/styleguide-icons/README.md**](https://github.com/expo/styleguide/blob/main/packages/styleguide-icons/README.md) to set up your credentials (Expo staff only) to generate icons. 19 | 20 | 3. Build the packages with 21 | 22 | ```bash 23 | yarn build 24 | ``` 25 | 26 | It can take several minutes to build the icons package. 27 | 28 | 4. Develop with 29 | 30 | ```bash 31 | yarn dev 32 | ``` 33 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "node_modules/lerna/schemas/lerna-schema.json", 3 | "version": "independent", 4 | "packages": ["packages/*"], 5 | "ignore": "example-*", 6 | "npmClient": "yarn" 7 | } 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "styleguide-root", 3 | "private": true, 4 | "scripts": { 5 | "build": "lerna run build --stream", 6 | "dev": "lerna run dev --parallel", 7 | "npm-publish": "lerna publish --no-private", 8 | "lint": "eslint packages/example-native packages/search-ui packages/styleguide packages/styleguide-base packages/styleguide-icons packages/styleguide-native" 9 | }, 10 | "workspaces": [ 11 | "packages/*" 12 | ], 13 | "resolutions": { 14 | "eslint-plugin-react-hooks": "^4.6.2" 15 | }, 16 | "dependencies": { 17 | "@rollup/plugin-terser": "^0.4.4", 18 | "@rollup/plugin-typescript": "^12.1.2", 19 | "@types/react": "^18.3.12", 20 | "eslint": "^8.57.0", 21 | "eslint-config-universe": "^14.0.0", 22 | "lerna": "^8.2.1", 23 | "npm-run-all": "^4.1.5", 24 | "prettier": "^3.5.3", 25 | "react": "^18.3.1", 26 | "rimraf": "^6.0.1", 27 | "rollup": "^4.36.0", 28 | "rollup-plugin-copy": "^3.5.0", 29 | "typescript": "^5.6.3" 30 | }, 31 | "packageManager": "yarn@4.7.0", 32 | "eslintConfig": { 33 | "root": true, 34 | "rules": { 35 | "max-len": [ 36 | "warn", 37 | { 38 | "code": 120 39 | } 40 | ] 41 | } 42 | }, 43 | "eslintIgnore": [ 44 | "node_modules", 45 | "dist", 46 | "out", 47 | "tmp", 48 | ".expo", 49 | ".next", 50 | "packages/example-native/expo-env.d.ts", 51 | "packages/styleguide-icons/index.*", 52 | "packages/styleguide-icons/mergeClasses.*" 53 | ], 54 | "prettier": { 55 | "printWidth": 120, 56 | "singleQuote": true, 57 | "bracketSameLine": true, 58 | "trailingComma": "es5" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /packages/example-native/.gitignore: -------------------------------------------------------------------------------- 1 | # Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files 2 | 3 | # dependencies 4 | node_modules/ 5 | 6 | # Expo 7 | .expo/ 8 | dist/ 9 | web-build/ 10 | expo-env.d.ts 11 | 12 | # Native 13 | *.orig.* 14 | *.jks 15 | *.p8 16 | *.p12 17 | *.key 18 | *.mobileprovision 19 | 20 | # Metro 21 | .metro-health-check* 22 | 23 | # debug 24 | npm-debug.* 25 | yarn-debug.* 26 | yarn-error.* 27 | 28 | # macOS 29 | .DS_Store 30 | *.pem 31 | 32 | # local env files 33 | .env*.local 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | 38 | app-example 39 | -------------------------------------------------------------------------------- /packages/example-native/README.md: -------------------------------------------------------------------------------- 1 | # example-native 2 | 3 | An example Expo app created to test native Expo Styleguide packages. 4 | 5 | ## Running locally 6 | 7 | ```shell 8 | yarn start 9 | ``` 10 | 11 | Then press `a` to run Android app or `i` to run iOS app. 12 | -------------------------------------------------------------------------------- /packages/example-native/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "example-native", 4 | "slug": "example-native", 5 | "version": "1.0.0", 6 | "orientation": "portrait", 7 | "icon": "./assets/images/icon.png", 8 | "scheme": "myapp", 9 | "userInterfaceStyle": "automatic", 10 | "newArchEnabled": true, 11 | "ios": { 12 | "supportsTablet": true, 13 | "bundleIdentifier": "com.simek.examplenative" 14 | }, 15 | "android": { 16 | "adaptiveIcon": { 17 | "foregroundImage": "./assets/images/adaptive-icon.png", 18 | "backgroundColor": "#ffffff" 19 | }, 20 | "package": "com.simek.examplenative" 21 | }, 22 | "web": { 23 | "bundler": "metro", 24 | "output": "static", 25 | "favicon": "./assets/images/favicon.png" 26 | }, 27 | "plugins": [ 28 | "expo-router", 29 | [ 30 | "expo-splash-screen", 31 | { 32 | "image": "./assets/images/splash-icon.png", 33 | "imageWidth": 200, 34 | "resizeMode": "contain", 35 | "backgroundColor": "#ffffff" 36 | } 37 | ] 38 | ], 39 | "experiments": { 40 | "typedRoutes": true 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/example-native/app/(tabs)/_layout.tsx: -------------------------------------------------------------------------------- 1 | import { BottomTabHeaderProps } from '@react-navigation/bottom-tabs'; 2 | import { Tabs } from 'expo-router'; 3 | import { Platform } from 'react-native'; 4 | 5 | import { HapticTab } from '@/components/HapticTab'; 6 | import { Header } from '@/components/ui/Header'; 7 | import { IconSymbol } from '@/components/ui/IconSymbol'; 8 | import TabBarBackground from '@/components/ui/TabBarBackground'; 9 | import { Colors } from '@/constants/Colors'; 10 | import { useColorScheme } from '@/hooks/useColorScheme'; 11 | import { useThemeColor } from '@/hooks/useThemeColor'; 12 | 13 | export default function TabLayout() { 14 | const colorScheme = useColorScheme(); 15 | const borderColor = useThemeColor({}, 'border'); 16 | 17 | return ( 18 |
, 21 | headerTransparent: Platform.OS === 'ios', 22 | tabBarActiveTintColor: Colors[colorScheme ?? 'light'].tint, 23 | tabBarButton: HapticTab, 24 | tabBarBackground: TabBarBackground, 25 | tabBarStyle: Platform.select({ 26 | ios: { 27 | position: 'absolute', 28 | }, 29 | default: {}, 30 | }), 31 | }}> 32 | , 37 | }} 38 | /> 39 | , 44 | }} 45 | /> 46 | 47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /packages/example-native/app/(tabs)/effects.tsx: -------------------------------------------------------------------------------- 1 | import { shadows } from '@expo/styleguide-native'; 2 | import { Platform, View } from 'react-native'; 3 | import { SafeAreaView } from 'react-native-safe-area-context'; 4 | 5 | import { ThemedText } from '@/components/ThemedText'; 6 | import { ThemedView } from '@/components/ThemedView'; 7 | import { SafeScrollView } from '@/components/ui/SafeScrollView'; 8 | 9 | export default function Effects() { 10 | const Container = Platform.OS === 'ios' ? SafeAreaView : ThemedView; 11 | return ( 12 | 13 | 14 | Shadows 15 | 16 | 17 | shadows.xs 18 | 19 | 20 | shadows.sm 21 | 22 | 23 | shadows.md 24 | 25 | 26 | shadows.lg 27 | 28 | 29 | shadows.xl 30 | 31 | 32 | 33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /packages/example-native/app/(tabs)/index.tsx: -------------------------------------------------------------------------------- 1 | import { typography } from '@expo/styleguide-native'; 2 | import { Platform } from 'react-native'; 3 | import { SafeAreaView } from 'react-native-safe-area-context'; 4 | 5 | import { ThemedText } from '@/components/ThemedText'; 6 | import { ThemedView } from '@/components/ThemedView'; 7 | import { SafeScrollView } from '@/components/ui/SafeScrollView'; 8 | 9 | const largeHeaders = typography.headers.default.large; 10 | const headers = typography.headers.default.medium; 11 | const smallHeaders = typography.headers.default.small; 12 | 13 | export default function Typography() { 14 | const Container = Platform.OS === 'ios' ? SafeAreaView : ThemedView; 15 | return ( 16 | 17 | 18 | Headers 19 | 20 | default.large.huge 21 | default.large.h1 22 | default.large.h2 23 | default.large.h3 24 | default.large.h4 25 | default.large.h5 26 | default.large.h6 27 | 28 | 29 | default.medium.huge 30 | default.medium.h1 31 | default.medium.h2 32 | default.medium.h3 33 | default.medium.h4 34 | default.medium.h5 35 | default.medium.h6 36 | 37 | 38 | default.small.huge 39 | default.small.h1 40 | default.small.h2 41 | default.small.h3 42 | default.small.h4 43 | default.small.h5 44 | default.small.h6 45 | 46 | 47 | 48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /packages/example-native/app/+not-found.tsx: -------------------------------------------------------------------------------- 1 | import { Link, Stack } from 'expo-router'; 2 | import { StyleSheet } from 'react-native'; 3 | 4 | import { ThemedText } from '@/components/ThemedText'; 5 | import { ThemedView } from '@/components/ThemedView'; 6 | 7 | export default function NotFoundScreen() { 8 | return ( 9 | <> 10 | 11 | 12 | This screen doesn't exist. 13 | 14 | Go to home screen! 15 | 16 | 17 | 18 | ); 19 | } 20 | 21 | const styles = StyleSheet.create({ 22 | container: { 23 | flex: 1, 24 | alignItems: 'center', 25 | justifyContent: 'center', 26 | padding: 20, 27 | }, 28 | link: { 29 | marginTop: 15, 30 | paddingVertical: 15, 31 | }, 32 | }); 33 | -------------------------------------------------------------------------------- /packages/example-native/app/_layout.tsx: -------------------------------------------------------------------------------- 1 | import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native'; 2 | import { Stack } from 'expo-router'; 3 | import * as SplashScreen from 'expo-splash-screen'; 4 | import { StatusBar } from 'expo-status-bar'; 5 | import { useEffect } from 'react'; 6 | import 'react-native-reanimated'; 7 | 8 | import { useColorScheme } from '@/hooks/useColorScheme'; 9 | 10 | import './global.css'; 11 | 12 | SplashScreen.preventAutoHideAsync(); 13 | 14 | export default function RootLayout() { 15 | const colorScheme = useColorScheme(); 16 | 17 | useEffect(() => { 18 | SplashScreen.hideAsync(); 19 | }, []); 20 | 21 | return ( 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /packages/example-native/app/global.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | .box { 6 | @apply w-full h-24 rounded-lg items-center justify-center; 7 | } 8 | -------------------------------------------------------------------------------- /packages/example-native/assets/images/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/expo/styleguide/fd85f92dfd9faa2dbf2d0880ffc023dde7fd2312/packages/example-native/assets/images/adaptive-icon.png -------------------------------------------------------------------------------- /packages/example-native/assets/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/expo/styleguide/fd85f92dfd9faa2dbf2d0880ffc023dde7fd2312/packages/example-native/assets/images/icon.png -------------------------------------------------------------------------------- /packages/example-native/assets/images/splash-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/expo/styleguide/fd85f92dfd9faa2dbf2d0880ffc023dde7fd2312/packages/example-native/assets/images/splash-icon.png -------------------------------------------------------------------------------- /packages/example-native/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function (api) { 2 | api.cache(true); 3 | return { 4 | presets: [['babel-preset-expo', { jsxImportSource: 'nativewind' }], 'nativewind/babel'], 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /packages/example-native/components/HapticTab.tsx: -------------------------------------------------------------------------------- 1 | import { BottomTabBarButtonProps } from '@react-navigation/bottom-tabs'; 2 | import { PlatformPressable } from '@react-navigation/elements'; 3 | import * as Haptics from 'expo-haptics'; 4 | 5 | export function HapticTab(props: BottomTabBarButtonProps) { 6 | return ( 7 | { 10 | if (process.env.EXPO_OS === 'ios') { 11 | await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); 12 | } 13 | props.onPressIn?.(ev); 14 | }} 15 | /> 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /packages/example-native/components/ThemedText.tsx: -------------------------------------------------------------------------------- 1 | import { typography } from '@expo/styleguide-native'; 2 | import { Text, type TextProps } from 'react-native'; 3 | 4 | import { useThemeColor } from '@/hooks/useThemeColor'; 5 | 6 | export type ThemedTextProps = TextProps & { 7 | lightColor?: string; 8 | darkColor?: string; 9 | type?: 'default' | 'title'; 10 | }; 11 | 12 | export function ThemedText({ style, lightColor, darkColor, type = 'default', ...rest }: ThemedTextProps) { 13 | const color = useThemeColor({ light: lightColor, dark: darkColor }, type === 'title' ? 'icon' : 'text'); 14 | 15 | return ( 16 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /packages/example-native/components/ThemedView.tsx: -------------------------------------------------------------------------------- 1 | import { View, type ViewProps } from 'react-native'; 2 | 3 | import { useThemeColor } from '@/hooks/useThemeColor'; 4 | 5 | export type ThemedViewProps = ViewProps & { 6 | lightColor?: string; 7 | darkColor?: string; 8 | }; 9 | 10 | export function ThemedView({ style, lightColor, darkColor, ...otherProps }: ThemedViewProps) { 11 | const backgroundColor = useThemeColor({ light: lightColor, dark: darkColor }, 'background'); 12 | 13 | return ; 14 | } 15 | -------------------------------------------------------------------------------- /packages/example-native/components/ui/Header.tsx: -------------------------------------------------------------------------------- 1 | import { typography } from '@expo/styleguide-native'; 2 | import { type BottomTabHeaderProps } from '@react-navigation/bottom-tabs'; 3 | import { BlurView } from 'expo-blur'; 4 | import { Platform, StyleSheet } from 'react-native'; 5 | import { SafeAreaView } from 'react-native-safe-area-context'; 6 | 7 | import { ThemedText } from '@/components/ThemedText'; 8 | import { ThemedView } from '@/components/ThemedView'; 9 | 10 | export function Header({ borderColor, ...props }: BottomTabHeaderProps & { borderColor: string }) { 11 | return ( 12 | 17 | {Platform.OS === 'ios' ? ( 18 | 19 | ) : ( 20 | 21 | )} 22 | 23 | {props.options.title} 24 | 25 | 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /packages/example-native/components/ui/IconSymbol.ios.tsx: -------------------------------------------------------------------------------- 1 | import { SymbolView, SymbolViewProps, SymbolWeight } from 'expo-symbols'; 2 | import { StyleProp, ViewStyle } from 'react-native'; 3 | 4 | export function IconSymbol({ 5 | name, 6 | size = 24, 7 | color, 8 | style, 9 | weight = 'regular', 10 | }: { 11 | name: SymbolViewProps['name']; 12 | size?: number; 13 | color: string; 14 | style?: StyleProp; 15 | weight?: SymbolWeight; 16 | }) { 17 | return ( 18 | 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /packages/example-native/components/ui/IconSymbol.tsx: -------------------------------------------------------------------------------- 1 | // Fallback for using MaterialIcons on Android and web. 2 | 3 | import MaterialIcons from '@expo/vector-icons/MaterialIcons'; 4 | import { SymbolWeight, SymbolViewProps } from 'expo-symbols'; 5 | import { ComponentProps } from 'react'; 6 | import { OpaqueColorValue, StyleProp, TextStyle } from 'react-native'; 7 | 8 | type IconMapping = Record['name']>; 9 | type IconSymbolName = keyof typeof MAPPING; 10 | 11 | /** 12 | * Add your SF Symbols to Material Icons mappings here. 13 | * - see Material Icons in the [Icons Directory](https://icons.expo.fyi). 14 | * - see SF Symbols in the [SF Symbols](https://developer.apple.com/sf-symbols/) app. 15 | */ 16 | const MAPPING = { 17 | 'textformat.size': 'text-fields', 18 | 'wand.and.rays': 'gradient', 19 | 'chevron.left.forwardslash.chevron.right': 'code', 20 | 'chevron.right': 'chevron-right', 21 | } as IconMapping; 22 | 23 | /** 24 | * An icon component that uses native SF Symbols on iOS, and Material Icons on Android and web. 25 | * This ensures a consistent look across platforms, and optimal resource usage. 26 | * Icon `name`s are based on SF Symbols and require manual mapping to Material Icons. 27 | */ 28 | export function IconSymbol({ 29 | name, 30 | size = 24, 31 | color, 32 | style, 33 | }: { 34 | name: IconSymbolName; 35 | size?: number; 36 | color: string | OpaqueColorValue; 37 | style?: StyleProp; 38 | weight?: SymbolWeight; 39 | }) { 40 | return ; 41 | } 42 | -------------------------------------------------------------------------------- /packages/example-native/components/ui/SafeScrollView.tsx: -------------------------------------------------------------------------------- 1 | import { PropsWithChildren } from 'react'; 2 | import { Platform, ScrollView } from 'react-native'; 3 | 4 | import { ThemedView } from '@/components/ThemedView'; 5 | 6 | export function SafeScrollView({ children }: PropsWithChildren) { 7 | return ( 8 | 9 | 15 | {children} 16 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /packages/example-native/components/ui/TabBarBackground.ios.tsx: -------------------------------------------------------------------------------- 1 | import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs'; 2 | import { BlurView } from 'expo-blur'; 3 | import { StyleSheet } from 'react-native'; 4 | import { useSafeAreaInsets } from 'react-native-safe-area-context'; 5 | 6 | export default function BlurTabBarBackground() { 7 | return ; 8 | } 9 | 10 | export function useBottomTabOverflow() { 11 | const tabHeight = useBottomTabBarHeight(); 12 | const { bottom } = useSafeAreaInsets(); 13 | return tabHeight - bottom; 14 | } 15 | -------------------------------------------------------------------------------- /packages/example-native/components/ui/TabBarBackground.tsx: -------------------------------------------------------------------------------- 1 | // This is a shim for web and Android where the tab bar is generally opaque. 2 | export default undefined; 3 | 4 | export function useBottomTabOverflow() { 5 | return 0; 6 | } 7 | -------------------------------------------------------------------------------- /packages/example-native/constants/Colors.ts: -------------------------------------------------------------------------------- 1 | export const Colors = { 2 | light: { 3 | text: '#11181C', 4 | background: '#fff', 5 | border: '#ddd', 6 | tint: '#0a7ea4', 7 | icon: '#687076', 8 | tabIconDefault: '#687076', 9 | tabIconSelected: '#0a7ea4', 10 | headerIcon: '#A1CEDC', 11 | header: '#0a7ea4', 12 | }, 13 | dark: { 14 | text: '#ECEDEE', 15 | background: '#111111', 16 | border: '#222', 17 | tint: '#0a7ea4', 18 | icon: '#9BA1A6', 19 | tabIconDefault: '#9BA1A6', 20 | tabIconSelected: '#0a7ea4', 21 | headerIcon: '#1D3D47', 22 | header: '#0a7ea4', 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /packages/example-native/hooks/useColorScheme.ts: -------------------------------------------------------------------------------- 1 | export { useColorScheme } from 'react-native'; 2 | -------------------------------------------------------------------------------- /packages/example-native/hooks/useColorScheme.web.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { useColorScheme as useRNColorScheme } from 'react-native'; 3 | 4 | /** 5 | * To support static rendering, this value needs to be re-calculated on the client side for web 6 | */ 7 | export function useColorScheme() { 8 | const [hasHydrated, setHasHydrated] = useState(false); 9 | 10 | useEffect(() => { 11 | setHasHydrated(true); 12 | }, []); 13 | 14 | const colorScheme = useRNColorScheme(); 15 | 16 | if (hasHydrated) { 17 | return colorScheme; 18 | } 19 | 20 | return 'light'; 21 | } 22 | -------------------------------------------------------------------------------- /packages/example-native/hooks/useThemeColor.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Learn more about light and dark modes: 3 | * https://docs.expo.dev/guides/color-schemes/ 4 | */ 5 | 6 | import { Colors } from '@/constants/Colors'; 7 | import { useColorScheme } from '@/hooks/useColorScheme'; 8 | 9 | export function useThemeColor( 10 | props: { light?: string; dark?: string }, 11 | colorName: keyof typeof Colors.light & keyof typeof Colors.dark 12 | ) { 13 | const theme = useColorScheme() ?? 'light'; 14 | const colorFromProps = props[theme]; 15 | 16 | if (colorFromProps) { 17 | return colorFromProps; 18 | } else { 19 | return Colors[theme][colorName]; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/example-native/metro.config.js: -------------------------------------------------------------------------------- 1 | const { getDefaultConfig } = require('expo/metro-config'); 2 | const { withNativeWind } = require('nativewind/metro'); 3 | 4 | // eslint-disable-next-line no-undef 5 | const config = getDefaultConfig(__dirname); 6 | 7 | module.exports = withNativeWind(config, { input: './app/global.css' }); 8 | -------------------------------------------------------------------------------- /packages/example-native/nativewind-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /packages/example-native/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-native", 3 | "version": "0.0.0", 4 | "private": true, 5 | "main": "expo-router/entry", 6 | "scripts": { 7 | "start": "expo start", 8 | "android": "expo run:android", 9 | "ios": "expo run:ios" 10 | }, 11 | "dependencies": { 12 | "@expo/styleguide-native": "workspace:*", 13 | "@expo/vector-icons": "^14.0.2", 14 | "@react-navigation/bottom-tabs": "^7.2.0", 15 | "@react-navigation/native": "^7.0.14", 16 | "expo": "~52.0.40", 17 | "expo-blur": "~14.0.3", 18 | "expo-constants": "~17.0.8", 19 | "expo-haptics": "~14.0.1", 20 | "expo-linking": "~7.0.5", 21 | "expo-router": "~4.0.19", 22 | "expo-splash-screen": "~0.29.22", 23 | "expo-status-bar": "~2.0.1", 24 | "expo-symbols": "~0.2.2", 25 | "expo-system-ui": "~4.0.8", 26 | "expo-web-browser": "~14.0.2", 27 | "nativewind": "^4.1.23", 28 | "react": "18.3.1", 29 | "react-dom": "18.3.1", 30 | "react-native": "0.76.7", 31 | "react-native-gesture-handler": "~2.20.2", 32 | "react-native-reanimated": "~3.16.1", 33 | "react-native-safe-area-context": "4.12.0", 34 | "react-native-screens": "~4.4.0", 35 | "react-native-svg": "15.8.0", 36 | "react-native-web": "~0.19.13", 37 | "react-native-webview": "13.12.5" 38 | }, 39 | "devDependencies": { 40 | "@babel/core": "^7.26.10", 41 | "@tailwindcss/typography": "^0.5.15", 42 | "@types/react": "~18.3.12", 43 | "tailwindcss": "3.4.17", 44 | "typescript": "^5.6.3" 45 | }, 46 | "eslintConfig": { 47 | "extends": "universe/native" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/example-native/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ['./app/**/*.{js,jsx,ts,tsx}', './components/**/*.{js,jsx,ts,tsx}', './constants/**/*.{js,jsx,ts,tsx}'], 4 | presets: [require('nativewind/preset')], 5 | }; 6 | -------------------------------------------------------------------------------- /packages/example-native/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "expo/tsconfig.base", 3 | "compilerOptions": { 4 | "strict": true, 5 | "paths": { 6 | "@/*": [ 7 | "./*" 8 | ] 9 | } 10 | }, 11 | "include": [ 12 | "**/*.ts", 13 | "**/*.tsx", 14 | ".expo/types/**/*.ts", 15 | "expo-env.d.ts", 16 | "nativewind-env.d.ts" 17 | ] 18 | } -------------------------------------------------------------------------------- /packages/example-web/.gitignore: -------------------------------------------------------------------------------- 1 | .vercel 2 | -------------------------------------------------------------------------------- /packages/example-web/README.md: -------------------------------------------------------------------------------- 1 | # example-web 2 | 3 | An example Next app created to test web Expo Styleguide packages. 4 | 5 | ## Running locally 6 | 7 | ```shell 8 | yarn dev 9 | ``` 10 | -------------------------------------------------------------------------------- /packages/example-web/common/SearchDialogContext.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, Dispatch, PropsWithChildren, type SetStateAction, useContext, useState } from 'react'; 2 | 3 | type SearchDialogContextType = { 4 | isOpen: boolean; 5 | setOpen: Dispatch>; 6 | }; 7 | 8 | export const SearchDialogContext = createContext({ 9 | isOpen: false, 10 | setOpen: (_: SetStateAction) => {}, 11 | }); 12 | 13 | export function SearchDialogProvider({ children }: PropsWithChildren) { 14 | const [isOpen, setOpen] = useState(false); 15 | return ( 16 | 21 | {children} 22 | 23 | ); 24 | } 25 | 26 | export function useSearchDialogContext() { 27 | return useContext(SearchDialogContext); 28 | } 29 | -------------------------------------------------------------------------------- /packages/example-web/common/utils.ts: -------------------------------------------------------------------------------- 1 | export function getPaletteClasses(colorName: string) { 2 | switch (colorName) { 3 | case 'red': 4 | return [ 5 | 'bg-palette-red1', 6 | 'bg-palette-red2', 7 | 'bg-palette-red3', 8 | 'bg-palette-red4', 9 | 'bg-palette-red5', 10 | 'bg-palette-red6', 11 | 'bg-palette-red7', 12 | 'bg-palette-red8', 13 | 'bg-palette-red9', 14 | 'bg-palette-red10', 15 | 'bg-palette-red11', 16 | 'bg-palette-red12', 17 | ]; 18 | case 'orange': 19 | return [ 20 | 'bg-palette-orange1', 21 | 'bg-palette-orange2', 22 | 'bg-palette-orange3', 23 | 'bg-palette-orange4', 24 | 'bg-palette-orange5', 25 | 'bg-palette-orange6', 26 | 'bg-palette-orange7', 27 | 'bg-palette-orange8', 28 | 'bg-palette-orange9', 29 | 'bg-palette-orange10', 30 | 'bg-palette-orange11', 31 | 'bg-palette-orange12', 32 | ]; 33 | case 'yellow': 34 | return [ 35 | 'bg-palette-yellow1', 36 | 'bg-palette-yellow2', 37 | 'bg-palette-yellow3', 38 | 'bg-palette-yellow4', 39 | 'bg-palette-yellow5', 40 | 'bg-palette-yellow6', 41 | 'bg-palette-yellow7', 42 | 'bg-palette-yellow8', 43 | 'bg-palette-yellow9', 44 | 'bg-palette-yellow10', 45 | 'bg-palette-yellow11', 46 | 'bg-palette-yellow12', 47 | ]; 48 | case 'green': 49 | return [ 50 | 'bg-palette-green1', 51 | 'bg-palette-green2', 52 | 'bg-palette-green3', 53 | 'bg-palette-green4', 54 | 'bg-palette-green5', 55 | 'bg-palette-green6', 56 | 'bg-palette-green7', 57 | 'bg-palette-green8', 58 | 'bg-palette-green9', 59 | 'bg-palette-green10', 60 | 'bg-palette-green11', 61 | 'bg-palette-green12', 62 | ]; 63 | case 'blue': 64 | return [ 65 | 'bg-palette-blue1', 66 | 'bg-palette-blue2', 67 | 'bg-palette-blue3', 68 | 'bg-palette-blue4', 69 | 'bg-palette-blue5', 70 | 'bg-palette-blue6', 71 | 'bg-palette-blue7', 72 | 'bg-palette-blue8', 73 | 'bg-palette-blue9', 74 | 'bg-palette-blue10', 75 | 'bg-palette-blue11', 76 | 'bg-palette-blue12', 77 | ]; 78 | case 'purple': 79 | return [ 80 | 'bg-palette-purple1', 81 | 'bg-palette-purple2', 82 | 'bg-palette-purple3', 83 | 'bg-palette-purple4', 84 | 'bg-palette-purple5', 85 | 'bg-palette-purple6', 86 | 'bg-palette-purple7', 87 | 'bg-palette-purple8', 88 | 'bg-palette-purple9', 89 | 'bg-palette-purple10', 90 | 'bg-palette-purple11', 91 | 'bg-palette-purple12', 92 | ]; 93 | case 'pink': 94 | return [ 95 | 'bg-palette-pink1', 96 | 'bg-palette-pink2', 97 | 'bg-palette-pink3', 98 | 'bg-palette-pink4', 99 | 'bg-palette-pink5', 100 | 'bg-palette-pink6', 101 | 'bg-palette-pink7', 102 | 'bg-palette-pink8', 103 | 'bg-palette-pink9', 104 | 'bg-palette-pink10', 105 | 'bg-palette-pink11', 106 | 'bg-palette-pink12', 107 | ]; 108 | case 'gray': 109 | return [ 110 | 'bg-palette-gray1', 111 | 'bg-palette-gray2', 112 | 'bg-palette-gray3', 113 | 'bg-palette-gray4', 114 | 'bg-palette-gray5', 115 | 'bg-palette-gray6', 116 | 'bg-palette-gray7', 117 | 'bg-palette-gray8', 118 | 'bg-palette-gray9', 119 | 'bg-palette-gray10', 120 | 'bg-palette-gray11', 121 | 'bg-palette-gray12', 122 | ]; 123 | default: 124 | return []; 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /packages/example-web/components/ButtonsRow.tsx: -------------------------------------------------------------------------------- 1 | import { Button, ButtonProps } from '@expo/styleguide'; 2 | import { PaletteIcon } from '@expo/styleguide-icons/outline/PaletteIcon'; 3 | 4 | import { DemoTile } from '@/components/DemoTile'; 5 | 6 | type Props = ButtonProps & { 7 | iconOnly?: boolean; 8 | }; 9 | 10 | export function ButtonsRow({ theme, disabled = false, iconOnly = false }: Props) { 11 | return ( 12 | 13 |
14 | 17 | 20 | 23 | 26 | 29 | 32 | 35 |
36 |
37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /packages/example-web/components/DemoTile.tsx: -------------------------------------------------------------------------------- 1 | import { createElement } from 'react'; 2 | import type { PropsWithChildren } from 'react'; 3 | 4 | type DemoTileProps = PropsWithChildren<{ 5 | title: string; 6 | className?: string; 7 | tag?: string; 8 | }>; 9 | 10 | export function DemoTile({ title, className, children = 'Build developer trust.', tag = 'p' }: DemoTileProps) { 11 | return ( 12 |
13 |

{title}

14 | {createElement(tag, { className }, children)} 15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /packages/example-web/components/Sidebar.tsx: -------------------------------------------------------------------------------- 1 | import { Button, useTheme, Themes, mergeClasses } from '@expo/styleguide'; 2 | import { ThemeIcon } from '@expo/styleguide-icons/custom/ThemeIcon'; 3 | import { CommandMenuTrigger } from '@expo/styleguide-search-ui'; 4 | import Image from 'next/image'; 5 | import Link from 'next/link'; 6 | 7 | import { useSearchDialogContext } from '@/common/SearchDialogContext'; 8 | import { SidebarLink } from '@/components/SidebarLink'; 9 | 10 | export function Sidebar() { 11 | const { themeName, setDarkMode, setLightMode } = useTheme(); 12 | const { setOpen } = useSearchDialogContext(); 13 | 14 | function toggleTheme() { 15 | if (themeName === Themes.AUTO || themeName === Themes.LIGHT) { 16 | setDarkMode(); 17 | } else { 18 | setLightMode(); 19 | } 20 | } 21 | 22 | return ( 23 |
30 |
35 | 36 | Expo Styleguide Logo 43 | Expo Styleguide Logo 50 | 51 | 55 |
62 | 63 | 64 | 65 | 66 |
67 |

UI

68 | 69 | 70 |
71 | 78 |
79 | ); 80 | } 81 | -------------------------------------------------------------------------------- /packages/example-web/components/SidebarLink.tsx: -------------------------------------------------------------------------------- 1 | import { mergeClasses } from '@expo/styleguide'; 2 | import Link from 'next/link'; 3 | import { useRouter } from 'next/router'; 4 | 5 | type Props = { 6 | href: string; 7 | text: string; 8 | size?: 'sm' | 'md'; 9 | }; 10 | 11 | export function SidebarLink({ href, text, size = 'md' }: Props) { 12 | const { pathname } = useRouter(); 13 | return ( 14 | 23 | {text} 24 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /packages/example-web/components/headers.tsx: -------------------------------------------------------------------------------- 1 | import { mergeClasses } from '@expo/styleguide'; 2 | import { HTMLAttributes } from 'react'; 3 | 4 | export function H1({ children, className, ...rest }: HTMLAttributes) { 5 | return ( 6 |

14 | {children} 15 |

16 | ); 17 | } 18 | 19 | export function H3({ children, className, ...rest }: HTMLAttributes) { 20 | return ( 21 |

29 | {children} 30 |

31 | ); 32 | } 33 | 34 | export function H4({ children, className, ...rest }: HTMLAttributes) { 35 | return ( 36 |

44 | {children} 45 |

46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /packages/example-web/components/search/QuickActionItem.tsx: -------------------------------------------------------------------------------- 1 | import { Themes, useTheme } from '@expo/styleguide'; 2 | import { BuildIcon } from '@expo/styleguide-icons/custom/BuildIcon'; 3 | import { addHighlight, CommandItemBase } from '@expo/styleguide-search-ui'; 4 | import { type ComponentType, HTMLAttributes } from 'react'; 5 | 6 | type Props = { 7 | item: QuickActionItemType; 8 | query: string; 9 | onSelect?: () => void; 10 | }; 11 | 12 | export type QuickActionItemType = { 13 | label: string; 14 | Icon?: ComponentType>; 15 | }; 16 | 17 | export const QuickActionToggleThemeItem = ({ item, query }: Props) => { 18 | const { themeName, setDarkMode, setLightMode } = useTheme(); 19 | const Icon = item.Icon ?? BuildIcon; 20 | 21 | function toggleTheme() { 22 | if (themeName === Themes.AUTO || themeName === Themes.LIGHT) { 23 | setDarkMode(); 24 | } else { 25 | setLightMode(); 26 | } 27 | } 28 | 29 | return ( 30 | 31 |
32 | 33 |

34 |

35 |
36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /packages/example-web/components/search/SearchMenu.tsx: -------------------------------------------------------------------------------- 1 | import { CommandMenu } from '@expo/styleguide-search-ui'; 2 | import { ReactNode, useState } from 'react'; 3 | 4 | import { useSearchDialogContext } from '@/common/SearchDialogContext'; 5 | import { StyleguideItem } from '@/components/search/StyleguideItem'; 6 | import { entries as quickActionEntries } from '@/data/quickActionEntries'; 7 | import { entries as styleguideEntries } from '@/data/styleguideEntries'; 8 | 9 | export function SearchMenu() { 10 | const { isOpen, setOpen } = useSearchDialogContext(); 11 | const [styleguideItems, setStyleguideItems] = useState([]); 12 | const [quickActionItems, setQuickActionItems] = useState([]); 13 | 14 | const getStyleguideItems = async (query: string) => { 15 | const filteredEntries = styleguideEntries.filter((entry) => 16 | entry.label.toLowerCase().includes(query.toLowerCase()) 17 | ); 18 | setStyleguideItems( 19 | filteredEntries.map((item) => ( 20 | setOpen(false)} /> 21 | )) 22 | ); 23 | }; 24 | 25 | const getQuickActionItems = async (query: string) => { 26 | const filteredEntries = quickActionEntries.filter((entry) => 27 | entry.label.toLowerCase().includes(query.toLowerCase()) 28 | ); 29 | setQuickActionItems(filteredEntries.map((item) => )); 30 | }; 31 | 32 | return ( 33 | url, 37 | }} 38 | open={isOpen} 39 | setOpen={setOpen} 40 | customSections={[ 41 | { 42 | heading: 'Quick Actions', 43 | items: quickActionItems, 44 | getItemsAsync: getQuickActionItems, 45 | sectionIndex: 0, 46 | }, 47 | { 48 | heading: 'Expo Styleguide', 49 | items: styleguideItems, 50 | getItemsAsync: getStyleguideItems, 51 | sectionIndex: 1, 52 | }, 53 | ]} 54 | /> 55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /packages/example-web/components/search/StyleguideItem.tsx: -------------------------------------------------------------------------------- 1 | import { BuildIcon } from '@expo/styleguide-icons/custom/BuildIcon'; 2 | import { addHighlight } from '@expo/styleguide-search-ui'; 3 | import { Command } from 'cmdk'; 4 | import Link from 'next/link'; 5 | import { useRef, type ComponentType, HTMLAttributes } from 'react'; 6 | 7 | type Props = { 8 | item: StyleguideItemType; 9 | query: string; 10 | onSelect?: () => void; 11 | }; 12 | 13 | export type StyleguideItemType = { 14 | label: string; 15 | url: string; 16 | Icon?: ComponentType>; 17 | }; 18 | 19 | export const StyleguideItem = ({ item, onSelect, query }: Props) => { 20 | const linkRef = useRef(null); 21 | const Icon = item.Icon ?? BuildIcon; 22 | 23 | return ( 24 | 25 | { 28 | linkRef?.current?.click(); 29 | onSelect?.(); 30 | }}> 31 |
32 | 33 |

34 |

35 |
36 | 37 | ); 38 | }; 39 | -------------------------------------------------------------------------------- /packages/example-web/data/quickActionEntries.ts: -------------------------------------------------------------------------------- 1 | import { ThemeIcon } from '@expo/styleguide-icons/custom/ThemeIcon'; 2 | 3 | import { QuickActionToggleThemeItem } from '@/components/search/QuickActionItem'; 4 | 5 | export const entries = [ 6 | { 7 | label: 'Toggle Theme', 8 | Icon: ThemeIcon, 9 | Element: QuickActionToggleThemeItem, 10 | }, 11 | ]; 12 | -------------------------------------------------------------------------------- /packages/example-web/data/styleguideEntries.ts: -------------------------------------------------------------------------------- 1 | import { ExpoGoLogo } from '@expo/styleguide'; 2 | import { BezierCurve03Icon } from '@expo/styleguide-icons/outline/BezierCurve03Icon'; 3 | import { LayoutAlt03Icon } from '@expo/styleguide-icons/outline/LayoutAlt03Icon'; 4 | import { PaletteIcon } from '@expo/styleguide-icons/outline/PaletteIcon'; 5 | import { TextInputIcon } from '@expo/styleguide-icons/outline/TextInputIcon'; 6 | import { Type01Icon } from '@expo/styleguide-icons/outline/Type01Icon'; 7 | 8 | export const entries = [ 9 | { 10 | label: 'Colors: Semantic', 11 | url: '/colors#semantic', 12 | Icon: PaletteIcon, 13 | }, 14 | { 15 | label: 'Colors: Palette', 16 | url: '/colors#palette', 17 | Icon: PaletteIcon, 18 | }, 19 | { 20 | label: 'Colors: Project Background', 21 | url: '/colors#project', 22 | Icon: PaletteIcon, 23 | }, 24 | { 25 | label: 'Typography: Headings', 26 | url: '/typography#headings', 27 | Icon: Type01Icon, 28 | }, 29 | { 30 | label: 'Typography: Elements', 31 | url: '/typography#elements', 32 | Icon: Type01Icon, 33 | }, 34 | { 35 | label: 'Icons: Logos', 36 | url: '/icons#logos', 37 | Icon: ExpoGoLogo, 38 | }, 39 | { 40 | label: 'Icons: Icon Set', 41 | url: '/icons#set', 42 | Icon: BezierCurve03Icon, 43 | }, 44 | { 45 | label: 'UI: Components', 46 | url: '/ui/components', 47 | Icon: LayoutAlt03Icon, 48 | }, 49 | { 50 | label: 'UI: Search', 51 | url: '/ui/search', 52 | Icon: TextInputIcon, 53 | }, 54 | ]; 55 | -------------------------------------------------------------------------------- /packages/example-web/hooks/useCopy.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | type Text = string | null; 4 | type CopyFn = (text: string) => Promise; 5 | 6 | export default function useCopy(): [Text, CopyFn] { 7 | const [copiedText, setCopiedText] = useState(null); 8 | 9 | const copy: CopyFn = async (text) => { 10 | if (!navigator?.clipboard) { 11 | console.warn('Clipboard is not supported!'); 12 | return false; 13 | } 14 | 15 | try { 16 | await navigator.clipboard.writeText(text); 17 | setCopiedText(text); 18 | return true; 19 | } catch (error) { 20 | console.warn('Cannot write to clipboard!', error); 21 | } 22 | 23 | setCopiedText(null); 24 | return false; 25 | }; 26 | 27 | return [copiedText, copy]; 28 | } 29 | -------------------------------------------------------------------------------- /packages/example-web/merge-icon-imports.js: -------------------------------------------------------------------------------- 1 | // Since removal of barrel files from the @expo/styleguide-icons, in order to 2 | // display all icons we are automatically generating a file with all exports on 3 | // the side of example-web. 4 | const fs = require('node:fs'); 5 | const { join } = require('node:path'); 6 | 7 | const base = join(__dirname, '../..', '/node_modules/@expo/styleguide-icons'); 8 | const dirs = ['custom', 'duotone', 'outline', 'solid']; 9 | 10 | async function run() { 11 | const files = [ 12 | ...(await Promise.all( 13 | dirs.map((directory) => 14 | fs.promises 15 | .readdir(join(base, directory)) 16 | .catch((error) => { 17 | console.error(error.message); 18 | process.exit(1); 19 | }) 20 | .then((files) => 21 | files 22 | .filter((file) => file.endsWith('.js')) 23 | .filter((file) => !file.startsWith('index')) 24 | .map((file) => { 25 | const iconName = file.replaceAll('.js', '').split('/').at(-1); 26 | return `export { ${iconName} } from '@expo/styleguide-icons/${directory}/${iconName}';`; 27 | }) 28 | ) 29 | ) 30 | )), 31 | ].flat(); 32 | 33 | const content = files.join('\n'); 34 | await fs.promises.writeFile('common/icon-imports.ts', content); 35 | } 36 | 37 | run(); 38 | -------------------------------------------------------------------------------- /packages/example-web/next.config.js: -------------------------------------------------------------------------------- 1 | const withBundleAnalyzer = require('@next/bundle-analyzer')({ 2 | enabled: process.env.ANALYZE === 'true', 3 | }); 4 | 5 | /** @type {import('next').NextConfig} */ 6 | const nextConfig = { 7 | reactStrictMode: true, 8 | devIndicators: { 9 | position: 'bottom-right', 10 | }, 11 | async redirects() { 12 | return [ 13 | { 14 | source: '/ui', 15 | destination: '/ui/components', 16 | permanent: true, 17 | }, 18 | ]; 19 | }, 20 | }; 21 | 22 | module.exports = withBundleAnalyzer(nextConfig); 23 | -------------------------------------------------------------------------------- /packages/example-web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-web", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "yarn clean && yarn update-icons && next dev", 7 | "build": "next build", 8 | "analyze-build": "ANALYZE=true next build", 9 | "update-icons": "node ./merge-icon-imports.js", 10 | "clean": "rimraf .next .vercel", 11 | "lint": "yarn eslint ." 12 | }, 13 | "dependencies": { 14 | "@expo/styleguide": "workspace:*", 15 | "@expo/styleguide-icons": "workspace:*", 16 | "@expo/styleguide-search-ui": "workspace:*", 17 | "next": "^15.2.3", 18 | "react": "^18.3.1", 19 | "react-dom": "^18.3.1", 20 | "typescript": "^5.6.3" 21 | }, 22 | "devDependencies": { 23 | "@next/bundle-analyzer": "^15.2.3", 24 | "@tailwindcss/typography": "^0.5.15", 25 | "@types/node": "^18.18.9", 26 | "@types/react": "^18.3.12", 27 | "@types/react-dom": "^18.3.1", 28 | "autoprefixer": "^10.4.21", 29 | "eslint": "^8.57.1", 30 | "eslint-config-next": "^15.2.3", 31 | "eslint-plugin-tailwindcss": "^3.18.0", 32 | "postcss": "^8.5.3", 33 | "prettier-plugin-tailwindcss": "^0.6.9", 34 | "rimraf": "*", 35 | "tailwind-merge": "^2.5.4", 36 | "tailwindcss": "^3.4.17" 37 | }, 38 | "peerDependencies": { 39 | "cmdk": "*" 40 | }, 41 | "eslintConfig": { 42 | "root": true, 43 | "extends": [ 44 | "universe/web", 45 | "next/core-web-vitals", 46 | "plugin:tailwindcss/recommended" 47 | ], 48 | "rules": { 49 | "@next/next/no-html-link-for-pages": [ 50 | "error" 51 | ], 52 | "tailwindcss/classnames-order": "off", 53 | "tailwindcss/enforces-negative-arbitrary-values": "error", 54 | "tailwindcss/enforces-shorthand": "error", 55 | "tailwindcss/no-arbitrary-value": "off", 56 | "tailwindcss/no-custom-classname": [ 57 | "error", 58 | { 59 | "cssFiles": [], 60 | "whitelist": [ 61 | "dark-theme" 62 | ], 63 | "callees": [ 64 | "mergeClasses" 65 | ] 66 | } 67 | ], 68 | "tailwindcss/no-unnecessary-arbitrary-value": [ 69 | "error", 70 | { 71 | "callees": [ 72 | "mergeClasses" 73 | ] 74 | } 75 | ] 76 | }, 77 | "ignorePatterns": [ 78 | "icon-imports.ts" 79 | ] 80 | }, 81 | "prettier": { 82 | "printWidth": 120, 83 | "singleQuote": true, 84 | "bracketSameLine": true, 85 | "trailingComma": "es5", 86 | "plugins": [ 87 | "prettier-plugin-tailwindcss" 88 | ], 89 | "tailwindFunctions": [ 90 | "mergeClasses" 91 | ] 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /packages/example-web/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import { mergeClasses, ThemeProvider } from '@expo/styleguide'; 2 | import type { AppProps } from 'next/app'; 3 | import { Inter, Fira_Code } from 'next/font/google'; 4 | import Head from 'next/head'; 5 | 6 | import { SearchDialogProvider } from '@/common/SearchDialogContext'; 7 | import { Sidebar } from '@/components/Sidebar'; 8 | import { SearchMenu } from '@/components/search/SearchMenu'; 9 | 10 | import '@expo/styleguide/dist/expo-theme.css'; 11 | import '@expo/styleguide-search-ui/dist/expo-search-ui.css'; 12 | import 'public/global.css'; 13 | 14 | export const regularFont = Inter({ 15 | variable: '--regular-font', 16 | display: 'swap', 17 | subsets: ['latin'], 18 | }); 19 | 20 | export const monospaceFont = Fira_Code({ 21 | variable: '--monospace-font', 22 | weight: '400', 23 | display: 'swap', 24 | subsets: ['latin'], 25 | }); 26 | 27 | export default function App({ Component, pageProps }: AppProps) { 28 | return ( 29 | 30 | 31 | 47 | 48 | @expo/styleguide 49 | 50 | 51 |
58 | 59 |
60 | 61 |
62 |
63 | 64 |
65 |
66 | ); 67 | } 68 | -------------------------------------------------------------------------------- /packages/example-web/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import { BlockingSetInitialColorMode } from '@expo/styleguide'; 2 | import { Html, Head, Main, NextScript } from 'next/document'; 3 | 4 | export default function Document() { 5 | return ( 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /packages/example-web/pages/colors.tsx: -------------------------------------------------------------------------------- 1 | import { mergeClasses } from '@expo/styleguide'; 2 | 3 | import { getPaletteClasses } from '@/common/utils'; 4 | import { H1, H3, H4 } from '@/components/headers'; 5 | import useCopy from '@/hooks/useCopy'; 6 | 7 | export default function ColorsPage() { 8 | const [, copy] = useCopy(); 9 | return ( 10 | <> 11 |

Colors

12 |

Semantic

13 |

Backgrounds

14 |
15 | {[ 16 | 'bg-default', 17 | 'bg-screen', 18 | 'bg-subtle', 19 | 'bg-element', 20 | 'bg-hover', 21 | 'bg-selected', 22 | 'bg-overlay', 23 | 'bg-success', 24 | 'bg-warning', 25 | 'bg-danger', 26 | 'bg-info', 27 | ].map((className, index) => ( 28 |
29 |
copy(className)} 37 | /> 38 |

{className.replace('bg-', '')}

39 |
40 | ))} 41 |
42 |

Borders

43 |
44 | {['border-default', 'border-secondary', 'border-success', 'border-warning', 'border-danger', 'border-info'].map( 45 | (className, index) => ( 46 |
47 |
copy(className)} 55 | /> 56 |

{className.replace('border-', '')}

57 |
58 | ) 59 | )} 60 |
61 |

Text colors

62 |
63 | {[ 64 | 'text-default', 65 | 'text-secondary', 66 | 'text-tertiary', 67 | 'text-quaternary', 68 | 'text-success', 69 | 'text-warning', 70 | 'text-danger', 71 | 'text-info', 72 | ].map((className, index) => ( 73 |
74 |
copy(className)}> 82 | T 83 |
84 |

{className.replace('text-', '')}

85 |
86 | ))} 87 |
88 |

Palette

89 |
90 | {['red', 'orange', 'yellow', 'green', 'blue', 'purple', 'pink', 'gray'].map((color) => ( 91 |
92 | {getPaletteClasses(color).map((className, index) => ( 93 |
94 |
copy(`palette-${color}${index + 1}`)} 102 | /> 103 |

104 | {color} 105 | {index + 1} 106 |

107 |
108 | ))} 109 |
110 | ))} 111 |
112 |

Project background colors

113 |
114 | {[ 115 | 'bg-app-cyan', 116 | 'bg-app-light-blue', 117 | 'bg-app-dark-blue', 118 | 'bg-app-indigo', 119 | 'bg-app-purple', 120 | 'bg-app-pink', 121 | 'bg-app-orange', 122 | 'bg-app-gold', 123 | 'bg-app-yellow', 124 | 'bg-app-lime', 125 | 'bg-app-light-green', 126 | 'bg-app-dark-green', 127 | ].map((className, index) => ( 128 |
129 |
copy(className)} 137 | /> 138 |

{className.replace('bg-app-', '')}

139 |
140 | ))} 141 |
142 | 143 | ); 144 | } 145 | -------------------------------------------------------------------------------- /packages/example-web/pages/icons.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Logo, 3 | DocsLogo, 4 | ExpoGoLogo, 5 | SnackLogo, 6 | WordMarkLogo, 7 | RouterLogo, 8 | OrbitLogo, 9 | mergeClasses, 10 | Button, 11 | } from '@expo/styleguide'; 12 | import { CheckCircleIcon } from '@expo/styleguide-icons/outline/CheckCircleIcon'; 13 | import { PlaceholderIcon } from '@expo/styleguide-icons/outline/PlaceholderIcon'; 14 | import { SearchMdIcon } from '@expo/styleguide-icons/outline/SearchMdIcon'; 15 | import { createElement, useState } from 'react'; 16 | 17 | import * as StyleguideIcons from '@/common/icon-imports'; 18 | import { H1, H3 } from '@/components/headers'; 19 | import useCopy from '@/hooks/useCopy'; 20 | 21 | type IconNames = keyof typeof StyleguideIcons; 22 | 23 | const iconClasses = mergeClasses( 24 | 'flex flex-col items-center justify-center gap-1 rounded-md border border-transparent px-2 py-4 transition', 25 | 'hover:cursor-pointer hocus:border-secondary hocus:shadow-xs', 26 | 'active:scale-98' 27 | ); 28 | 29 | export default function IconsPage() { 30 | const [, copy] = useCopy(); 31 | const [filters, setFilters] = useState({ outline: true, solid: false, duotone: false }); 32 | const [search, setSearch] = useState(''); 33 | 34 | const iconNames = Object.keys(StyleguideIcons) 35 | .filter((key) => { 36 | let skip = false; 37 | if (!filters.solid) skip = skip || key.endsWith('SolidIcon'); 38 | if (!filters.duotone) skip = skip || key.endsWith('DuotoneIcon'); 39 | if (!filters.outline) skip = skip || (!key.endsWith('SolidIcon') && !key.endsWith('DuotoneIcon')); 40 | return !skip; 41 | }) 42 | .filter((key) => (search ? key.toLowerCase().includes(search) : true)) 43 | .filter((key) => key.endsWith('Icon')) as IconNames[]; 44 | 45 | return ( 46 |
47 |

Icons

48 |

Logos

49 |
50 | {[ 51 | { name: 'WordMarkLogo', element: WordMarkLogo }, 52 | { name: 'Logo', element: Logo }, 53 | { name: 'DocsLogo', element: DocsLogo }, 54 | { name: 'ExpoGoLogo', element: ExpoGoLogo }, 55 | { name: 'OrbitLogo', element: OrbitLogo }, 56 | { name: 'RouterLogo', element: RouterLogo }, 57 | { name: 'SnackLogo', element: SnackLogo }, 58 | ].map((logo) => ( 59 |
copy(logo.name)} key={logo.name}> 60 | {createElement(logo.element, { className: 'icon-xl text-default' })} 61 | {logo.name} 62 |
63 | ))} 64 |
65 |

Icon set

66 |
67 |
68 | 69 | setSearch(e.target.value)} 72 | /> 73 |
74 |
75 | 83 | 89 | 97 |
98 |
99 |
100 | {iconNames.map((iconName) => ( 101 |
copy(iconName)} key={iconName}> 102 | {createElement(StyleguideIcons[iconName], { className: 'icon-xl text-default translate-z' })} 103 | {iconName} 104 |
105 | ))} 106 |
107 |
108 | ); 109 | } 110 | -------------------------------------------------------------------------------- /packages/example-web/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { WordMarkLogo } from '@expo/styleguide'; 2 | 3 | import { H1 } from '@/components/headers'; 4 | 5 | export default function HomePage() { 6 | return ( 7 | <> 8 |

9 | @expo/styleguide 10 |

11 |

12 | A collection of packages used to share styles and icons across{' '} 13 | websites and projects. 14 |

15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /packages/example-web/pages/layouts.tsx: -------------------------------------------------------------------------------- 1 | import { mergeClasses } from '@expo/styleguide'; 2 | 3 | import { H1, H3 } from '@/components/headers'; 4 | 5 | const VIEWPORT_CLASS = mergeClasses( 6 | 'mx-auto mt-4 border border-default bg-screen px-6 pt-4 pb-5 rounded-lg shadow-xs' 7 | ); 8 | const SCREEN_CLASS = mergeClasses('mx-auto mt-4 min-h-28 border border-secondary bg-default px-4 pt-3 rounded-md'); 9 | 10 | export default function LayoutsPage() { 11 | return ( 12 | <> 13 |

Layouts

14 |

screen-2xl

15 |
16 | max-2xl-gutters: scope or{' '} 17 | 18 | 1572px 19 |
20 | 21 | 1524px 22 |
23 |
24 |

screen-xl

25 |
26 | max-xl-gutters: scope or{' '} 27 | 28 | 1248px 29 |
30 | 31 | 1200px 32 |
33 |
34 |

screen-lg

35 |
36 | max-lg-gutters: scope or{' '} 37 | 38 | 1008px 39 |
40 | 41 | 960px 42 |
43 |
44 |

screen-md

45 |
46 | max-md-gutters: scope or{' '} 47 | 48 | 788px 49 |
50 | 51 | 740px 52 |
53 |
54 |

screen-sm

55 |
56 | max-sm-gutters: scope or{' '} 57 | 58 | 468px 59 |
60 | 61 | 420px 62 |
63 |
64 | 65 | ); 66 | } 67 | 68 | function WidthExplanation({ screen }: { screen: string }) { 69 | return ( 70 | <> 71 | (w/min-w 72 | /max-w)-{screen} 73 |
74 | 75 | ); 76 | } 77 | -------------------------------------------------------------------------------- /packages/example-web/pages/typography.tsx: -------------------------------------------------------------------------------- 1 | import { DemoTile } from '@/components/DemoTile'; 2 | import { H1, H3 } from '@/components/headers'; 3 | 4 | export default function TypographyPage() { 5 | return ( 6 | <> 7 |

Typography

8 |

Headings classes

9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |

Text classes

21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 |
32 |

33 | Elements (legacy) 34 |

35 |
36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 |
44 | 45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /packages/example-web/pages/ui/components.tsx: -------------------------------------------------------------------------------- 1 | import { ButtonBase, Button, Link, LinkBase, type ButtonTheme } from '@expo/styleguide'; 2 | import { EasMetadataIcon } from '@expo/styleguide-icons/custom/EasMetadataIcon'; 3 | import { AlignTopArrow01Icon } from '@expo/styleguide-icons/outline/AlignTopArrow01Icon'; 4 | import { ArrowUpRightIcon } from '@expo/styleguide-icons/outline/ArrowUpRightIcon'; 5 | import { BookClosedIcon } from '@expo/styleguide-icons/outline/BookClosedIcon'; 6 | import { Diamond01Icon } from '@expo/styleguide-icons/outline/Diamond01Icon'; 7 | import { Trash01Icon } from '@expo/styleguide-icons/outline/Trash01Icon'; 8 | import { Fragment } from 'react'; 9 | 10 | import { ButtonsRow } from '@/components/ButtonsRow'; 11 | import { DemoTile } from '@/components/DemoTile'; 12 | import { H1, H3 } from '@/components/headers'; 13 | 14 | const THEMES = [ 15 | 'primary', 16 | 'secondary', 17 | 'tertiary', 18 | 'quaternary', 19 | 'primary-destructive', 20 | 'secondary-destructive', 21 | 'tertiary-destructive', 22 | ] as ButtonTheme[]; 23 | 24 | export default function ComponentsPage() { 25 | return ( 26 | <> 27 |

UI: Components

28 |

Inline Help

29 | 30 |
31 | 32 | Info text 33 |
34 |
35 | 36 |
37 | 38 | Warning text 39 |
40 |
41 | 42 |
43 | 44 | Danger text 45 |
46 |
47 | 48 |
49 | 50 | Success text 51 |
52 |
53 |

Link Base

54 | 55 | LinkBase 56 | 57 | 58 | LinkBase 59 | 60 | 61 | 62 | LinkBase 63 | 64 | 65 | 66 | LinkBase 67 | 68 | 69 | 70 | LinkBase 71 | 72 | 73 |

Link

74 | 75 | Link 76 | 77 |

Button Base

78 | 79 | alert('ButtonBase clicked')}>ButtonBase 80 | 81 |

Buttons

82 | {THEMES.map((buttonTheme) => ( 83 | 84 | 85 | 86 | 87 | ))} 88 |

Icon Buttons

89 | {THEMES.map((buttonTheme) => ( 90 | 91 | 92 | 93 | ))} 94 |

Link Buttons

95 | 96 | 99 | 100 | 101 | 104 | 105 | 106 | 113 | 114 | 115 | 123 | 124 | 125 | 128 | 129 |

Customized Buttons

130 | 131 | 134 | 142 | 143 | 144 | 149 | 155 | 156 | 157 | 165 | 174 | 175 | 176 | 177 | 180 | 183 | 184 | 185 | 186 | ); 187 | } 188 | -------------------------------------------------------------------------------- /packages/example-web/pages/ui/search.tsx: -------------------------------------------------------------------------------- 1 | import { CommandMenuTrigger } from '@expo/styleguide-search-ui'; 2 | 3 | import { useSearchDialogContext } from '@/common/SearchDialogContext'; 4 | import { DemoTile } from '@/components/DemoTile'; 5 | import { H1, H3 } from '@/components/headers'; 6 | 7 | export default function SearchPage() { 8 | const { setOpen } = useSearchDialogContext(); 9 | return ( 10 | <> 11 |

UI: Search

12 |

@expo/search-ui

13 | 14 | 15 | 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /packages/example-web/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | 'postcss-import': {}, 4 | tailwindcss: {}, 5 | autoprefixer: {}, 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /packages/example-web/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/expo/styleguide/fd85f92dfd9faa2dbf2d0880ffc023dde7fd2312/packages/example-web/public/favicon.png -------------------------------------------------------------------------------- /packages/example-web/public/global.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /packages/example-web/public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/expo/styleguide/fd85f92dfd9faa2dbf2d0880ffc023dde7fd2312/packages/example-web/public/icon.png -------------------------------------------------------------------------------- /packages/example-web/tailwind.config.js: -------------------------------------------------------------------------------- 1 | const expoTheme = require('@expo/styleguide/tailwind'); 2 | 3 | /** @type {import('tailwindcss').Config} */ 4 | module.exports = { 5 | content: [ 6 | './common/**/*.{js,ts,jsx,tsx}', 7 | './components/**/*.{js,ts,jsx,tsx}', 8 | './pages/**/*.{js,ts,jsx,tsx}', 9 | `../../node_modules/@expo/styleguide/dist/**/*.{js,ts,jsx,tsx}`, 10 | `../../node_modules/@expo/styleguide-search-ui/dist/**/*.{js,ts}`, 11 | ], 12 | ...expoTheme, 13 | }; 14 | -------------------------------------------------------------------------------- /packages/example-web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "noEmit": true, 14 | "esModuleInterop": true, 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "jsx": "preserve", 18 | "incremental": true, 19 | "baseUrl": ".", 20 | "paths": { 21 | "@/*": [ 22 | "./*" 23 | ] 24 | }, 25 | "module": "esnext", 26 | "moduleResolution": "node", 27 | "plugins": [ 28 | { 29 | "name": "next" 30 | } 31 | ] 32 | }, 33 | "include": [ 34 | "next-env.d.ts", 35 | "**/*.ts", 36 | "**/*.tsx", 37 | ".next/types/**/*.ts" 38 | ], 39 | "exclude": [ 40 | "node_modules" 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /packages/search-ui/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Expo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /packages/search-ui/README.md: -------------------------------------------------------------------------------- 1 | # @expo/styleguide-search-ui 2 | 3 | Expo's common search component for use on the web. 4 | 5 | ## Usage 6 | 7 | 1. Install Expo Search UI package: 8 | ```shell 9 | yarn add @expo/styleguide-search-ui 10 | ``` 11 | 2. Import global CSS files from the package in your JS(X)/TS(X) code: 12 | ```jsx 13 | import "@expo/styleguide-search-ui/dist/expo-search-ui.css"; 14 | ``` 15 | or import it the main stylesheet file: 16 | ```css 17 | @import "@expo/styleguide-search-ui/dist/expo-search-ui.css"; 18 | ``` 19 | 3. Add `'./node_modules/@expo/styleguide-search-ui/dist/**/*.{js,ts,jsx,tsx}'` to the Tailwind `content` paths. 20 | 21 | ## Development 22 | 23 | ### Get started 24 | 25 | 1. Install dependencies with `yarn`. 26 | 2. Build everything with `yarn build`. 27 | 3. Develop with `yarn dev`. 28 | -------------------------------------------------------------------------------- /packages/search-ui/index.ts: -------------------------------------------------------------------------------- 1 | import { Command } from 'cmdk'; 2 | 3 | export { CommandMenu } from './src/components/CommandMenu'; 4 | export { CommandMenuTrigger } from './src/components/CommandMenuTrigger'; 5 | export { CommandItemBaseWithCopy } from './src/components/CommandItemBaseWithCopy'; 6 | 7 | export { addHighlight } from './src/utils'; 8 | export * from './src/types'; 9 | 10 | export const CommandItemBase = Command.Item; 11 | -------------------------------------------------------------------------------- /packages/search-ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@expo/styleguide-search-ui", 3 | "version": "2.3.6", 4 | "description": "Expo's common search component for use on the web.", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "files": [ 8 | "dist" 9 | ], 10 | "scripts": { 11 | "clean": "rimraf dist", 12 | "bundle": "rollup --config", 13 | "build": "run-s clean bundle", 14 | "dev": "rollup --config --watch" 15 | }, 16 | "homepage": "https://github.com/expo/styleguide", 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/expo/styleguide.git", 20 | "directory": "packages/search-ui" 21 | }, 22 | "keywords": [ 23 | "expo" 24 | ], 25 | "author": "Expo", 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/expo/styleguide/issues" 29 | }, 30 | "dependencies": { 31 | "@expo/styleguide": "^9.1.2", 32 | "@expo/styleguide-icons": "^2.2.2", 33 | "@sanity/client": "^6.28.3", 34 | "@sanity/image-url": "^1.1.0", 35 | "cmdk": "^0.2.1", 36 | "lodash.groupby": "^4.6.0" 37 | }, 38 | "devDependencies": { 39 | "@types/lodash.groupby": "^4.6.9", 40 | "npm-run-all": "*", 41 | "rimraf": "*", 42 | "rollup": "*", 43 | "tailwindcss": "^3.4.17", 44 | "user-agent-data-types": "^0.4.2" 45 | }, 46 | "peerDependencies": { 47 | "next": ">= 13", 48 | "react": ">= 16" 49 | }, 50 | "eslintConfig": { 51 | "extends": "universe/web" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /packages/search-ui/rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import terser from '@rollup/plugin-terser'; 2 | import typescript from '@rollup/plugin-typescript'; 3 | import copy from 'rollup-plugin-copy'; 4 | 5 | const config = [ 6 | { 7 | input: 'index.ts', 8 | output: { 9 | dir: 'dist', 10 | format: 'cjs', 11 | }, 12 | plugins: [ 13 | typescript(), 14 | terser(), 15 | copy({ 16 | targets: [ 17 | { src: './src/styles/expo-search-ui.css', dest: 'dist' }, 18 | ], 19 | }), 20 | ], 21 | external: [ 22 | '@expo/styleguide', 23 | '@expo/styleguide-base', 24 | '@expo/styleguide-icons', 25 | '@sanity/client', 26 | '@sanity/image-url', 27 | 'cmdk', 28 | 'lodash.groupby', 29 | 'react', 30 | 'tailwind-merge' 31 | ], 32 | }, 33 | ]; 34 | 35 | export default config; 36 | -------------------------------------------------------------------------------- /packages/search-ui/src/Items/ExpoBlogItem.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { ExternalLinkIcon } from './icons'; 4 | import { CommandItemBaseWithCopy } from '../components/CommandItemBaseWithCopy'; 5 | import { ExpoBlogItemType } from '../types'; 6 | import { addHighlight, getSanityAsset } from '../utils'; 7 | 8 | type Props = { 9 | item: ExpoBlogItemType; 10 | query: string; 11 | onSelect?: () => void; 12 | }; 13 | 14 | export const ExpoBlogItem = ({ item, onSelect, query }: Props) => { 15 | return ( 16 | 21 |
22 | {item.mainImage.altText} 27 |
28 |

29 |

33 |

34 | 35 |
36 |
37 | ); 38 | }; 39 | -------------------------------------------------------------------------------- /packages/search-ui/src/Items/ExpoDocsItem.tsx: -------------------------------------------------------------------------------- 1 | import { DocsLogo, mergeClasses } from '@expo/styleguide'; 2 | import { PlanEnterpriseIcon } from '@expo/styleguide-icons/custom/PlanEnterpriseIcon'; 3 | import { BookOpen02Icon } from '@expo/styleguide-icons/outline/BookOpen02Icon'; 4 | import { GraduationHat02Icon } from '@expo/styleguide-icons/outline/GraduationHat02Icon'; 5 | import { Hash02Icon } from '@expo/styleguide-icons/outline/Hash02Icon'; 6 | import { Home02Icon } from '@expo/styleguide-icons/outline/Home02Icon'; 7 | import React from 'react'; 8 | 9 | import { FootnoteSection } from './FootnoteSection'; 10 | import { ExternalLinkIcon, FootnoteArrowIcon } from './icons'; 11 | import { CommandItemBaseWithCopy } from '../components/CommandItemBaseWithCopy'; 12 | import type { AlgoliaItemType } from '../types'; 13 | import { 14 | getContentHighlightHTML, 15 | getHighlightHTML, 16 | isReferencePath, 17 | isEASPath, 18 | isHomePath, 19 | isLearnPath, 20 | } from '../utils'; 21 | 22 | type Props = { 23 | item: AlgoliaItemType; 24 | onSelect?: () => void; 25 | isNested?: boolean; 26 | transformUrl?: (url: string) => string; 27 | }; 28 | 29 | type ItemIconProps = { 30 | url: string; 31 | className?: string; 32 | isNested?: boolean; 33 | }; 34 | 35 | const ItemIcon = ({ url, className, isNested }: ItemIconProps) => { 36 | if (isNested) { 37 | return ; 38 | } else if (isReferencePath(url)) { 39 | return ; 40 | } else if (isEASPath(url)) { 41 | return ; 42 | } else if (isHomePath(url)) { 43 | return ; 44 | } else if (isLearnPath(url)) { 45 | return ; 46 | } 47 | return ; 48 | }; 49 | 50 | const getFootnotePrefix = (url: string) => { 51 | if (isReferencePath(url)) { 52 | return 'Reference'; 53 | } else if (isEASPath(url)) { 54 | return 'Expo Application Services'; 55 | } else if (isHomePath(url)) { 56 | return 'Home'; 57 | } else if (isLearnPath(url)) { 58 | return 'Learn'; 59 | } else { 60 | return 'Guides'; 61 | } 62 | }; 63 | 64 | const ItemFootnotePrefix = ({ url, isNested = false }: { url: string; isNested?: boolean }) => { 65 | return isNested ? ( 66 | <> 67 | {getFootnotePrefix(url)} 68 | 69 | 70 | ) : ( 71 |

{getFootnotePrefix(url)}

72 | ); 73 | }; 74 | 75 | export const ExpoDocsItem = ({ item, onSelect, isNested, transformUrl }: Props) => { 76 | const { lvl0, lvl2, lvl3, lvl4, lvl6 } = item.hierarchy; 77 | 78 | const titleClasses = mergeClasses(isNested ? 'text-2xs' : 'text-xs font-medium'); 79 | const hierarchyClasses = mergeClasses('text-3xs text-quaternary', isNested && 'hidden'); 80 | 81 | return ( 82 | 89 |
90 |
91 | 92 |
93 | {lvl6 && ( 94 | <> 95 |

96 | {!isNested && ( 97 |

98 | 99 | 100 | 101 | 102 | 103 |

104 | )} 105 | 106 | )} 107 | {!lvl6 && lvl4 && ( 108 | <> 109 |

110 | {!isNested && ( 111 |

112 | 113 | 114 | 115 | 116 |

117 | )} 118 | 119 | )} 120 | {!lvl6 && !lvl4 && lvl3 && ( 121 | <> 122 |

123 | {!isNested && ( 124 |

125 | 126 | 127 | 128 |

129 | )} 130 | 131 | )} 132 | {!lvl6 && !lvl4 && !lvl3 && lvl2 && ( 133 | <> 134 |

135 | {!isNested && ( 136 |

137 | 138 | 139 |

140 | )} 141 | 142 | )} 143 | {!lvl6 && !lvl4 && !lvl3 && !lvl2 && lvl0 && ( 144 | <> 145 |

146 | 147 | 148 | )} 149 | {(!isNested || item.content) && ( 150 |

154 | )} 155 |

156 |
157 | {!transformUrl && } 158 |
159 |
160 | ); 161 | }; 162 | -------------------------------------------------------------------------------- /packages/search-ui/src/Items/FootnoteSection.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { FootnoteArrowIcon } from './icons'; 4 | import type { AlgoliaItemHierarchy, AlgoliaItemType } from '../types'; 5 | import { getHighlightHTML } from '../utils'; 6 | 7 | export const FootnoteSection = ({ 8 | item, 9 | levelKey = 'lvl0', 10 | }: { 11 | item: AlgoliaItemType; 12 | levelKey: keyof AlgoliaItemHierarchy; 13 | }) => 14 | item.hierarchy[levelKey] ? ( 15 | <> 16 | 17 | 18 | 19 | ) : null; 20 | -------------------------------------------------------------------------------- /packages/search-ui/src/Items/RNDirectoryItem.tsx: -------------------------------------------------------------------------------- 1 | import { GithubIcon } from '@expo/styleguide-icons/custom/GithubIcon'; 2 | import { Download01Icon } from '@expo/styleguide-icons/outline/Download01Icon'; 3 | import { Star01Icon } from '@expo/styleguide-icons/outline/Star01Icon'; 4 | import React from 'react'; 5 | 6 | import { ExternalLinkIcon } from './icons'; 7 | import { CommandItemBaseWithCopy } from '../components/CommandItemBaseWithCopy'; 8 | import type { RNDirectoryItemType } from '../types'; 9 | import { addHighlight } from '../utils'; 10 | 11 | type Props = { 12 | item: RNDirectoryItemType; 13 | query: string; 14 | onSelect?: () => void; 15 | }; 16 | 17 | const numberFormat = new Intl.NumberFormat(); 18 | 19 | export const RNDirectoryItem = ({ item, onSelect, query }: Props) => { 20 | return ( 21 | 22 |
23 | 24 |
25 |

26 |

27 | 28 | {numberFormat.format(item.github.stats.stars)} stars 29 | {item.npm.downloads ? ( 30 | <> 31 | {' '} 32 | · 33 | {numberFormat.format(item.npm.downloads)} downloads 34 | 35 | ) : undefined} 36 |

37 |
38 | 39 |
40 |
41 | ); 42 | }; 43 | -------------------------------------------------------------------------------- /packages/search-ui/src/Items/RNDocsItem.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { FootnoteSection } from './FootnoteSection'; 4 | import { ExternalLinkIcon, ReactIcon } from './icons'; 5 | import { CommandItemBaseWithCopy } from '../components/CommandItemBaseWithCopy'; 6 | import type { AlgoliaItemType } from '../types'; 7 | import { getContentHighlightHTML, getHighlightHTML } from '../utils'; 8 | 9 | type Props = { 10 | item: AlgoliaItemType; 11 | onSelect?: () => void; 12 | }; 13 | 14 | export const RNDocsItem = ({ item, onSelect }: Props) => { 15 | const { lvl0, lvl1, lvl2, lvl3, lvl4 } = item.hierarchy; 16 | return ( 17 | 18 |
19 | 20 |
21 | {lvl4 && ( 22 | <> 23 |

24 |

25 | 26 | 27 | 28 | 29 |

30 | 31 | )} 32 | {!lvl4 && lvl3 && ( 33 | <> 34 |

35 |

36 | 37 | 38 | 39 |

40 | 41 | )} 42 | {!lvl3 && lvl2 && ( 43 | <> 44 |

45 |

46 | 47 | 48 |

49 | 50 | )} 51 | {!lvl3 && !lvl2 && lvl1 && ( 52 | <> 53 |

54 |

55 | 56 | )} 57 | {!lvl3 && !lvl2 && !lvl1 && lvl0 &&

} 58 |

59 |

60 | 61 |
62 |
63 | ); 64 | }; 65 | -------------------------------------------------------------------------------- /packages/search-ui/src/Items/icons.tsx: -------------------------------------------------------------------------------- 1 | import { mergeClasses } from '@expo/styleguide'; 2 | import { ArrowUpRightIcon } from '@expo/styleguide-icons/outline/ArrowUpRightIcon'; 3 | import { ChevronRightIcon } from '@expo/styleguide-icons/outline/ChevronRightIcon'; 4 | import React, { HTMLAttributes } from 'react'; 5 | 6 | export function FootnoteArrowIcon() { 7 | return ; 8 | } 9 | 10 | export const ExternalLinkIcon = () => ; 11 | 12 | export const ReactIcon = ({ className }: HTMLAttributes) => ( 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | ); 22 | 23 | export const AlgoliaLogo = () => ( 24 | 32 | 33 | 38 | 39 | 44 | 49 | 54 | 59 | 64 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | ); 78 | -------------------------------------------------------------------------------- /packages/search-ui/src/Items/index.ts: -------------------------------------------------------------------------------- 1 | export { RNDirectoryItem } from './RNDirectoryItem'; 2 | export { RNDocsItem } from './RNDocsItem'; 3 | export { ExpoDocsItem } from './ExpoDocsItem'; 4 | -------------------------------------------------------------------------------- /packages/search-ui/src/components/BarLoader.tsx: -------------------------------------------------------------------------------- 1 | import { mergeClasses } from '@expo/styleguide'; 2 | import React from 'react'; 3 | 4 | type Props = { isLoading?: boolean }; 5 | 6 | export const BarLoader = ({ isLoading }: Props) => ( 7 |
14 | ); 15 | -------------------------------------------------------------------------------- /packages/search-ui/src/components/CommandFooter.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { AlgoliaLogo } from '../Items/icons'; 4 | 5 | export const CommandFooter = () => ( 6 |
7 |

8 | 9 | to select 10 |

11 |

12 | 13 | 14 | to navigate 15 |

16 |

17 | esc 18 | to close 19 |

20 |

21 | Search by 22 | 25 | 26 | 27 |

28 |
29 | ); 30 | -------------------------------------------------------------------------------- /packages/search-ui/src/components/CommandItemBaseWithCopy.tsx: -------------------------------------------------------------------------------- 1 | import { mergeClasses } from '@expo/styleguide'; 2 | import { Command } from 'cmdk'; 3 | import React, { PropsWithChildren, useState } from 'react'; 4 | 5 | import { openLink } from '../utils'; 6 | 7 | type Props = PropsWithChildren<{ 8 | url: string; 9 | onSelect?: () => void; 10 | isExternalLink?: boolean; 11 | isNested?: boolean; 12 | className?: string; 13 | value?: string; 14 | }>; 15 | 16 | export const CommandItemBaseWithCopy = ({ 17 | children, 18 | url, 19 | isExternalLink, 20 | isNested, 21 | onSelect, 22 | className, 23 | value, 24 | }: Props) => { 25 | const [copyDone, setCopyDone] = useState(false); 26 | const [isMetaClick, setMetaClick] = useState(false); 27 | 28 | const copyUrl = () => { 29 | navigator.clipboard?.writeText(url); 30 | setCopyDone(true); 31 | setTimeout(() => setCopyDone(false), 1500); 32 | }; 33 | 34 | return ( 35 | { 40 | if (event.metaKey || event.ctrlKey) { 41 | setMetaClick(true); 42 | } 43 | }} 44 | onMouseUp={(event) => { 45 | // note(Keith): middle click (typical *nix copy shortcut), right click (works with Mac trackpads), onAuxClick is not supported in Safari 46 | if (event.button === 1 || event.button === 2) { 47 | copyUrl(); 48 | } 49 | }} 50 | onSelect={() => { 51 | openLink(url, isMetaClick ? true : isExternalLink); 52 | if (isMetaClick) { 53 | setMetaClick(false); 54 | } else { 55 | onSelect && onSelect(); 56 | } 57 | }} 58 | onContextMenu={(event) => { 59 | event.preventDefault(); 60 | }}> 61 | {children} 62 | {copyDone && ( 63 |
64 | Copied! 65 |
66 | )} 67 |
68 | ); 69 | }; 70 | -------------------------------------------------------------------------------- /packages/search-ui/src/components/CommandMenu.tsx: -------------------------------------------------------------------------------- 1 | import { SearchSmIcon } from '@expo/styleguide-icons/outline/SearchSmIcon'; 2 | import { XIcon } from '@expo/styleguide-icons/outline/XIcon'; 3 | import { Command } from 'cmdk'; 4 | import groupBy from 'lodash.groupby'; 5 | import React, { useEffect, useState, Dispatch, SetStateAction } from 'react'; 6 | 7 | import { BarLoader } from './BarLoader'; 8 | import { CommandFooter } from './CommandFooter'; 9 | import { RNDirectoryItem, RNDocsItem, ExpoDocsItem } from '../Items'; 10 | import { ExpoBlogItem } from '../Items/ExpoBlogItem'; 11 | import type { RNDirectoryItemType, AlgoliaItemType, CommandMenuConfig, CommandMenuSection } from '../types'; 12 | import { ExpoBlogItemType } from '../types'; 13 | import { 14 | getExpoDocsResults, 15 | getRNDocsResults, 16 | getDirectoryResults, 17 | getItemsAsync, 18 | isAppleDevice, 19 | getExpoBlogResults, 20 | getSanityItemsAsync, 21 | } from '../utils'; 22 | 23 | type Props = { 24 | open: boolean; 25 | setOpen: Dispatch>; 26 | config: CommandMenuConfig; 27 | customSections?: CommandMenuSection[]; 28 | }; 29 | 30 | export const CommandMenu = ({ 31 | config: { docsVersion, docsTransformUrl }, 32 | open, 33 | setOpen, 34 | customSections = [], 35 | }: Props) => { 36 | const [initialized, setInitialized] = useState(false); 37 | const [loading, setLoading] = useState(true); 38 | const [query, setQuery] = useState(''); 39 | const [isMac, setIsMac] = useState(null); 40 | 41 | const [expoDocsItems, setExpoDocsItems] = useState([]); 42 | const [expoBlogItems, setExpoBlogItems] = useState([]); 43 | const [rnDocsItems, setRnDocsItems] = useState([]); 44 | const [directoryItems, setDirectoryItems] = useState([]); 45 | 46 | const getExpoDocsItems = async () => getItemsAsync(query, getExpoDocsResults, setExpoDocsItems, docsVersion); 47 | const getExpoBlogItems = async () => getSanityItemsAsync(query, getExpoBlogResults, setExpoBlogItems); 48 | const getRNDocsItems = async () => getItemsAsync(query, getRNDocsResults, setRnDocsItems); 49 | const getDirectoryItems = async () => getItemsAsync(query, getDirectoryResults, setDirectoryItems); 50 | 51 | const dismiss = () => setOpen(false); 52 | 53 | const fetchData = (callback: () => void) => { 54 | Promise.all([ 55 | getExpoDocsItems(), 56 | getExpoBlogItems(), 57 | getRNDocsItems(), 58 | getDirectoryItems(), 59 | ...customSections?.map((section) => section.getItemsAsync(query)), 60 | ]).then(callback); 61 | }; 62 | 63 | const onQueryChange = () => { 64 | if (open) { 65 | setLoading(true); 66 | const inputTimeout = setTimeout(() => fetchData(() => setLoading(false)), 150); 67 | return () => clearTimeout(inputTimeout); 68 | } 69 | }; 70 | 71 | const onMenuOpen = () => { 72 | if (open && !initialized) { 73 | fetchData(() => { 74 | setInitialized(true); 75 | setLoading(false); 76 | }); 77 | } 78 | }; 79 | 80 | useEffect(() => { 81 | setIsMac(typeof navigator !== 'undefined' && isAppleDevice()); 82 | }, []); 83 | 84 | useEffect(() => { 85 | if (isMac !== null) { 86 | const keyDownListener = (e: KeyboardEvent) => { 87 | if (e.key === 'k' && (isMac ? e.metaKey : e.ctrlKey)) { 88 | e.preventDefault(); 89 | setOpen((open) => !open); 90 | } 91 | }; 92 | document.addEventListener('keydown', keyDownListener, false); 93 | return () => document.removeEventListener('keydown', keyDownListener); 94 | } 95 | }, [isMac]); 96 | 97 | useEffect(onMenuOpen, [open]); 98 | useEffect(onQueryChange, [query]); 99 | 100 | const expoDocsGroupedItems = groupBy( 101 | expoDocsItems.map((expoDocsItem: AlgoliaItemType) => ({ 102 | ...expoDocsItem, 103 | baseUrl: expoDocsItem.url.replace(/#.+/, ''), 104 | })), 105 | 'baseUrl' 106 | ); 107 | 108 | const data = [ 109 | expoDocsItems.length > 0 && ( 110 | 111 | {Object.values(expoDocsGroupedItems).map((values) => 112 | values 113 | .sort((a, b) => a.url.localeCompare(a.baseUrl) - b.url.localeCompare(b.baseUrl)) 114 | .slice(0, 6) 115 | .map((item, index) => ( 116 | 123 | )) 124 | )} 125 | 126 | ), 127 | expoBlogItems.length > 0 && ( 128 | 129 | {expoBlogItems.map((item) => ( 130 | 131 | ))} 132 | 133 | ), 134 | rnDocsItems.length > 0 && ( 135 | 136 | {rnDocsItems.map((item) => ( 137 | 138 | ))} 139 | 140 | ), 141 | directoryItems.length > 0 && ( 142 | 143 | {directoryItems.map((item) => ( 144 | 145 | ))} 146 | 147 | ), 148 | ]; 149 | 150 | customSections?.forEach( 151 | ({ items, heading, sectionIndex }) => 152 | items.length > 0 && 153 | data.splice( 154 | sectionIndex, 155 | 0, 156 | 157 | {items} 158 | 159 | ) 160 | ); 161 | 162 | return ( 163 | 164 | 165 |
166 | setOpen(false)} /> 167 |
168 | 169 | 170 | 171 | {initialized && data} 172 | {data.filter(Boolean).length === 0 && ( 173 | 174 |

No results found.

175 |
176 | )} 177 |
178 | 179 |
180 | ); 181 | }; 182 | -------------------------------------------------------------------------------- /packages/search-ui/src/components/CommandMenuTrigger.tsx: -------------------------------------------------------------------------------- 1 | import { Button, mergeClasses } from '@expo/styleguide'; 2 | import { SearchSmIcon } from '@expo/styleguide-icons/outline/SearchSmIcon'; 3 | import React, { Dispatch, SetStateAction, useEffect, useState } from 'react'; 4 | 5 | import { isAppleDevice } from '../utils'; 6 | 7 | type Props = { 8 | setOpen: Dispatch>; 9 | className?: string; 10 | }; 11 | 12 | export const CommandMenuTrigger = ({ setOpen, className }: Props) => { 13 | const [isMac, setIsMac] = useState(null); 14 | 15 | useEffect(() => { 16 | setIsMac(typeof navigator !== 'undefined' && isAppleDevice()); 17 | }, []); 18 | 19 | return ( 20 | 38 | ); 39 | }; 40 | -------------------------------------------------------------------------------- /packages/search-ui/src/styles/expo-search-ui.css: -------------------------------------------------------------------------------- 1 | @keyframes searchUIBarLoader { 2 | 0% { 3 | width: 0; 4 | } 5 | 80% { 6 | width: 100%; 7 | opacity: 1; 8 | } 9 | 100% { 10 | width: 100%; 11 | opacity: 0; 12 | } 13 | } 14 | 15 | #__next[aria-hidden] { 16 | filter: blur(3.33px); 17 | } 18 | 19 | [cmdk-overlay] { 20 | background-color: rgba(0, 0, 0, 0.33); 21 | height: 100vh; 22 | left: 0; 23 | position: fixed; 24 | top: 0; 25 | width: 100vw; 26 | z-index: 1000; 27 | } 28 | 29 | [cmdk-root] { 30 | position: fixed; 31 | top: 45%; 32 | left: 50%; 33 | transform: translate(-50%, -50%); 34 | min-height: 75vh; 35 | max-height: 75vh; 36 | background: var(--expo-theme-background-default); 37 | border-radius: 10px; 38 | box-shadow: var(--expo-theme-shadows-sm); 39 | width: 40vw; 40 | min-width: 680px; 41 | border: 1px solid var(--expo-theme-border-default); 42 | z-index: 1001; 43 | } 44 | 45 | @media screen and (max-width: 788px) { 46 | [cmdk-root] { 47 | top: 50%; 48 | width: 96vw; 49 | min-width: 96vw; 50 | min-height: 96dvh; 51 | max-height: 96dvh; 52 | } 53 | } 54 | 55 | [cmdk-root] kbd, .cmdk-trigger kbd { 56 | font-size: 0.8125rem; 57 | line-height: 19px; 58 | letter-spacing: -0.003rem; 59 | background-color: var(--expo-theme-background-subtle); 60 | border: 1px solid var(--expo-theme-border-default); 61 | white-space: pre-wrap; 62 | font-weight: 500; 63 | color: var(--expo-theme-text-secondary); 64 | padding: 0 4px; 65 | box-shadow: 0 0.1rem 0 1px var(--expo-theme-border-default); 66 | border-radius: 4px; 67 | position: relative; 68 | display: inline-flex; 69 | min-width: 22px; 70 | justify-content: center; 71 | top: -1px; 72 | } 73 | 74 | [cmdk-input] { 75 | appearance: none; 76 | background: transparent; 77 | color: var(--expo-theme-text-default); 78 | flex: 1; 79 | font: inherit; 80 | height: 100%; 81 | outline: none; 82 | padding: 0 44px; 83 | min-height: 46px; 84 | margin: 16px 16px 0; 85 | border: 1px solid var(--expo-theme-border-default); 86 | border-radius: 6px; 87 | width: calc(100% - 32px); 88 | box-sizing: border-box; 89 | box-shadow: var(--expo-theme-shadows-xs); 90 | } 91 | 92 | [cmdk-input]::placeholder { 93 | color: var(--expo-theme-icon-secondary); 94 | } 95 | 96 | [cmdk-item] { 97 | content-visibility: auto; 98 | cursor: pointer; 99 | min-height: 52px; 100 | border-radius: 6px; 101 | display: flex; 102 | flex-direction: column; 103 | justify-content: center; 104 | padding: 4px 12px; 105 | color: var(--expo-theme-text-default); 106 | user-select: none; 107 | will-change: background, color; 108 | transition: all 150ms ease; 109 | transition-property: none; 110 | } 111 | 112 | [cmdk-item][aria-selected='true'], 113 | [cmdk-item]:active { 114 | background: var(--expo-theme-background-element); 115 | color: var(--expo-theme-text-default); 116 | } 117 | 118 | [cmdk-item][aria-disabled='true'] { 119 | color: var(--expo-theme-icon-secondary); 120 | cursor: not-allowed; 121 | } 122 | 123 | [cmdk-item] + [cmdk-item] { 124 | margin-top: 4px; 125 | } 126 | 127 | [cmdk-item] mark { 128 | color: var(--blue-12); 129 | background: var(--blue-4); 130 | border-radius: 2px; 131 | opacity: 0.85; 132 | } 133 | 134 | [cmdk-list] { 135 | height: calc(75vh - 50px - 50px - 20px); 136 | max-height: calc(75vh - 50px - 50px - 20px); 137 | overflow: auto; 138 | overscroll-behavior: contain; 139 | border-top: 1px solid var(--expo-theme-border-default); 140 | border-bottom: 1px solid var(--expo-theme-border-default); 141 | padding: 0 16px 12px; 142 | margin: 12px 0 0; 143 | } 144 | 145 | @media screen and (max-width: 788px) { 146 | [cmdk-list] { 147 | height: calc(96dvh - 50px - 50px - 20px); 148 | max-height: calc(96dvh - 50px - 50px - 20px); 149 | } 150 | } 151 | 152 | [cmdk-separator] { 153 | height: 1px; 154 | width: 100%; 155 | background: var(--expo-theme-border-default); 156 | margin: 4px 0; 157 | } 158 | 159 | [cmdk-group-heading] { 160 | font-size: 0.75rem; 161 | line-height: 1.58; 162 | user-select: none; 163 | color: var(--expo-theme-text-secondary); 164 | padding: 16px 8px 8px; 165 | display: flex; 166 | align-items: center; 167 | gap: 4px; 168 | margin: 0 2px; 169 | } 170 | 171 | [cmdk-empty] { 172 | display: flex; 173 | align-items: center; 174 | justify-content: center; 175 | white-space: pre-wrap; 176 | padding: 32px 0; 177 | } 178 | 179 | html.dark-theme [cmdk-item] mark { 180 | background: var(--blue-6); 181 | } 182 | 183 | html.dark-theme [cmdk-item][data-selected='true'] mark { 184 | background: var(--blue-7); 185 | } 186 | 187 | [cmdk-list]::-webkit-scrollbar { 188 | width: 6px; 189 | } 190 | 191 | [cmdk-list]::-webkit-scrollbar-track { 192 | background-color: transparent; 193 | cursor: pointer; 194 | } 195 | 196 | [cmdk-list]::-webkit-scrollbar-thumb { 197 | background-color: var(--slate-5); 198 | border-radius: 10px; 199 | } 200 | 201 | [cmdk-list]::-webkit-scrollbar-thumb:hover { 202 | background-color: var(--slate-6); 203 | } 204 | -------------------------------------------------------------------------------- /packages/search-ui/src/types.ts: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from 'react'; 2 | 3 | export type CommandMenuConfig = { 4 | docsVersion: string; 5 | docsTransformUrl?: (url: string) => string; 6 | disableDashboardSection?: boolean; 7 | }; 8 | 9 | export type CommandMenuSection = { 10 | heading: string; 11 | getItemsAsync: (query: string) => Promise; 12 | items: ReactNode[]; 13 | sectionIndex: number; 14 | }; 15 | 16 | export type AlgoliaHighlight = { 17 | value: string; 18 | }; 19 | 20 | export type AlgoliaItemHierarchy = { 21 | lvl0?: T | null; 22 | lvl1?: T | null; 23 | lvl2?: T | null; 24 | lvl3?: T | null; 25 | lvl4?: T | null; 26 | lvl5?: T | null; 27 | lvl6?: T | null; 28 | }; 29 | 30 | export type AlgoliaItemType = { 31 | url: string; 32 | objectID: string; 33 | anchor: string | null; 34 | content: string | null; 35 | hierarchy: AlgoliaItemHierarchy; 36 | _highlightResult: { 37 | content: AlgoliaHighlight | null; 38 | hierarchy: AlgoliaItemHierarchy; 39 | }; 40 | }; 41 | 42 | export type RNDirectoryItemType = { 43 | npmPkg: string; 44 | githubUrl: string; 45 | npm: { 46 | downloads: number; 47 | }; 48 | github: { 49 | stats: { 50 | stars: number; 51 | }; 52 | }; 53 | }; 54 | 55 | export type ExpoBlogItemType = { 56 | title: string; 57 | tags: string[]; 58 | metadataDescription: string; 59 | slug: { 60 | current: string; 61 | }; 62 | mainImage: { 63 | altText: string; 64 | image: { 65 | asset: { 66 | _ref: string; 67 | }; 68 | }; 69 | }; 70 | }; 71 | -------------------------------------------------------------------------------- /packages/search-ui/src/utils.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import { ClientReturn, createClient } from '@sanity/client'; 4 | import imageUrlBuilder from '@sanity/image-url'; 5 | import type { Dispatch, SetStateAction } from 'react'; 6 | 7 | import type { AlgoliaItemHierarchy, AlgoliaItemType } from './types'; 8 | 9 | export const SANITY_CLIENT = createClient({ 10 | projectId: 'siias52v', 11 | dataset: 'production', 12 | useCdn: true, 13 | apiVersion: '2024-09-03', 14 | }); 15 | 16 | const sanityAssetHelper = imageUrlBuilder({ projectId: 'siias52v', dataset: 'production' }); 17 | 18 | export function getSanityAsset(source: string) { 19 | return sanityAssetHelper.image(source).auto('format').height(150).url(); 20 | } 21 | 22 | export const getItemsAsync = async ( 23 | query: string, 24 | fetcher: (query: string, version?: string) => Promise, 25 | setter: Dispatch>, 26 | version?: string 27 | ) => { 28 | const { hits, libraries } = await fetcher(query, version).then((response) => response.json()); 29 | setter(hits || libraries || []); 30 | }; 31 | 32 | export const getSanityItemsAsync = async ( 33 | query: string, 34 | fetcher: (query: string, version?: string) => Promise>, 35 | setter: Dispatch> 36 | ) => { 37 | setter(await fetcher(query)); 38 | }; 39 | 40 | const getAlgoliaFetchParams = ( 41 | query: string, 42 | appId: string, 43 | apiKey: string, 44 | indexName: string, 45 | hits: number, 46 | additionalParams: object = {} 47 | ): [string, RequestInit] => [ 48 | `https://${appId}-dsn.algolia.net/1/indexes/${indexName}/query`, 49 | { 50 | method: 'POST', 51 | headers: { 52 | 'X-Algolia-Application-Id': appId, 53 | 'X-Algolia-API-Key': apiKey, 54 | }, 55 | body: JSON.stringify({ 56 | params: `query=${query}&hitsPerPage=${hits}`, 57 | highlightPreTag: '', 58 | highlightPostTag: '', 59 | ...additionalParams, 60 | }), 61 | }, 62 | ]; 63 | 64 | export const getExpoDocsResults = (query: string, version?: string) => { 65 | return fetch( 66 | ...getAlgoliaFetchParams(query, 'QEX7PB7D46', '6652d26570e8628af4601e1d78ad456b', 'expo', 20, { 67 | facetFilters: [['version:none', `version:${version}`]], 68 | }) 69 | ); 70 | }; 71 | 72 | export const getRNDocsResults = (query: string) => { 73 | return fetch( 74 | ...getAlgoliaFetchParams(query, '8TDSE0OHGQ', 'c9c791d9d5fd7f315d7f3859b32c1f3b', 'react-native-v2', 5, { 75 | facetFilters: [['version:current']], 76 | }) 77 | ); 78 | }; 79 | 80 | export const getExpoBlogResults = (query: string) => { 81 | return SANITY_CLIENT.fetch( 82 | `*[_type == "post" && publishAt < now() && (title match "${query}*" || metadataDescription match "${query}*")] | order(publishAt desc)[0...10] { 83 | title, 84 | slug, 85 | tags, 86 | metadataDescription, 87 | mainImage 88 | }` 89 | ); 90 | }; 91 | 92 | export const getDirectoryResults = (query: string) => { 93 | return fetch(`https://reactnative.directory/api/libraries?search=${encodeURI(query)}&limit=5`); 94 | }; 95 | 96 | export const getHighlightHTML = (item: AlgoliaItemType, tag: keyof AlgoliaItemHierarchy) => ({ 97 | dangerouslySetInnerHTML: { 98 | __html: item._highlightResult.hierarchy[`${tag}`]?.value || '', 99 | }, 100 | }); 101 | 102 | const trimContent = (content: string, length = 36) => { 103 | if (!content || !content.length) return ''; 104 | 105 | const trimStart = Math.max(content.indexOf('') - length, 0); 106 | const trimEnd = Math.min(content.indexOf('') + length + 6, content.length); 107 | 108 | return `${trimStart !== 0 ? '…' : ''}${content.substring(trimStart, trimEnd).trim()}${ 109 | trimEnd !== content.length ? '…' : '' 110 | }`; 111 | }; 112 | 113 | export const getContentHighlightHTML = (item: AlgoliaItemType, skipDescription = false) => 114 | skipDescription 115 | ? {} 116 | : { 117 | dangerouslySetInnerHTML: { 118 | __html: item._highlightResult.content?.value 119 | ? trimContent(item._highlightResult.content?.value) 120 | : trimContent(item._highlightResult.hierarchy.lvl1?.value || '', 82), 121 | }, 122 | }; 123 | 124 | // note(simek): this code make sure that browser popup blocker 125 | // do not prevent opening links via key press (when it fires windows.open) 126 | export const openLink = (url: string, isExternal: boolean = false) => { 127 | const link = document.createElement('a'); 128 | if (isExternal) { 129 | link.target = '_blank'; 130 | link.rel = 'noopener noreferrer'; 131 | } 132 | link.href = url; 133 | link.click(); 134 | }; 135 | 136 | const ReferencePathChunks = ['/versions/', '/more/'] as const; 137 | 138 | export const isReferencePath = (url: string) => { 139 | return ReferencePathChunks.some((pathChunk) => url.includes(pathChunk)); 140 | }; 141 | 142 | const EASPathChunks = [ 143 | '/app-signing/', 144 | '/build/', 145 | '/build-reference/', 146 | '/development/', 147 | '/eas/', 148 | '/eas/metadata/', 149 | '/eas-update/', 150 | '/submit/', 151 | ] as const; 152 | 153 | export const isEASPath = (url: string) => { 154 | return EASPathChunks.some((pathChunk) => url.includes(pathChunk)); 155 | }; 156 | 157 | const HomePathChunks = [ 158 | '/get-started/', 159 | '/develop/', 160 | '/deploy/', 161 | '/faq/', 162 | '/core-concepts/', 163 | '/debugging/', 164 | '/config-plugins/', 165 | ] as const; 166 | 167 | export const isHomePath = (url: string) => { 168 | return HomePathChunks.some((pathChunk) => url.includes(pathChunk)); 169 | }; 170 | 171 | const LearnPathChunks = ['/tutorial', '/ui-programming/', '/additional-resources/'] as const; 172 | 173 | export const isLearnPath = (url: string) => { 174 | return LearnPathChunks.some((pathChunk) => url.includes(pathChunk)); 175 | }; 176 | 177 | export const isAppleDevice = () => { 178 | return /(Mac|iPhone|iPod|iPad)/i.test(navigator?.platform ?? navigator?.userAgentData?.platform ?? ''); 179 | }; 180 | 181 | export const addHighlight = (content: string, query: string) => { 182 | if (!content || !content.length) { 183 | return ''; 184 | } 185 | 186 | const highlightStart = content.toLowerCase().indexOf(query.toLowerCase()); 187 | 188 | if (highlightStart === -1) return content; 189 | 190 | const highlightEnd = highlightStart + query.length; 191 | return ( 192 | content.substring(0, highlightStart) + 193 | '' + 194 | content.substring(highlightStart, highlightEnd) + 195 | '' + 196 | content.substring(highlightEnd) 197 | ); 198 | }; 199 | -------------------------------------------------------------------------------- /packages/search-ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react", 4 | "strict": true, 5 | "noImplicitAny": true, 6 | "allowSyntheticDefaultImports": true, 7 | "outDir": "dist", 8 | "target": "ES2021", 9 | "moduleResolution": "node", 10 | "esModuleInterop": true, 11 | "declaration": true 12 | }, 13 | "include": ["./src/**/*.tsx", "./src/**/*.ts", "./index.ts"], 14 | "exclude": ["node_modules"] 15 | } 16 | -------------------------------------------------------------------------------- /packages/styleguide-base/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Expo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /packages/styleguide-base/README.md: -------------------------------------------------------------------------------- 1 | # @expo/styleguide-base 2 | 3 | Expo's base colors and style values. 4 | 5 | ## Get started 6 | 7 | 1. Install dependencies with `yarn`. 8 | 2. Build everything with `yarn build`. 9 | 3. Develop with `yarn dev`. -------------------------------------------------------------------------------- /packages/styleguide-base/index.ts: -------------------------------------------------------------------------------- 1 | export * from './src/breakpoints'; 2 | export * from './src/palette'; 3 | export * from './src/sizing'; 4 | export * from './src/spacing'; 5 | export * from './src/themes'; 6 | -------------------------------------------------------------------------------- /packages/styleguide-base/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@expo/styleguide-base", 3 | "version": "2.0.3", 4 | "description": "Expo's base colors and style values.", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "files": [ 8 | "dist" 9 | ], 10 | "scripts": { 11 | "clean": "rimraf dist", 12 | "bundle": "rollup --config", 13 | "build": "run-s clean bundle", 14 | "dev": "rollup --config --watch" 15 | }, 16 | "author": "Expo", 17 | "license": "MIT", 18 | "homepage": "https://github.com/expo/styleguide", 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/expo/styleguide.git", 22 | "directory": "packages/styleguide" 23 | }, 24 | "bugs": { 25 | "url": "https://github.com/expo/styleguide/issues" 26 | }, 27 | "dependencies": { 28 | "@radix-ui/colors": "^3.0.0" 29 | }, 30 | "devDependencies": { 31 | "npm-run-all": "*", 32 | "rimraf": "*", 33 | "rollup": "*" 34 | }, 35 | "peerDependencies": { 36 | "react": ">= 16" 37 | }, 38 | "eslintConfig": { 39 | "extends": "universe/node" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/styleguide-base/rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import terser from '@rollup/plugin-terser'; 2 | import typescript from '@rollup/plugin-typescript'; 3 | 4 | const config = [ 5 | { 6 | input: 'index.ts', 7 | output: { 8 | dir: 'dist', 9 | format: 'cjs', 10 | }, 11 | plugins: [ 12 | typescript(), 13 | terser() 14 | ], 15 | external: ['@radix-ui/colors'], 16 | }, 17 | ]; 18 | 19 | export default config; 20 | -------------------------------------------------------------------------------- /packages/styleguide-base/src/breakpoints.ts: -------------------------------------------------------------------------------- 1 | export const breakpoints = { 2 | small: 400, 3 | medium: 900, 4 | large: 1200, 5 | }; 6 | -------------------------------------------------------------------------------- /packages/styleguide-base/src/palette.ts: -------------------------------------------------------------------------------- 1 | import { 2 | blue, 3 | blueDark, 4 | red, 5 | redDark, 6 | amber, 7 | amberDark, 8 | green, 9 | greenDark, 10 | orange, 11 | orangeDark, 12 | purple, 13 | purpleDark, 14 | pink, 15 | pinkDark, 16 | slate, 17 | slateDark, 18 | } from '@radix-ui/colors'; 19 | 20 | export const palette = { 21 | white: 'hsl(0, 0%, 100%)', 22 | black: 'hsl(0, 0%, 0%)', 23 | light: { 24 | ...blue, 25 | ...red, 26 | ...green, 27 | ...orange, 28 | ...purple, 29 | ...pink, 30 | yellow1: amber.amber1, 31 | yellow2: amber.amber2, 32 | yellow3: amber.amber3, 33 | yellow4: amber.amber4, 34 | yellow5: amber.amber5, 35 | yellow6: amber.amber6, 36 | yellow7: amber.amber7, 37 | yellow8: amber.amber8, 38 | yellow9: amber.amber9, 39 | yellow10: amber.amber10, 40 | yellow11: amber.amber11, 41 | yellow12: amber.amber12, 42 | gray1: slate.slate1, 43 | gray2: slate.slate2, 44 | gray3: slate.slate3, 45 | gray4: slate.slate4, 46 | gray5: slate.slate5, 47 | gray6: slate.slate6, 48 | gray7: slate.slate7, 49 | gray8: slate.slate8, 50 | gray9: slate.slate9, 51 | gray10: slate.slate10, 52 | gray11: slate.slate11, 53 | gray12: slate.slate12, 54 | }, 55 | dark: { 56 | ...blueDark, 57 | ...redDark, 58 | ...greenDark, 59 | ...orangeDark, 60 | ...purpleDark, 61 | ...pinkDark, 62 | yellow1: amberDark.amber1, 63 | yellow2: amberDark.amber2, 64 | yellow3: amberDark.amber3, 65 | yellow4: amberDark.amber4, 66 | yellow5: amberDark.amber5, 67 | yellow6: amberDark.amber6, 68 | yellow7: amberDark.amber7, 69 | yellow8: amberDark.amber8, 70 | yellow9: amberDark.amber9, 71 | yellow10: amberDark.amber10, 72 | yellow11: amberDark.amber11, 73 | yellow12: amberDark.amber12, 74 | gray1: slateDark.slate1, 75 | gray2: slateDark.slate2, 76 | gray3: slateDark.slate3, 77 | gray4: slateDark.slate4, 78 | gray5: slateDark.slate5, 79 | gray6: slateDark.slate6, 80 | gray7: slateDark.slate7, 81 | gray8: slateDark.slate8, 82 | gray9: slateDark.slate9, 83 | gray10: slateDark.slate10, 84 | gray11: slateDark.slate11, 85 | gray12: slateDark.slate12, 86 | }, 87 | }; 88 | -------------------------------------------------------------------------------- /packages/styleguide-base/src/sizing.ts: -------------------------------------------------------------------------------- 1 | export const borderRadius = { 2 | none: 0, 3 | xs: 2, 4 | sm: 4, 5 | md: 6, 6 | lg: 10, 7 | xl: 16, 8 | '2xl': 20, 9 | '3xl': 24, 10 | full: 9999, 11 | }; 12 | 13 | export const iconSize = { 14 | xs: 16, 15 | sm: 20, 16 | md: 24, 17 | lg: 28, 18 | xl: 32, 19 | }; 20 | -------------------------------------------------------------------------------- /packages/styleguide-base/src/spacing.ts: -------------------------------------------------------------------------------- 1 | const baseSize = 16; 2 | 3 | export const spacing = { 4 | 0: 1, 5 | '0.5': baseSize * 0.125, 6 | 1: baseSize * 0.25, 7 | '1.5': baseSize * 0.375, 8 | 2: baseSize * 0.5, 9 | '2.5': baseSize * 0.625, 10 | 3: baseSize * 0.75, 11 | '3.5': baseSize * 0.875, 12 | 4: baseSize * 1, 13 | 5: baseSize * 1.25, 14 | 6: baseSize * 1.5, 15 | 7: baseSize * 1.75, 16 | 8: baseSize * 2, 17 | 9: baseSize * 2.25, 18 | 10: baseSize * 2.5, 19 | 11: baseSize * 2.75, 20 | 12: baseSize * 3, 21 | 14: baseSize * 3.5, 22 | 16: baseSize * 4, 23 | 20: baseSize * 5, 24 | 24: baseSize * 6, 25 | 28: baseSize * 7, 26 | 32: baseSize * 8, 27 | 36: baseSize * 9, 28 | 40: baseSize * 10, 29 | 44: baseSize * 11, 30 | 48: baseSize * 12, 31 | 52: baseSize * 13, 32 | 56: baseSize * 14, 33 | 60: baseSize * 15, 34 | 64: baseSize * 16, 35 | 72: baseSize * 18, 36 | 80: baseSize * 20, 37 | 96: baseSize * 24, 38 | }; 39 | -------------------------------------------------------------------------------- /packages/styleguide-base/src/themes.ts: -------------------------------------------------------------------------------- 1 | import { palette } from './palette'; 2 | 3 | export const lightTheme = { 4 | background: { 5 | default: palette.white, 6 | screen: palette.light.gray1, 7 | subtle: palette.light.gray2, 8 | element: palette.light.gray3, 9 | hover: palette.light.gray4, 10 | selected: palette.light.gray5, 11 | overlay: palette.white, 12 | success: palette.light.green3, 13 | warning: palette.light.yellow3, 14 | danger: palette.light.red3, 15 | info: palette.light.blue3, 16 | }, 17 | icon: { 18 | default: palette.light.gray11, 19 | secondary: palette.light.gray10, 20 | tertiary: palette.light.gray9, 21 | quaternary: palette.light.gray8, 22 | success: palette.light.green10, 23 | warning: palette.light.yellow11, 24 | danger: palette.light.red10, 25 | info: palette.light.blue10, 26 | }, 27 | text: { 28 | default: palette.light.gray12, 29 | secondary: palette.light.gray11, 30 | tertiary: palette.light.gray10, 31 | quaternary: palette.light.gray9, 32 | link: palette.light.blue11, 33 | success: palette.light.green11, 34 | warning: palette.light.yellow11, 35 | danger: palette.light.red11, 36 | info: palette.light.blue11, 37 | }, 38 | border: { 39 | default: palette.light.gray7, 40 | secondary: palette.light.gray6, 41 | success: palette.light.green7, 42 | warning: palette.light.yellow7, 43 | danger: palette.light.red7, 44 | info: palette.light.blue7, 45 | }, 46 | button: { 47 | primary: { 48 | background: palette.light.blue10, 49 | border: palette.light.blue10, 50 | hover: palette.light.blue11, 51 | icon: palette.light.blue3, 52 | text: palette.white, 53 | disabled: { 54 | background: palette.light.blue7, 55 | border: palette.light.blue7, 56 | text: palette.white, 57 | }, 58 | destructive: { 59 | background: palette.light.red10, 60 | border: palette.light.red10, 61 | hover: palette.light.red11, 62 | icon: palette.light.red3, 63 | text: palette.white, 64 | disabled: { 65 | background: palette.light.red7, 66 | border: palette.light.red7, 67 | text: palette.white, 68 | }, 69 | }, 70 | }, 71 | secondary: { 72 | background: palette.white, 73 | border: palette.light.gray8, 74 | hover: palette.light.gray3, 75 | icon: palette.light.gray11, 76 | text: palette.light.gray12, 77 | disabled: { 78 | background: palette.white, 79 | border: palette.light.gray6, 80 | text: palette.light.gray9, 81 | }, 82 | destructive: { 83 | background: palette.white, 84 | border: palette.light.red7, 85 | hover: palette.light.red3, 86 | icon: palette.light.red9, 87 | text: palette.light.red11, 88 | disabled: { 89 | background: palette.white, 90 | border: palette.light.red5, 91 | text: palette.light.red8, 92 | }, 93 | }, 94 | }, 95 | tertiary: { 96 | background: 'transparent', 97 | border: 'transparent', 98 | hover: palette.light.blue4, 99 | icon: palette.light.blue9, 100 | text: palette.light.blue10, 101 | disabled: { 102 | background: 'transparent', 103 | border: 'transparent', 104 | text: palette.light.blue8, 105 | }, 106 | }, 107 | quaternary: { 108 | background: 'transparent', 109 | border: 'transparent', 110 | hover: palette.light.gray4, 111 | icon: palette.light.gray11, 112 | text: palette.light.gray12, 113 | disabled: { 114 | background: 'transparent', 115 | border: 'transparent', 116 | text: palette.light.gray9, 117 | }, 118 | }, 119 | }, 120 | }; 121 | 122 | export const darkTheme = { 123 | background: { 124 | default: palette.dark.gray1, 125 | screen: '#0C0D0E', 126 | subtle: palette.dark.gray2, 127 | element: palette.dark.gray3, 128 | hover: palette.dark.gray4, 129 | selected: palette.dark.gray5, 130 | overlay: palette.dark.gray2, 131 | success: palette.dark.green3, 132 | warning: palette.dark.yellow3, 133 | danger: palette.dark.red3, 134 | info: palette.dark.blue3, 135 | }, 136 | icon: { 137 | default: palette.dark.gray11, 138 | secondary: palette.dark.gray10, 139 | tertiary: palette.dark.gray9, 140 | quaternary: palette.dark.gray8, 141 | success: palette.dark.green10, 142 | warning: palette.dark.yellow11, 143 | danger: palette.dark.red10, 144 | info: palette.dark.blue10, 145 | }, 146 | text: { 147 | default: palette.dark.gray12, 148 | secondary: palette.dark.gray11, 149 | tertiary: palette.dark.gray10, 150 | quaternary: palette.dark.gray9, 151 | link: palette.dark.blue11, 152 | success: palette.dark.green11, 153 | warning: palette.dark.yellow11, 154 | danger: palette.dark.red11, 155 | info: palette.dark.blue11, 156 | }, 157 | border: { 158 | default: palette.dark.gray7, 159 | secondary: palette.dark.gray6, 160 | success: palette.dark.green7, 161 | warning: palette.dark.yellow7, 162 | danger: palette.dark.red7, 163 | info: palette.dark.blue7, 164 | }, 165 | button: { 166 | primary: { 167 | background: palette.dark.blue8, 168 | border: palette.dark.blue8, 169 | hover: palette.dark.blue7, 170 | icon: palette.dark.blue12, 171 | text: palette.white, 172 | disabled: { 173 | background: palette.dark.blue7, 174 | border: palette.dark.blue7, 175 | text: palette.dark.gray11, 176 | }, 177 | destructive: { 178 | background: palette.dark.red8, 179 | border: palette.dark.red8, 180 | hover: palette.dark.red7, 181 | icon: palette.dark.red12, 182 | text: palette.white, 183 | disabled: { 184 | background: palette.dark.red6, 185 | border: palette.dark.red6, 186 | text: palette.dark.red11, 187 | }, 188 | }, 189 | }, 190 | secondary: { 191 | background: palette.dark.gray3, 192 | border: palette.dark.gray8, 193 | hover: palette.dark.gray4, 194 | icon: palette.dark.gray12, 195 | text: palette.white, 196 | disabled: { 197 | background: palette.dark.gray1, 198 | border: palette.dark.gray7, 199 | text: palette.dark.gray11, 200 | }, 201 | destructive: { 202 | background: palette.dark.red3, 203 | border: palette.dark.red7, 204 | hover: palette.dark.red2, 205 | icon: palette.dark.red9, 206 | text: palette.white, 207 | disabled: { 208 | background: palette.dark.red2, 209 | border: palette.dark.red6, 210 | text: palette.dark.red10, 211 | }, 212 | }, 213 | }, 214 | tertiary: { 215 | background: 'transparent', 216 | border: 'transparent', 217 | hover: palette.dark.blue4, 218 | icon: palette.dark.blue10, 219 | text: palette.dark.blue11, 220 | disabled: { 221 | background: 'transparent', 222 | border: 'transparent', 223 | text: palette.dark.blue8, 224 | }, 225 | }, 226 | quaternary: { 227 | background: 'transparent', 228 | border: 'transparent', 229 | hover: palette.dark.gray4, 230 | icon: palette.dark.gray10, 231 | text: palette.dark.gray11, 232 | disabled: { 233 | background: 'transparent', 234 | border: 'transparent', 235 | text: palette.dark.gray9, 236 | }, 237 | }, 238 | }, 239 | }; 240 | -------------------------------------------------------------------------------- /packages/styleguide-base/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "noImplicitAny": true, 5 | "allowSyntheticDefaultImports": true, 6 | "declaration": true, 7 | "outDir": "dist" 8 | }, 9 | "include": ["./src/**/*.ts", "./index.ts"], 10 | "exclude": ["node_modules"] 11 | } 12 | -------------------------------------------------------------------------------- /packages/styleguide-icons/.env.example: -------------------------------------------------------------------------------- 1 | FIGMA_TOKEN=[your personal access token] 2 | FILE_ID=zHeGLN45wIgf39kqdvKpoj -------------------------------------------------------------------------------- /packages/styleguide-icons/.gitignore: -------------------------------------------------------------------------------- 1 | src/custom 2 | src/duotone 3 | src/outline 4 | src/solid 5 | 6 | custom 7 | duotone 8 | outline 9 | solid 10 | 11 | /index.js 12 | /mergeClasses.js 13 | 14 | *.d.ts 15 | -------------------------------------------------------------------------------- /packages/styleguide-icons/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2023 Expo 2 | 3 | Unauthorized use of this software via any medium is strictly prohibited. Any 4 | person obtaining a copy of this software and associated documentation files 5 | (the "Software"), are restricted from using the software in any way other 6 | than for the use in Expo websites, products, and apps. It is not permitted 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software without the authorization of Expo. 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /packages/styleguide-icons/README.md: -------------------------------------------------------------------------------- 1 | # @expo/styleguide-icons 2 | 3 | Expo's icons for use on the web. 4 | 5 | ## Get started 6 | 7 | 1. Install dependencies with `yarn`. 8 | 2. Set up a .env file. To do this, you'll need to: 9 | a. Duplicate the **.env.example** file, and name the copy: **.env**. 10 | b. Inside it, define a `FIGMA_TOKEN` with a personal access token from Figma. Click on your avatar in Figma in the top right > Settings > Account tab. The personal access token settings are near the bottom. 11 | 3. Build everything with `yarn build`. 12 | 13 | ### Icon generation 14 | 15 | We generate all icon files based on our Figma icons. The process is: 16 | 17 | 1. Make a call to Figma to get all the icons from a specific file. 18 | 2. Once we get every component from the icons pages specified in **figma.config.js**, we optimize them all with SVGO. 19 | 3. After that, we use SVGR to create React components of each icon. The outputter is defined in **figma.config.js**, and we use a custom template in **svgr-icon-template.js**. These components are stored in **tmp**. 20 | 4. Finally, we use `rollup` to build our final package. These files are saved in **dist**. 21 | 22 | -------------------------------------------------------------------------------- /packages/styleguide-icons/check-env.sh: -------------------------------------------------------------------------------- 1 | if [ -z "${FIGMA_TOKEN}" ] && [ ! -f ./.env ]; then 2 | echo "⚠️ You do not have packages/styleguide-icons/.env file or correct environment variables set. 3 | 4 | Please create it from packages/styleguide-icons/.env.example and fill it with your credentials. 5 | 6 | Instead, bundling a stub for @expo/styleguide-icons that is empty. 7 | Read the README.md in packages/styleguide-icons to build the icon set. 8 | " 9 | yarn build-stub && exit 0 10 | else 11 | yarn build-icons && exit 0 12 | fi 13 | -------------------------------------------------------------------------------- /packages/styleguide-icons/figma.config.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | 3 | const svgo = require('@figma-export/transform-svg-with-svgo'); 4 | const figmaUtils = require('@figma-export/utils'); 5 | 6 | const template = require('./svgr-icon-template'); 7 | const pascalCase = figmaUtils.pascalCase; 8 | const fileId = process.env.FILE_ID; 9 | 10 | function getComponentName({ componentName, pageName }) { 11 | if (pageName === 'outline' || pageName === 'custom') { 12 | return pascalCase(componentName) + 'Icon'; 13 | } 14 | 15 | return pascalCase(componentName) + pascalCase(pageName) + 'Icon'; 16 | } 17 | 18 | const outputters = [ 19 | require('@figma-export/output-components-as-svgr')({ 20 | getFileExtension: () => '.tsx', 21 | getComponentName, 22 | getSvgrConfig: ({ componentName, pageName }) => ({ 23 | typescript: true, 24 | svgProps: { 25 | className: '{_className}', 26 | role: 'img', 27 | }, 28 | replaceAttrValues: { 29 | black: 'currentColor', 30 | [componentName]: `${componentName}-${pageName}-icon`, 31 | }, 32 | template, 33 | }), 34 | // Exports the component as a named export without the '.tsx' extension inside the generated index.ts file. By default the '.tsx' extension is added, which makes TypeScript complain. 35 | getExportTemplate: ({ componentName, pageName }) => 36 | `export { ${getComponentName({ 37 | componentName, 38 | pageName, 39 | })} } from './${getComponentName({ componentName, pageName })}';`, 40 | output: './src', 41 | }), 42 | ]; 43 | 44 | const commonSVGOConfig = [ 45 | { 46 | name: 'removeHiddenElems', 47 | active: true, 48 | }, 49 | { 50 | name: 'removeXMLNS', 51 | active: true, 52 | }, 53 | { 54 | name: 'removeUselessStrokeAndFill', 55 | active: true, 56 | }, 57 | { 58 | name: 'removeUselessDefs', 59 | active: true, 60 | }, 61 | { 62 | name: 'collapseGroups', 63 | active: true, 64 | }, 65 | { 66 | name: 'removeEmptyContainers', 67 | active: true, 68 | }, 69 | { 70 | name: 'removeDimensions', 71 | active: true, 72 | }, 73 | { 74 | name: 'sortAttrs', 75 | active: true, 76 | }, 77 | ]; 78 | 79 | /** @type {import('svgo').PluginConfig[]} */ 80 | const solidSVGOConfig = [ 81 | ...commonSVGOConfig, 82 | { 83 | name: 'removeAttrs', 84 | params: { 85 | attrs: 'fill', 86 | }, 87 | }, 88 | { 89 | name: 'addAttributesToSVGElement', 90 | params: { 91 | attribute: { 92 | fill: 'currentColor', 93 | }, 94 | }, 95 | }, 96 | ]; 97 | 98 | /** @type {import('svgo').PluginConfig[]} */ 99 | const customSVGOConfig = [...commonSVGOConfig]; 100 | 101 | /** @type {import('svgo').PluginConfig[]} */ 102 | const outlineSVGOConfig = [ 103 | ...commonSVGOConfig, 104 | { 105 | name: 'removeAttrs', 106 | params: { 107 | attrs: 'stroke', 108 | }, 109 | }, 110 | { 111 | name: 'addAttributesToSVGElement', 112 | params: { 113 | attribute: { 114 | stroke: 'currentColor', 115 | }, 116 | }, 117 | }, 118 | ]; 119 | 120 | /** @type {import('svgo').PluginConfig[]} */ 121 | const duotoneSVGOConfig = [ 122 | ...commonSVGOConfig, 123 | { 124 | name: 'removeAttrs', 125 | params: { 126 | attrs: 'stroke', 127 | }, 128 | }, 129 | { 130 | name: 'addAttributesToSVGElement', 131 | params: { 132 | attribute: { 133 | stroke: 'currentColor', 134 | }, 135 | }, 136 | }, 137 | ]; 138 | 139 | /** @type {import('@figma-export/types').FigmaExportRC} */ 140 | module.exports = { 141 | commands: [ 142 | [ 143 | 'components', 144 | { 145 | fileId, 146 | onlyFromPages: ['outline'], 147 | transformers: [svgo({ multipass: true, plugins: outlineSVGOConfig })], 148 | outputters, 149 | }, 150 | ], 151 | [ 152 | 'components', 153 | { 154 | fileId, 155 | onlyFromPages: ['duotone'], 156 | transformers: [svgo({ multipass: true, plugins: duotoneSVGOConfig })], 157 | outputters, 158 | }, 159 | ], 160 | [ 161 | 'components', 162 | { 163 | fileId, 164 | onlyFromPages: ['solid'], 165 | transformers: [svgo({ multipass: true, plugins: solidSVGOConfig })], 166 | outputters, 167 | }, 168 | ], 169 | [ 170 | 'components', 171 | { 172 | fileId, 173 | onlyFromPages: ['custom'], 174 | transformers: [svgo({ multipass: true, plugins: customSVGOConfig })], 175 | outputters, 176 | }, 177 | ], 178 | ], 179 | }; 180 | -------------------------------------------------------------------------------- /packages/styleguide-icons/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@expo/styleguide-icons", 3 | "version": "2.2.2", 4 | "description": "Expo's icons for use on the web.", 5 | "main": "index", 6 | "types": "index.d.ts", 7 | "sideEffects": false, 8 | "files": [ 9 | "custom", 10 | "duotone", 11 | "outline", 12 | "solid", 13 | "index.d.ts", 14 | "index.js", 15 | "mergeClasses.d.ts", 16 | "mergeClasses.js" 17 | ], 18 | "scripts": { 19 | "check-env": "sh ./check-env.sh", 20 | "clean": "rimraf custom duotone outline solid 'index.*' 'mergeClasses.*'", 21 | "export": "figma-export use-config figma.config.js", 22 | "bundle": "rollup --config && node ./postbundle.js", 23 | "build": "run-s check-env", 24 | "build-stub": "export STUB=true && run-s clean bundle", 25 | "build-icons": "run-s clean export bundle" 26 | }, 27 | "author": "Expo", 28 | "license": "UNLICENSED", 29 | "homepage": "https://github.com/expo/styleguide", 30 | "repository": { 31 | "type": "git", 32 | "url": "https://github.com/expo/styleguide.git", 33 | "directory": "packages/styleguide" 34 | }, 35 | "bugs": { 36 | "url": "https://github.com/expo/styleguide/issues" 37 | }, 38 | "dependencies": { 39 | "tailwind-merge": "^2.5.4" 40 | }, 41 | "devDependencies": { 42 | "@figma-export/cli": "^4.8.0", 43 | "@figma-export/output-components-as-svgr": "^4.8.0", 44 | "@figma-export/transform-svg-with-svgo": "^4.8.0", 45 | "dotenv": "^16.3.1", 46 | "npm-run-all": "*", 47 | "rimraf": "*", 48 | "rollup": "*", 49 | "tslib": "^2.8.0" 50 | }, 51 | "peerDependencies": { 52 | "react": ">= 16" 53 | }, 54 | "eslintConfig": { 55 | "extends": [ 56 | "universe/web", 57 | "universe/node" 58 | ], 59 | "ignorePatterns": [ 60 | "custom", 61 | "duotone", 62 | "outline", 63 | "solid" 64 | ] 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /packages/styleguide-icons/postbundle.js: -------------------------------------------------------------------------------- 1 | const fs = require('node:fs'); 2 | const path = require('node:path'); 3 | 4 | // Flatten package structure. 5 | fs.cpSync(path.resolve(__dirname, 'dist'), path.resolve(__dirname), { recursive: true }); 6 | fs.rmSync('./dist', { recursive: true }); 7 | 8 | console.log(''); 9 | 10 | // Replace all index files content. 11 | fs.readdirSync(path.resolve(__dirname), { withFileTypes: true }) 12 | .reduce((acc, dirent) => { 13 | const directoryPath = path.resolve(__dirname, dirent.name); 14 | if (dirent.isDirectory()) { 15 | const files = fs.readdirSync(directoryPath).map((fileName) => path.join(directoryPath, fileName)); 16 | return acc.concat(files); 17 | } 18 | return acc.concat(directoryPath); 19 | }, []) 20 | .filter((file) => ['index.js', 'index.d.ts'].includes(path.basename(file))) 21 | .forEach((indexFile) => { 22 | // Overwrite the index file with an empty file. 23 | fs.writeFileSync(indexFile, ''); 24 | console.log(`🧹 Cleared: \x1b[36m${indexFile.replace(__dirname, '')}\x1b[0m`); 25 | }); 26 | 27 | console.log(''); 28 | -------------------------------------------------------------------------------- /packages/styleguide-icons/rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import terser from '@rollup/plugin-terser'; 2 | import typescript from '@rollup/plugin-typescript'; 3 | import copy from 'rollup-plugin-copy'; 4 | 5 | const baseConfig = { 6 | input: 'src/index.ts', 7 | output: { 8 | name: 'index', 9 | dir: 'dist', 10 | format: 'cjs', 11 | generatedCode: 'es2015', 12 | exports: 'named', 13 | preserveModules: true, 14 | preserveModulesRoot: 'src', 15 | }, 16 | treeshake: 'smallest', 17 | plugins: [ 18 | typescript(), 19 | terser(), 20 | ], 21 | external: ['react', 'tailwind-merge'], 22 | }; 23 | 24 | function getConfig() { 25 | if (process.env.STUB) { 26 | return { 27 | ...baseConfig, 28 | input: 'src/index-stub.js', 29 | plugins: [ 30 | copy({ 31 | targets: [ 32 | { src: './stub.d.ts', dest: './', rename: 'index.d.ts' } 33 | ], 34 | }), 35 | ], 36 | }; 37 | } 38 | return baseConfig; 39 | } 40 | 41 | const config = getConfig(); 42 | 43 | export default config; 44 | -------------------------------------------------------------------------------- /packages/styleguide-icons/src/index-stub.js: -------------------------------------------------------------------------------- 1 | // The purpose of this file is to provide a module that can be imported by the example website without breaking when there is no .env file present. 2 | 3 | const stub = (args) => null; 4 | 5 | export { stub }; 6 | -------------------------------------------------------------------------------- /packages/styleguide-icons/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './solid'; 2 | export * from './outline'; 3 | export * from './duotone'; 4 | export * from './custom'; 5 | -------------------------------------------------------------------------------- /packages/styleguide-icons/src/mergeClasses.ts: -------------------------------------------------------------------------------- 1 | import { extendTailwindMerge } from 'tailwind-merge'; 2 | 3 | type AdditionalClassGroupIds = 'icon'; 4 | 5 | export const mergeClasses = extendTailwindMerge({ 6 | extend: { 7 | classGroups: { 8 | icon: [{ icon: ['2xs', 'xs', 'sm', 'md', 'lg', 'xl', '2xl'] }], 9 | }, 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /packages/styleguide-icons/stub.d.ts: -------------------------------------------------------------------------------- 1 | declare function stub(props: any): JSX.Element; 2 | export default stub; 3 | -------------------------------------------------------------------------------- /packages/styleguide-icons/svgr-icon-template.js: -------------------------------------------------------------------------------- 1 | const svgrTemplate = ({ imports, interfaces, componentName, props, jsx }, { tpl, options }) => { 2 | return tpl`${imports} 3 | 4 | import { mergeClasses } from "../mergeClasses"; 5 | 6 | export function ${componentName}({ className, ...props }: React.SVGProps & React.HTMLAttributes) { 7 | const _className = mergeClasses("icon-md text-icon-default translate-z shrink-0", className); 8 | return ${jsx}; 9 | } 10 | 11 | ${componentName}.displayName = "${componentName}";`; 12 | }; 13 | 14 | module.exports = svgrTemplate; 15 | -------------------------------------------------------------------------------- /packages/styleguide-icons/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react", 4 | "strict": true, 5 | "noImplicitAny": true, 6 | "allowSyntheticDefaultImports": true, 7 | "declaration": true, 8 | "outDir": "dist", 9 | "target": "es2018", 10 | "paths": { 11 | "@/*": [ 12 | "./src/*" 13 | ] 14 | } 15 | }, 16 | "include": ["./src/**/*.tsx", "./src/**/*.ts", "./src/*.ts"], 17 | "exclude": ["node_modules"] 18 | } 19 | -------------------------------------------------------------------------------- /packages/styleguide-native/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Expo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /packages/styleguide-native/README.md: -------------------------------------------------------------------------------- 1 | # @expo/styleguide-native 2 | 3 | Expo's React Native styleguide components. 4 | 5 | ## Get started 6 | 7 | 1. Install dependencies with `yarn`. 8 | 2. Build everything with `yarn build`. 9 | 3. Develop with `yarn dev`. 10 | -------------------------------------------------------------------------------- /packages/styleguide-native/index.ts: -------------------------------------------------------------------------------- 1 | export * from './src/logos'; 2 | export type { IconProps } from './src/types'; 3 | export { shadows } from './src/styles/shadows'; 4 | export { typography } from './src/styles/typography'; 5 | -------------------------------------------------------------------------------- /packages/styleguide-native/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@expo/styleguide-native", 3 | "version": "8.0.0", 4 | "description": "Expo's React Native styleguide components.", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "scripts": { 8 | "clean": "rimraf dist", 9 | "bundle": "rollup --config", 10 | "build": "run-s clean bundle", 11 | "dev": "rollup --config --watch" 12 | }, 13 | "files": [ 14 | "dist" 15 | ], 16 | "author": "Expo", 17 | "license": "MIT", 18 | "homepage": "https://github.com/expo/styleguide", 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/expo/styleguide.git", 22 | "directory": "packages/styleguide-native" 23 | }, 24 | "devDependencies": { 25 | "@babel/preset-env": "^7.26.9", 26 | "@babel/preset-react": "^7.26.3", 27 | "@babel/preset-typescript": "^7.26.0", 28 | "@rollup/plugin-babel": "^6.0.4", 29 | "@rollup/plugin-commonjs": "^28.0.3", 30 | "@rollup/plugin-node-resolve": "^16.0.1", 31 | "@svgr/cli": "^6.5.1", 32 | "npm-run-all": "*", 33 | "react-native-svg": "^15.8.0", 34 | "rimraf": "*", 35 | "rollup": "*" 36 | }, 37 | "peerDependencies": { 38 | "react": "*", 39 | "react-native-svg": "*" 40 | }, 41 | "eslintConfig": { 42 | "extends": "universe/native" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/styleguide-native/rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import babel from '@rollup/plugin-babel'; 2 | import resolve from '@rollup/plugin-node-resolve'; 3 | import commonjs from '@rollup/plugin-commonjs'; 4 | import terser from '@rollup/plugin-terser'; 5 | import typescript from '@rollup/plugin-typescript'; 6 | 7 | const config = [ 8 | { 9 | input: 'index.ts', 10 | output: { 11 | file: 'dist/index.js', 12 | format: 'esm', 13 | sourcemap: true, 14 | }, 15 | external: [ 16 | 'react', 17 | 'react-native', 18 | 'react-native-svg', 19 | ], 20 | plugins: [ 21 | typescript(), 22 | resolve({ 23 | extensions: ['.js', '.jsx', '.ts', '.tsx'], 24 | }), 25 | commonjs(), 26 | babel({ 27 | exclude: 'node_modules/**', 28 | babelHelpers: 'runtime', 29 | presets: [ 30 | '@babel/preset-env', 31 | '@babel/preset-react', 32 | '@babel/preset-typescript', 33 | ], 34 | plugins: ['@babel/plugin-transform-runtime'], 35 | }), 36 | terser(), 37 | ], 38 | }, 39 | ]; 40 | 41 | export default config; 42 | -------------------------------------------------------------------------------- /packages/styleguide-native/src/logos/DocsLogo.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Svg, { Path } from 'react-native-svg'; 3 | 4 | import { IconProps } from '../types'; 5 | export default function DocsLogo(props: IconProps) { 6 | const { color } = props; 7 | return ( 8 | 9 | 13 | 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /packages/styleguide-native/src/logos/ExpoGoLogo.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Svg, { Path } from 'react-native-svg'; 3 | 4 | import { IconProps } from '../types'; 5 | export default function ExpoGoLogo(props: IconProps) { 6 | const { color } = props; 7 | return ( 8 | 9 | 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /packages/styleguide-native/src/logos/Logo.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Svg, { Path } from 'react-native-svg'; 3 | 4 | import { IconProps } from '../types'; 5 | export default function Logo(props: IconProps) { 6 | const { color } = props; 7 | return ( 8 | 9 | 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /packages/styleguide-native/src/logos/SnackLogo.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Svg, { Path } from 'react-native-svg'; 3 | 4 | import { IconProps } from '../types'; 5 | export default function SnackLogo(props: IconProps) { 6 | const { color } = props; 7 | return ( 8 | 9 | 15 | 21 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /packages/styleguide-native/src/logos/WordMarkLogo.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Svg, { Path } from 'react-native-svg'; 3 | 4 | import { IconProps } from '../types'; 5 | export default function WordMarkLogo(props: IconProps) { 6 | const { color } = props; 7 | return ( 8 | 9 | 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /packages/styleguide-native/src/logos/index.tsx: -------------------------------------------------------------------------------- 1 | export { default as DocsLogo } from './DocsLogo'; 2 | export { default as ExpoGoLogo } from './ExpoGoLogo'; 3 | export { default as Logo } from './Logo'; 4 | export { default as SnackLogo } from './SnackLogo'; 5 | export { default as WordMarkLogo } from './WordMarkLogo'; 6 | -------------------------------------------------------------------------------- /packages/styleguide-native/src/styles/shadows.ts: -------------------------------------------------------------------------------- 1 | export const shadows = { 2 | xs: { 3 | elevation: 1, 4 | shadowColor: '#000', 5 | shadowRadius: 1, 6 | shadowOffset: { height: 1, width: 0 }, 7 | shadowOpacity: 0.075, 8 | }, 9 | sm: { 10 | elevation: 4, 11 | shadowColor: '#000', 12 | shadowRadius: 3, 13 | shadowOffset: { height: 3, width: 0 }, 14 | shadowOpacity: 0.15, 15 | }, 16 | md: { 17 | elevation: 8, 18 | shadowColor: '#000', 19 | shadowRadius: 8, 20 | shadowOffset: { height: 6, width: 0 }, 21 | shadowOpacity: 0.15, 22 | }, 23 | lg: { 24 | elevation: 16, 25 | shadowColor: '#000', 26 | shadowRadius: 10, 27 | shadowOffset: { height: 10, width: 0 }, 28 | shadowOpacity: 0.17, 29 | }, 30 | xl: { 31 | elevation: 28, 32 | shadowColor: '#000', 33 | shadowRadius: 25, 34 | shadowOffset: { height: 16, width: 0 }, 35 | shadowOpacity: 0.2, 36 | }, 37 | }; 38 | -------------------------------------------------------------------------------- /packages/styleguide-native/src/types/index.ts: -------------------------------------------------------------------------------- 1 | import { SvgProps } from 'react-native-svg'; 2 | 3 | export type IconProps = SvgProps & { 4 | size?: number; 5 | }; 6 | -------------------------------------------------------------------------------- /packages/styleguide-native/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react", 4 | "strict": true, 5 | "noImplicitAny": true, 6 | "allowSyntheticDefaultImports": true, 7 | "declaration": true, 8 | "outDir": "dist" 9 | }, 10 | "include": ["./src/**/*.tsx", "./src/**/*.ts", "./index.ts"], 11 | "exclude": ["node_modules"] 12 | } 13 | -------------------------------------------------------------------------------- /packages/styleguide/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Expo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /packages/styleguide/README.md: -------------------------------------------------------------------------------- 1 | # @expo/styleguide 2 | 3 | Expo's styleguide and components for use on the web. 4 | 5 | ## Usage 6 | 7 | 1. Install Expo Styleguide package: 8 | ```shell 9 | yarn add @expo/styleguide 10 | ``` 11 | 2. Import global CSS files from the package in your JS(X)/TS(X) code: 12 | ```jsx 13 | import "@expo/styleguide/dist/expo-theme.css"; 14 | ``` 15 | or import it the main stylesheet file: 16 | ```css 17 | @import "@expo/styleguide/dist/expo-theme.css"; 18 | ``` 19 | 3. Add `'./node_modules/@expo/styleguide/dist/**/*.{js,ts,jsx,tsx}'` to the Tailwind `content` paths. 20 | 21 | ### Tailwind theme 22 | 23 | For the Styleguide we use our custom Tailwind theme, which is based on the default TW theme, with the following differences: 24 | * only valid media screen scopes are: `xs:`, `sm:`, `md:`, `lg:` and `xl:` 25 | * there is a custom `hocus:` scope which is a shorthand for hover and focus states 26 | * typography elements are predefined as a `heading-[size]` styles sets 27 | * `icon-[size]` are custom component classes defined for icons sizing 28 | 29 | The theme can be extended, if needed, and includes `@tailwindcss/typography` plugin by default, with a stripped down version of default config. 30 | 31 | ## Development 32 | 33 | ### Get started 34 | 35 | 1. Install dependencies with `yarn`. 36 | 2. Build everything with `yarn build`. 37 | 3. Develop with `yarn dev`. 38 | 39 | ### Changing Tailwind theme 40 | 41 | In order to see changes made to the exported **tailwind.js** config: 42 | 43 | - Change a value in **packages/styleguide/tailwind.js** 44 | - Run `yarn build` in **packages/styleguide** 45 | - Navigate to **example-web** and restart the dev server -------------------------------------------------------------------------------- /packages/styleguide/index.ts: -------------------------------------------------------------------------------- 1 | export * from './src/components'; 2 | export * from './src/logos'; 3 | export { shadows } from './src/styles/shadows'; 4 | export { appBackgroundColors } from './src/styles/colors'; 5 | export { theme } from './src/styles/themes'; 6 | export { typography } from './src/styles/typography'; 7 | export { mergeClasses } from './src/helpers/mergeClasses'; 8 | -------------------------------------------------------------------------------- /packages/styleguide/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@expo/styleguide", 3 | "version": "9.1.2", 4 | "description": "Expo's styleguide components for use on the web.", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "files": [ 8 | "dist", 9 | "tailwind.js" 10 | ], 11 | "scripts": { 12 | "clean": "rimraf dist", 13 | "bundle": "rollup --config", 14 | "build": "run-s clean bundle", 15 | "dev": "rollup --config --watch" 16 | }, 17 | "homepage": "https://github.com/expo/styleguide", 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/expo/styleguide.git", 21 | "directory": "packages/styleguide" 22 | }, 23 | "keywords": [ 24 | "expo" 25 | ], 26 | "author": "Expo", 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/expo/styleguide/issues" 30 | }, 31 | "dependencies": { 32 | "@expo/styleguide-base": "^2.0.3", 33 | "tailwind-merge": "^2.5.4" 34 | }, 35 | "devDependencies": { 36 | "@tailwindcss/typography": "^0.5.15", 37 | "npm-run-all": "*", 38 | "rimraf": "*", 39 | "rollup": "*", 40 | "tailwindcss": "^3.4.17" 41 | }, 42 | "peerDependencies": { 43 | "next": ">= 13", 44 | "react": ">= 16" 45 | }, 46 | "eslintConfig": { 47 | "extends": "universe/web" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/styleguide/rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import terser from '@rollup/plugin-terser'; 2 | import typescript from '@rollup/plugin-typescript'; 3 | import copy from 'rollup-plugin-copy'; 4 | 5 | const config = [ 6 | { 7 | input: 'index.ts', 8 | output: { 9 | dir: 'dist', 10 | format: 'cjs', 11 | }, 12 | plugins: [ 13 | typescript(), 14 | terser({ 15 | keep_fnames: /.+ColorMode/ 16 | }), 17 | copy({ 18 | targets: [ 19 | { src: './src/styles/expo-theme.css', dest: 'dist' }, 20 | ], 21 | }), 22 | ], 23 | external: [ 24 | '@expo/styleguide-base', 25 | 'next/link', 26 | 'react', 27 | 'tailwind-merge' 28 | ], 29 | }, 30 | ]; 31 | 32 | export default config; 33 | -------------------------------------------------------------------------------- /packages/styleguide/src/components/Button/ButtonBase.tsx: -------------------------------------------------------------------------------- 1 | import React, { forwardRef } from 'react'; 2 | import type { ButtonHTMLAttributes } from 'react'; 3 | 4 | import { mergeClasses } from '../../helpers/mergeClasses'; 5 | 6 | export type ButtonBaseProps = ButtonHTMLAttributes & { 7 | testID?: string; 8 | }; 9 | 10 | export const ButtonBase = forwardRef( 11 | ({ children, testID, className, onClick, type = 'button', disabled = false, ...rest }, ref) => { 12 | return ( 13 | 23 | ); 24 | } 25 | ); 26 | 27 | ButtonBase.displayName = 'ButtonBase'; 28 | -------------------------------------------------------------------------------- /packages/styleguide/src/components/Button/helpers.ts: -------------------------------------------------------------------------------- 1 | const STOPWORDS = 'a an and at but by for in nor of on or out so the to up with yet'.split(' '); 2 | 3 | type Options = { 4 | keepSpaces?: boolean | undefined; 5 | stopwords?: string[] | undefined; 6 | }; 7 | 8 | export function titleCase(value: string, options: Options = {}) { 9 | if (!value) return ''; 10 | 11 | const stop = options.stopwords ?? STOPWORDS; 12 | const keep = options.keepSpaces; 13 | const splitter = /(\s+|[-‑–—,:;!?()])/; 14 | 15 | return value 16 | .split(splitter) 17 | .map((word, index, all) => { 18 | if (index % 2) { 19 | if (/\s+/.test(word)) return keep ? word : ' '; 20 | return word; 21 | } 22 | 23 | const lower = word.toLocaleLowerCase(); 24 | 25 | if (index !== 0 && index !== all.length - 1 && stop.includes(lower)) { 26 | return lower; 27 | } 28 | 29 | return capitalize(word); 30 | }) 31 | .join(''); 32 | } 33 | 34 | function capitalize(text: string) { 35 | return text.charAt(0).toUpperCase() + text.slice(1); 36 | } 37 | -------------------------------------------------------------------------------- /packages/styleguide/src/components/Button/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ButtonBase'; 2 | export * from './Button'; 3 | -------------------------------------------------------------------------------- /packages/styleguide/src/components/Link/Link.tsx: -------------------------------------------------------------------------------- 1 | import React, { forwardRef } from 'react'; 2 | 3 | import { LinkBase, LinkBaseProps } from './LinkBase'; 4 | import { mergeClasses } from '../../helpers/mergeClasses'; 5 | 6 | export const Link = forwardRef(({ className, disabled, ...rest }, ref) => { 7 | return ( 8 | 13 | ); 14 | }); 15 | -------------------------------------------------------------------------------- /packages/styleguide/src/components/Link/LinkBase.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import React, { forwardRef } from 'react'; 3 | import type { AnchorHTMLAttributes } from 'react'; 4 | 5 | export type LinkBaseProps = AnchorHTMLAttributes & { 6 | testID?: string; 7 | openInNewTab?: boolean; 8 | disabled?: boolean; 9 | skipNextLink?: boolean; 10 | }; 11 | 12 | export const LinkBase = forwardRef( 13 | ({ children, testID, href, openInNewTab, onClick, target, disabled, skipNextLink, rel, ...rest }, ref) => { 14 | if (disabled) { 15 | return ( 16 | 17 | {children} 18 | 19 | ); 20 | } 21 | 22 | if (!href || href.startsWith('#')) { 23 | return ( 24 | 25 | {children} 26 | 27 | ); 28 | } 29 | 30 | const Tag = skipNextLink ? 'a' : Link; 31 | 32 | return ( 33 | 41 | {children} 42 | 43 | ); 44 | } 45 | ); 46 | 47 | LinkBase.displayName = 'LinkBase'; 48 | -------------------------------------------------------------------------------- /packages/styleguide/src/components/Link/index.ts: -------------------------------------------------------------------------------- 1 | export * from './LinkBase'; 2 | export * from './Link'; 3 | -------------------------------------------------------------------------------- /packages/styleguide/src/components/Theme/BlockingSetInitialColorMode.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export function isLocalStorageAvailable(): boolean { 4 | try { 5 | if (!window.localStorage || typeof window.localStorage === 'undefined') { 6 | return false; 7 | } 8 | window.localStorage.setItem('localStorage:test', 'value'); 9 | if (window.localStorage.getItem('localStorage:test') !== 'value') { 10 | return false; 11 | } 12 | window.localStorage.removeItem('localStorage:test'); 13 | return true; 14 | } catch { 15 | return false; 16 | } 17 | } 18 | 19 | export function getInitialColorMode(): string | null { 20 | if (isLocalStorageAvailable()) { 21 | const preference = window.localStorage.getItem('data-expo-theme'); 22 | const hasPreference = typeof preference === 'string'; 23 | 24 | if (hasPreference) { 25 | return preference; 26 | } 27 | } 28 | 29 | const mql = window.matchMedia('(prefers-color-scheme: dark)'); 30 | 31 | const systemPreference = typeof mql.matches === 'boolean'; 32 | if (systemPreference) { 33 | return mql.matches ? 'dark' : 'light'; 34 | } 35 | 36 | return 'light'; 37 | } 38 | 39 | function setInitialColorMode() { 40 | const colorMode = getInitialColorMode(); 41 | 42 | // add HTML attribute if dark mode 43 | if (colorMode === 'dark') { 44 | document.documentElement.classList.add('dark-theme'); 45 | } else { 46 | document.documentElement.classList.remove('dark-theme'); 47 | } 48 | } 49 | 50 | // our function needs to be a string so that we can call it 51 | const blockingSetInitialColorMode = `(function() { 52 | ${isLocalStorageAvailable.toString()} 53 | ${getInitialColorMode.toString()} 54 | ${setInitialColorMode.toString()} 55 | setInitialColorMode(); 56 | })() 57 | `; 58 | 59 | export function BlockingSetInitialColorMode() { 60 | return ( 61 |