template_0205

This commit is contained in:
Sofio
2026-02-05 13:16:05 +08:00
commit d93e4d9c9f
197 changed files with 52810 additions and 0 deletions

View File

@@ -0,0 +1,320 @@
import { FC, ReactElement, useCallback, useEffect, useRef, useState } from "react"
import {
FlatList,
Image,
ImageStyle,
Platform,
SectionList,
TextStyle,
View,
ViewStyle,
} from "react-native"
import { Link, RouteProp, useRoute } from "@react-navigation/native"
import { Drawer } from "react-native-drawer-layout"
import { ListItem } from "@/components/ListItem"
import { Screen } from "@/components/Screen"
import { Text } from "@/components/Text"
import { TxKeyPath, isRTL } from "@/i18n"
import { translate } from "@/i18n/translate"
import { MainTabParamList, MainTabScreenProps } from "@/navigators/navigationTypes"
import { useAppTheme } from "@/theme/context"
import { $styles } from "@/theme/styles"
import type { ThemedStyle } from "@/theme/types"
import { s, fs } from "@/utils/responsive"
import { useSafeAreaInsetsStyle } from "@/utils/useSafeAreaInsetsStyle"
import * as Demos from "./demos"
import { DrawerIconButton } from "./DrawerIconButton"
import SectionListWithKeyboardAwareScrollView from "./SectionListWithKeyboardAwareScrollView"
const logo = require("@assets/images/logo.png")
interface DemoListItem {
item: { name: string; useCases: string[] }
sectionIndex: number
handleScroll?: (sectionIndex: number, itemIndex?: number) => void
}
const slugify = (str: string) =>
str
.toLowerCase()
.trim()
.replace(/[^\w\s-]/g, "")
.replace(/[\s_-]+/g, "-")
.replace(/^-+|-+$/g, "")
/**
* Type-safe utility to check if an unknown object has a valid string property.
* This is particularly useful in React 19 where props are typed as unknown by default.
* The function safely narrows down the type by checking both property existence and type.
* @param props - The unknown props to check.
* @param propName - The name of the property to check.
* @returns Whether the property is a valid string.
*/
function hasValidStringProp(props: unknown, propName: string): boolean {
return (
props !== null &&
typeof props === "object" &&
propName in props &&
typeof (props as Record<string, unknown>)[propName] === "string"
)
}
const WebListItem: FC<DemoListItem> = ({ item, sectionIndex }) => {
const sectionSlug = item.name.toLowerCase()
const { themed } = useAppTheme()
return (
<View>
<Link screen="Showroom" params={{ queryIndex: sectionSlug }} style={themed($menuContainer)}>
<Text preset="bold">{item.name}</Text>
</Link>
{item.useCases.map((u) => {
const itemSlug = slugify(u)
return (
<Link
key={`section${sectionIndex}-${u}`}
screen="Showroom"
params={{ queryIndex: sectionSlug, itemIndex: itemSlug }}
>
<Text>{u}</Text>
</Link>
)
})}
</View>
)
}
const NativeListItem: FC<DemoListItem> = ({ item, sectionIndex, handleScroll }) => {
const { themed } = useAppTheme()
return (
<View>
<Text
onPress={() => handleScroll?.(sectionIndex)}
preset="bold"
style={themed($menuContainer)}
>
{item.name}
</Text>
{item.useCases.map((u, index) => (
<ListItem
key={`section${sectionIndex}-${u}`}
onPress={() => handleScroll?.(sectionIndex, index)}
text={u}
rightIcon={isRTL ? "caretLeft" : "caretRight"}
/>
))}
</View>
)
}
const ShowroomListItem = Platform.select({ web: WebListItem, default: NativeListItem })
const isAndroid = Platform.OS === "android"
export const ShowroomScreen: FC<MainTabScreenProps<"Showroom">> = function ShowroomScreen(_props) {
const [open, setOpen] = useState(false)
const timeout = useRef<ReturnType<typeof setTimeout>>(null)
const listRef = useRef<SectionList>(null)
const menuRef = useRef<FlatList<DemoListItem["item"]>>(null)
const route = useRoute<RouteProp<MainTabParamList, "Showroom">>()
const params = route.params
const { themed, theme } = useAppTheme()
const toggleDrawer = useCallback(() => {
if (!open) {
setOpen(true)
} else {
setOpen(false)
}
}, [open])
const handleScroll = useCallback((sectionIndex: number, itemIndex = 0) => {
try {
listRef.current?.scrollToLocation({
animated: true,
itemIndex,
sectionIndex,
viewPosition: 0.25,
})
} catch (e) {
console.error(e)
}
}, [])
// handle Web links
useEffect(() => {
if (params !== undefined && Object.keys(params).length > 0) {
const demoValues = Object.values(Demos)
const findSectionIndex = demoValues.findIndex(
(x) => x.name.toLowerCase() === params.queryIndex,
)
let findItemIndex = 0
if (params.itemIndex) {
try {
findItemIndex = demoValues[findSectionIndex].data({ themed, theme }).findIndex((u) => {
if (hasValidStringProp(u.props, "name")) {
return slugify(translate((u.props as { name: TxKeyPath }).name)) === params.itemIndex
}
return false
})
} catch (err) {
console.error(err)
}
}
handleScroll(findSectionIndex, findItemIndex)
}
}, [handleScroll, params, theme, themed])
const scrollToIndexFailed = (info: {
index: number
highestMeasuredFrameIndex: number
averageItemLength: number
}) => {
listRef.current?.getScrollResponder()?.scrollToEnd()
timeout.current = setTimeout(
() =>
listRef.current?.scrollToLocation({
animated: true,
itemIndex: info.index,
sectionIndex: 0,
}),
50,
)
}
useEffect(() => {
return () => {
if (timeout.current) {
clearTimeout(timeout.current)
}
}
}, [])
const $drawerInsets = useSafeAreaInsetsStyle(["top"])
return (
<Drawer
open={open}
onOpen={() => setOpen(true)}
onClose={() => setOpen(false)}
drawerType="back"
drawerPosition={isRTL ? "right" : "left"}
renderDrawerContent={() => (
<View style={themed([$drawer, $drawerInsets])}>
<View style={themed($logoContainer)}>
<Image source={logo} style={$logoImage} />
</View>
<FlatList<DemoListItem["item"]>
ref={menuRef}
contentContainerStyle={themed($listContentContainer)}
data={Object.values(Demos).map((d) => ({
name: d.name,
useCases: d.data({ theme, themed }).map((u) => {
if (hasValidStringProp(u.props, "name")) {
return translate((u.props as { name: TxKeyPath }).name)
}
return ""
}),
}))}
keyExtractor={(item) => item.name}
renderItem={({ item, index: sectionIndex }) => (
<ShowroomListItem {...{ item, sectionIndex, handleScroll }} />
)}
/>
</View>
)}
>
<Screen
preset="fixed"
contentContainerStyle={$styles.flex1}
{...(isAndroid ? { KeyboardAvoidingViewProps: { behavior: undefined } } : {})}
>
<DrawerIconButton onPress={toggleDrawer} />
<SectionListWithKeyboardAwareScrollView
ref={listRef}
contentContainerStyle={themed($sectionListContentContainer)}
stickySectionHeadersEnabled={false}
sections={Object.values(Demos).map((d) => ({
name: d.name,
description: d.description,
data: [d.data({ theme, themed })],
}))}
renderItem={({ item, index: sectionIndex }) => (
<View>
{item.map((demo: ReactElement, demoIndex: number) => (
<View key={`${sectionIndex}-${demoIndex}`}>{demo}</View>
))}
</View>
)}
renderSectionFooter={() => <View style={themed($demoUseCasesSpacer)} />}
ListHeaderComponent={
<View style={themed($heading)}>
<Text preset="heading" tx="showroomScreen:jumpStart" />
</View>
}
onScrollToIndexFailed={scrollToIndexFailed}
renderSectionHeader={({ section }) => {
return (
<View>
<Text preset="heading" style={themed($demoItemName)}>
{section.name}
</Text>
<Text style={themed($demoItemDescription)}>{translate(section.description)}</Text>
</View>
)
}}
/>
</Screen>
</Drawer>
)
}
const $drawer: ThemedStyle<ViewStyle> = ({ colors }) => ({
backgroundColor: colors.background,
flex: 1,
})
const $listContentContainer: ThemedStyle<ViewStyle> = ({ spacing }) => ({
paddingHorizontal: spacing.lg,
})
const $sectionListContentContainer: ThemedStyle<ViewStyle> = ({ spacing }) => ({
paddingHorizontal: spacing.lg,
})
const $heading: ThemedStyle<ViewStyle> = ({ spacing }) => ({
marginBottom: spacing.xxxl,
})
const $logoImage: ImageStyle = {
height: s(42),
width: s(77),
}
const $logoContainer: ThemedStyle<ViewStyle> = ({ spacing }) => ({
alignSelf: "flex-start",
justifyContent: "center",
height: s(56),
paddingHorizontal: spacing.lg,
})
const $menuContainer: ThemedStyle<ViewStyle> = ({ spacing }) => ({
paddingBottom: spacing.xs,
paddingTop: spacing.lg,
})
const $demoItemName: ThemedStyle<TextStyle> = ({ spacing }) => ({
fontSize: fs(24),
marginBottom: spacing.md,
})
const $demoItemDescription: ThemedStyle<TextStyle> = ({ spacing }) => ({
marginBottom: spacing.xxl,
})
const $demoUseCasesSpacer: ThemedStyle<ViewStyle> = ({ spacing }) => ({
paddingBottom: spacing.xxl,
})